diff options
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src')
63 files changed, 25910 insertions, 0 deletions
diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java new file mode 100644 index 0000000..cc3af69 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java @@ -0,0 +1,335 @@ +/* +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.util.Enumeration; + +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; + +/** +* ActionAssociation binds any ActionEvent broadcaster +* (typically Buttons and the like) to a display group. +* Actions are invoked on all selected objects in the +* display group bound to the action aspect. +* Bindings are: +* <ul> +* <li>action: a method to be invoked on selected objects. +* If the argument aspect is bound, the method must take +* one argument. Otherwise, the method must take no arguments.</li> +* <li>argument: the attribute of the selected object(s) (possibly +* from a different display group) that will be used as an argument +* to the action method</li> +* <li>enabled: a boolean property that determines whether +* the controlled component is enabled</li> +* <li>visible: a boolean property that determines whether +* the controlled component is visible</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ActionAssociation extends EOAssociation + implements ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ActionAspect, ArgumentAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "target" + } ); + + static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public ActionAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 + ( addActionListener.implementedByObject( anObject ) ) + && ( removeActionListener.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. + */ + public static String primaryAspect () + { + return ActionAspect; + } + + /** + * 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. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + try + { + addActionListener.invoke( object(), this ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOActionAssociation: " + + "could not add action listener to object:" + object() ); + } + super.establishConnection(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + try + { + removeActionListener.invoke( object(), this ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOActionAssociation: " + + "could not add action listener to object:" + object() ); + } + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + * This implementation does nothing. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + + if ( component instanceof Component ) + { + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + Object value = + displayGroup.selectedObjectValueForKey( 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() ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( VisibleAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + if ( converted.booleanValue() != + ((Component)component).isVisible() ) + { + ((Component)component).setVisible( + converted.booleanValue() ); + } + } + } + } + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup actionDisplayGroup = null; + String actionKey = null; + + // action aspect + actionDisplayGroup = displayGroupForAspect( ActionAspect ); + if ( actionDisplayGroup != null ) + { + actionKey = displayGroupKeyForAspect( ActionAspect ); + + //TODO: argument aspect not implemented + + try + { + + NSSelector selector = new NSSelector( actionKey ); + Enumeration e = + actionDisplayGroup.selectedObjects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + selector.invoke( e.nextElement() ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "ActionAssociation: error invoking action: " + actionKey, exc ); + } + } + } +} + +/* + * $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.4 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:48:28 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java new file mode 100644 index 0000000..2dc7fec --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java @@ -0,0 +1,327 @@ +/* +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.Adjustable; +import java.awt.Component; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* AdjustableAssociation binds any Adjustable component to +* a display group. Components implementing the adjustable +* interface include: ScrollBar and JScrollBar. +* Bindings are: +* <ul> +* <li>value: a property convertable to/from a string</li> +* <li>enabled: a boolean property that determines whether +* the user can select the text in the field</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class AdjustableAssociation extends EOAssociation + implements AdjustmentListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "value" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public AdjustableAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof Adjustable ); + } + + /** + * 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 as a FocusListener to the specified object. + */ + public void establishConnection () + { + component().addAdjustmentListener( this ); + 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 () + { + component().removeAdjustmentListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + Adjustable component = component(); + 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 ) ); + } + value = displayGroup.selectedObjectValueForKey( key ); + + // convert value to int + value = ValueConverter.convertObjectToClass( + value, Integer.class ); + + int intValue; + if ( value == null ) + { + intValue = 0; + } + else + { + intValue = ((Integer)value).intValue(); + } + + if ( component.getValue() != intValue ) + { + component.setValue( intValue ); + } + } + + // 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() ); + } + } + + } + + /** + * 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() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Integer( component().getValue() ); + return displayGroup.setSelectedObjectValue( value, key ); + } + return false; + } + + // interface AdjustmentListener + + /** + * Updates object on action performed. + */ + public void adjustmentValueChanged(AdjustmentEvent e) + { + writeValueToDisplayGroup(); + } + + private Adjustable component() + { + return (Adjustable) object(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:48:35 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java new file mode 100644 index 0000000..38cf38b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java @@ -0,0 +1,444 @@ +/* +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.util.Iterator; + +import javax.swing.ButtonModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +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; + +/** +* ButtonAssociation binds any component that uses a ButtonModel +* (all Swing button classes) to a display group. This association +* should be used to handle individual JRadioButtons and JCheckBoxes. +* Bindings are: +* <ul> +* <li>value: a boolean property that determines the +* selected state of the button model. This will set +* the value for radio buttons and check boxes.</li> +* <li>enabled: a boolean property that determines the +* enabled state of the button model.</li> +* <li>visible: a boolean property that determines the +* visible state of the button model.</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ButtonAssociation extends EOAssociation + implements ChangeListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "model.selected" + } ); + + static NSSelector getModel = + new NSSelector( "getModel", new Class[] {} ); + + protected ButtonModel buttonModel; + protected boolean lastKnownValue; + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + * This implementation expects a ButtonModel or a class + * that has a "getModel" method that returns a ButtonModel. + */ + public ButtonAssociation ( Object anObject ) + { + super( anObject ); + + if ( anObject instanceof ButtonModel ) + { + buttonModel = (ButtonModel) anObject; + } + else + { + try + { + buttonModel = (ButtonModel) getModel.invoke( anObject ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOButtonAssociation: " + + "could not retrieve a button model from object:" + anObject ); + } + } + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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. + * This implementation expects a ButtonModel or a class + * that has a "getModel" method that returns a ButtonModel. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return + ( anObject instanceof ButtonModel ) + || ( getModel.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. + */ + 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. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + buttonModel.addChangeListener( this ); + super.establishConnection(); + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + buttonModel.removeChangeListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + * This implementation does nothing. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + } + + Object value; + 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 // displayGroup has only one object + { + value = + displayGroup.selectedObjectValueForKey( key ); + } // end checking size of displayGroup + + buttonModel.setArmed( false ); + buttonModel.setPressed( false ); + + if ( value != null ) + { + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + lastKnownValue = converted.booleanValue(); + if ( converted.booleanValue() != + buttonModel.isSelected() ) + { + buttonModel.removeChangeListener( this ); + buttonModel.setSelected( + converted.booleanValue() ); + buttonModel.addChangeListener( this ); + } + } // end checking converted == null + } + else + { + buttonModel.setArmed( true ); + buttonModel.setPressed( true ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != + buttonModel.isEnabled() ) + { + buttonModel.removeChangeListener( this ); + buttonModel.setEnabled( + converted.booleanValue() ); + buttonModel.addChangeListener( this ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( VisibleAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + if ( converted.booleanValue() != + ((Component)component).isVisible() ) + { + ((Component)component).setVisible( + converted.booleanValue() ); + } + } + } + } + + /** + * 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() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + boolean returnValue = true; + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Boolean( buttonModel.isSelected() ); + + 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 ChangeListener + + public void stateChanged(ChangeEvent e) + { + if ( buttonModel.isSelected() != lastKnownValue ) + { + lastKnownValue = buttonModel.isSelected(); + writeValueToDisplayGroup(); + } + } + +} + +/* + * $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.8 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.5 2001/06/29 22:28:19 mpowers + * Tabs to spaces. + * + * Revision 1.4 2001/06/29 22:17:31 mpowers + * Now updating the component on establishConnection. + * + * Revision 1.3 2001/02/27 02:10:38 mpowers + * No longer updating values to the display group if the value + * has not changed. + * + * Revision 1.2 2001/02/21 20:33:01 mpowers + * Fixed bug with change listener. + * + * Revision 1.1.1.1 2000/12/21 15:48:38 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java new file mode 100644 index 0000000..d0a087e --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java @@ -0,0 +1,700 @@ +/* +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.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ComboBoxAssociation binds JComboBoxes to +* display groups. Bindings are: +* <ul> +* +* <li>value: optional - a property of the selected object in the +* display group that will be bind to the item the user +* selects or the text that the user enters in the field. +* If the value aspect is not bound, then the combo box works +* as an "overview" assocation and changing the selected object +* in the combobox will modify the selection of the display group +* bound to the objects or the titles display groups (in that order).</li> +* +* <li>titles: optional - a property of the objects in the bound +* display group that will appear in the list. If the +* objects aspect is not bound, this property is also +* used to populate the value binding. If the titles +* aspect itself is not bound, the items already in the +* combobox will be used to update the value in the +* selected object in the bound display group.</li> +* +* <li>objects: optional - if specified, when the user +* selects a title in the list, the property of the +* object at the corresponding index of the bound display +* group will be used to populate the value binding. +* If the objects aspect is used with an editable combo +* box, any value entered that does not match one of the +* titles in the list will produce a null value.</li> +* +* <li>enabled: optional - a boolean property of the +* selected object in the display group that determines whether +* the user can edit the field.</li> +* +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ComboBoxAssociation extends EOAssociation + implements FocusListener, ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ValueAspect, + ObjectsAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text" + } ); + + private boolean wasNull; + private static final String EMPTY_STRING = ""; + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public ComboBoxAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof JComboBox ); + } + + /** + * 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. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + super.establishConnection(); + + // prepopulate titles + EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + addAsListener(); + subjectChanged(); + } + + protected void addAsListener() + { + component().addActionListener( this ); + component().addFocusListener( this ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void removeAsListener() + { + component().removeActionListener( this ); + component().removeFocusListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + removeAsListener(); + + JComboBox component = component(); + EODisplayGroup displayGroup; + String key; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + ComboBoxModel model = component().getModel(); + // if first time, or if backing group has changed +// if ( ( ! ( model instanceof ComboBoxAssociationModel ) ) +// || ( displayGroup.contentsChanged() ) ) +// { + key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); +// } + } + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + component.setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + //Object value = displayGroup.selectedObjectValueForKey( key ); + Object value; + + + if ( displayGroup.selectedObjects().size() > 1 ) + { + + 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 { + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + + // objects aspect + EODisplayGroup objectsDisplayGroup = + displayGroupForAspect( ObjectsAspect ); + if ( ( objectsDisplayGroup != null ) && ( value != null ) ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + Object match; + int index = NSArray.NotFound; + int count = objectsDisplayGroup.displayedObjects().count(); + for ( int i = 0; i < count; i++ ) + { + match = objectsDisplayGroup.valueForObjectAtIndex( i, objectKey ); + if ( value.equals( match ) ) + { + index = i; + } + } + if ( index == NSArray.NotFound ) + { + if ( component.getSelectedItem() != null ) + { + component.setSelectedItem( null ); + } + } + else + { + if ( component.getSelectedIndex() != index ) + { + component.setSelectedIndex( index ); + } + } + } + else + { + component.setSelectedItem( value ); + } + } + else // values aspect not bound + { + // use objects group if specified + EODisplayGroup sourceGroup = + displayGroupForAspect( ObjectsAspect ); + if ( sourceGroup == null ) + { + // fall back on titles group + sourceGroup = displayGroupForAspect( TitlesAspect ); + } + + if ( sourceGroup != null ) + { + List selection = sourceGroup.selectionIndexes(); + if ( ( selection != null ) && ( selection.size() > 0 ) ) + { + component.setSelectedIndex( ((Integer)selection.get(0)).intValue() ); + } + else + { + // the combo box model decides what to do with this value + component.setSelectedItem( null ); + } + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != component.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + addAsListener(); + } + + /** + * Called to repopulate the title list from the + * specified display group. + */ + protected void populateTitles( + EODisplayGroup displayGroup, String key ) + { + component().setModel( + new ComboBoxAssociationModel( displayGroup, key ) ); + } + + /** + * 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() + { + JComboBox component = component(); + EODisplayGroup displayGroup; + String key; + + // selected title aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + Object value = null; + + // selected object aspect, if any + EODisplayGroup objectsGroup = + displayGroupForAspect( ObjectsAspect ); + if ( objectsGroup != null ) + { + try + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + int index = component.getSelectedIndex(); + if ( index != -1 ) + { + value = objectsGroup + .valueForObjectAtIndex( index, objectKey ); + } + else // selected index is -1 + { + // the combo box is probably editable, + // so there is no corresponding object. + value = null; + } + } + catch ( NullPointerException npe ) + { + // catches NPE on line 436 of JComboBox.java: + // this is a common developer error + throw new WotonomyException( "ComboBoxAssociation: " + + "The object in the VALUE property may not have been found in the " + + "objects in the TITLES group.", npe ); + } + } + else // just use the selected item + { + value = component.getSelectedItem(); + } + + boolean returnValue = true; + if ( displayGroup.selectedObjects().size() == 1 ) + { // displayGroup has only one object + // only set value if changed + Object existingValue = displayGroup.selectedObjectValueForKey( key ); + if ( value == existingValue ) return true; + if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true; + + // value has changed: update the value. + return displayGroup.setSelectedObjectValue( value, key ); + } + else if ( displayGroup.selectedObjects().size() > 1 ) + { + // displayGroup has more than one object + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + + } // end checking size of displayGroup + + } + else // values aspect not bound + { + // use objects group if specified + EODisplayGroup sourceGroup = + displayGroupForAspect( ObjectsAspect ); + if ( sourceGroup == null ) + { + // fall back on titles group + sourceGroup = displayGroupForAspect( TitlesAspect ); + } + + if ( sourceGroup != null ) + { + int index = component.getSelectedIndex(); + if ( index != -1 ) + { + sourceGroup.setSelectionIndexes( new NSArray( new Integer( index ) ) ); + } + else + { + sourceGroup.setSelectedObject( null ); + } + return true; + } + } + + return false; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + writeValueToDisplayGroup(); + } + + // 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 ( component().isEditable() ) + { + if ( endEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + } + + // convenience + + private JComboBox component() + { + return (JComboBox) object(); + } + + /** + * Used as the data model for the controlled combo box. + */ + private class ComboBoxAssociationModel extends AbstractListModel + implements ComboBoxModel + { + EODisplayGroup displayGroup; + String key; + Object selectedItem; + + ComboBoxAssociationModel( + EODisplayGroup aDisplayGroup, String aKey ) + { + displayGroup = aDisplayGroup; + key = aKey; + selectedItem = null; + } + + public Object getElementAt(int index) + { + return displayGroup.valueForObjectAtIndex( index, key ); + } + + public int getSize() + { + return displayGroup.displayedObjects().count(); + } + + public void setSelectedItem(Object anItem) + { //System.out.println( "setSelectedItem: " + anItem ); + selectedItem = anItem; + + // must do this to notify an editable combo, + // otherwise the wrong value appears. + fireContentsChanged( this, -1, -1 ); + } + + public Object getSelectedItem() + { //System.out.println( "getSelectedItem: " + selectedItem ); + return selectedItem; + } + } +} + +/* + * $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.13 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.12 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.11 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.10 2001/07/23 20:17:56 mpowers + * Now works as an overview association if the values aspect is not bound. + * + * Revision 1.9 2001/06/30 14:57:29 mpowers + * Removed a println. + * + * Revision 1.8 2001/06/29 22:28:19 mpowers + * Tabs to spaces. + * + * Revision 1.7 2001/06/29 22:17:31 mpowers + * Now updating the component on establishConnection. + * + * Revision 1.6 2001/05/14 15:24:49 mpowers + * Only updating if change was made. Feels like I had fixed this here before. + * + * Revision 1.5 2001/04/09 21:41:08 mpowers + * Fixed a bug I thought that I had fixed before. + * + * Revision 1.4 2001/03/01 20:37:17 mpowers + * Updated docs to emphasize that titles aspect is optional. + * + * 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/10 17:01:08 mpowers + * Caught a common developer error. + * + * Revision 1.1.1.1 2000/12/21 15:48:43 mpowers + * Contributing wotonomy. + * + * Revision 1.8 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java new file mode 100644 index 0000000..ba50879 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java @@ -0,0 +1,613 @@ +/* +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: +* <ul> +* <li>value: a property convertable to/from a date</li> +* <li>editable: a boolean property that determines whether +* the user can edit the date in the component</li> +* <li>enabled: a boolean property that determines whether +* the component is enabled or disabled</li> +* </ul> +* +* @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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java new file mode 100644 index 0000000..290480d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java @@ -0,0 +1,134 @@ +/* +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.event.ActionEvent; +import java.awt.event.ActionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ActionAssociation binds any ActionEvent broadcaster +* (typically Buttons and the like) to a display group, +* but invokes actions directly on the bound display +* group rather than the selected objects. +* Bindings are: +* <ul> +* <li>action: a method to be invoked on the bound display group. +* If the argument aspect is bound, the method must take +* one argument. Otherwise, the method must take no arguments.</li> +* <li>argument: the attribute of the selected object(s) (possibly +* from a different display group) that will be used as an argument +* to the action method</li> +* <li>enabled: a boolean property that determines whether +* the controlled component is enabled</li> +* <li>visible: a boolean property that determines whether +* the controlled component is visible</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class DisplayGroupActionAssociation extends ActionAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ActionAspect, ArgumentAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "target" + } ); + + static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public DisplayGroupActionAssociation ( Object anObject ) + { + super( anObject ); + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup actionDisplayGroup = null; + String actionKey = null; + + // action aspect + actionDisplayGroup = displayGroupForAspect( ActionAspect ); + if ( actionDisplayGroup != null ) + { + actionKey = displayGroupKeyForAspect( ActionAspect ); + + //TODO: argument aspect not implemented + + try + { + NSSelector.invoke( actionKey, actionDisplayGroup ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "DisplayGroupActionAssociation: " + + "error invoking action: " + actionKey, exc ); + } + } + } + +} + +/* + * $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.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:48:46 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java new file mode 100644 index 0000000..c8ecd36 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java @@ -0,0 +1,120 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +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.BorderLayout; +import java.awt.Dimension; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.border.EmptyBorder; + +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.util.ObjectInspector; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* The DisplayGroupInspector displays a JFrame that +* shows allows you to view and manipulate a display group. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class DisplayGroupInspector +{ + protected JList list; + protected EODisplayGroup displayGroup; + +/** +* Displays and manipulats the specified display group. +*/ + public DisplayGroupInspector( EODisplayGroup aDisplayGroup ) + { + displayGroup = aDisplayGroup; + + list = new JList(); + list.addMouseListener( new MouseAdapter() + { + public void mouseClicked( MouseEvent e ) + { + if ( e.getClickCount() == 2 ) + { + Object selection = displayGroup.selectedObject(); + if ( selection != null ) + { + new ObjectInspector( selection ); + } + } + } + } ); + + EOAssociation assoc = new ListAssociation( list ); + assoc.bindAspect( EOAssociation.TitlesAspect, displayGroup, "" ); + assoc.establishConnection(); + + initLayout(); + + } + + protected void initLayout() + { + JPanel panel = new JPanel(); + panel.setLayout( new BorderLayout() ); + panel.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( list ); + scrollPane.setPreferredSize( new Dimension( 200, 200 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Display Group Inspector" ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } +} + +/* + * $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.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2003/06/26 23:28:00 mpowers + * Added double click. + * + * Revision 1.1 2001/05/29 19:57:47 mpowers + * Added some neglected files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java new file mode 100644 index 0000000..1756285 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java @@ -0,0 +1,1518 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import javax.swing.tree.TreePath; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource; + +/** +* DisplayGroupNodes are used as nodes in the +* TreeModelAssociation's implementation of TreeModel, +* and is tightly coupled with TreeModelAssociation +* and MasterDetailAssociation. <br><br> +* +* Even though it is no longer package access, +* don't rely on this class because we want to +* have the option of completely replacing this +* approach in the future. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + abstract public class DisplayGroupNode + extends EODisplayGroup + { + protected TreeModelAssociation parentAssociation; + protected EODelayedObserver targetObserver; + protected NSMutableDictionary childNodes; + protected EODisplayGroup parentGroup; + protected Object target; + protected boolean isFetched; + protected boolean isFetchNeeded; + protected boolean useParentOrderings; + protected boolean useParentQualifier; + + /** + * Constructor for all nodes. + * Root node must have a null target. + */ + public DisplayGroupNode( + TreeModelAssociation aParentAssociation, + EODisplayGroup aParentGroup, + Object aTarget ) + { +//new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget ); +//System.out.println( "DisplayGroupNode.new: " + aTarget ); + parentAssociation = aParentAssociation; + target = null; + targetObserver = null; + parentGroup = aParentGroup; + childNodes = new NSMutableDictionary(); + isFetched = false; + isFetchNeeded = false; + useParentOrderings = true; + useParentQualifier = true; + + EODataSource parentSource = null; + if ( parentGroup != null ) + { + parentSource = parentGroup.dataSource(); + } + else + if ( parentAssociation.titlesDisplayGroup != null ) + { + parentSource = parentAssociation.titlesDisplayGroup.dataSource(); + } + + // create child datasource + if ( aTarget != null ) // not root node + { + if ( parentAssociation.childrenKey != null ) + { + if ( parentSource == null ) + { + throw new WotonomyException( + "Need a data source when children aspect is bound." ); + } + + NSArray displayedObjects = parentGroup.displayedObjects(); + EODataSource childSource = parentSource.dataSourceQualifiedByKey( + parentAssociation.childrenKey ); + childSource.qualifyWithRelationshipKey( + parentAssociation.childrenKey, aTarget ); + + // create new display group using child data source + this.setDataSource( childSource ); + + // establish observer for target object + setTarget( aTarget ); + } + else // only titles is bound + { + // establish observer for target object + setTarget( aTarget ); + + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + return new NSArray(); + } + } ); + } + } + else // else root node + { + // root node uses PropertyDataSource by default + if ( parentSource == null ) + { + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + if ( parentGroup != null ) + { + return parentGroup.displayedObjects(); + } + return null; + } + } ); + } + else + { + // root node uses parent source directly + setDataSource( parentSource ); + } + } + } + + /** + * Overridden to unregister as an editor of the editing context, + * since we don't directly present a user interface. + */ + public void setDataSource ( EODataSource aDataSource ) + { + super.setDataSource( aDataSource ); + if ( ( aDataSource != null ) + && ( aDataSource.editingContext() != null ) ) + { + aDataSource.editingContext().removeEditor( this ); + } + } + + /** + * Returns whether the node should call fetch(). + */ + protected boolean isFetched() + { + if ( isFetchNeeded() ) + { + setFetchNeeded( false ); + fetch(); + } + return isFetched; + } + + /** + * Sets whether the node should call fetch(). + */ + protected void setFetched( boolean fetched ) + { +//System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetched = fetched; + } + + /** + * Returns whether the node is in need of a refetch. + */ + protected boolean isFetchNeeded() + { + return isFetchNeeded; + } + + /** + * Returns whether the node should call fetch(). + */ + protected void setFetchNeeded( boolean fetchNeeded ) + { +//System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetchNeeded = fetchNeeded; + } + + /** + * Subclasses should override this method to fire an appropriate insertion event. + */ + protected void fireNodesInserted( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesInserted: " + this ); + parentAssociation.fireTreeNodesInserted( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate change event. + */ + protected void fireNodesChanged( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesChanged: " + this ); + parentAssociation.fireTreeNodesChanged( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate deletion event. + */ + protected void fireNodesRemoved( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesRemoved: " + this ); + parentAssociation.fireTreeNodesRemoved( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate event. + */ + protected void fireStructureChanged( Object[] path, int[] indexes, Object[] objects ) + { + parentAssociation.fireTreeStructureChanged( + this, path, indexes, objects ); + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + int count = getChildCount(); // gets old count + if ( target == null ) + { + // if root node, forward to parent: + // circumventing delegating data source, if any + EODataSource dataSource = parentGroup.dataSource(); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( + ((DelegatingTreeDataSource)dataSource).delegateDataSource ); + } + parentGroup.insertObjectAtIndex( anObject, anIndex ); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( dataSource ); + } + return; // prevent event from firing (?) + } + else // not root node + { + super.insertObjectAtIndex( anObject, anIndex ); + } + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + boolean result; + Object node = getChildNodeAt( anIndex ); + if ( target == null ) + { + // if root node, forward to parent: + result = parentGroup.deleteObjectAtIndex( anIndex ); + } + else // not root node + { + result = super.deleteObjectAtIndex( anIndex ); + } + + return result; + } + + /** + * Returns the child node that corresponds to the + * specified index, creating it if necessary. + * The index must be within bounds or an exception + * is thrown. + */ + public DisplayGroupNode getChildNodeAt( int anIndex ) + { + boolean wasFetched = isFetched(); + if ( ! wasFetched ) fetch(); + Object o = displayedObjects.objectAtIndex( anIndex ); + DisplayGroupNode result = getChildNodeForObject( o ); + if ( result == null ) + { + result = createChildNodeForObject( o ); + } + return result; + } + + /** + * Returns a child node that corresponds to the + * specified object, returning null if not found. + */ + protected DisplayGroupNode getChildNodeForObject( Object anObject ) + { + return (DisplayGroupNode) + childNodes.objectForKey( new ReferenceKey( anObject ) ); + } + + /** + * Creates a child node that corresponds to the + * specified object. + */ + private DisplayGroupNode createChildNodeForObject( Object anObject ) + { + DisplayGroupNode result = parentAssociation.createNode( this, anObject ); + childNodes.setObjectForKey( result, new ReferenceKey( anObject ) ); + return result; + } + + /** + * Returns a tree path of all DisplayGroupNodes leading + * to this node, including the root node (but excluding the + * titles display group). + */ + public TreePath treePath() + { + List path = new LinkedList(); + EODisplayGroup node = this; + while ( node instanceof DisplayGroupNode ) + { + // insert at head of list + path.add( 0, node ); + node = ((DisplayGroupNode)node).parentGroup; + } + return new TreePath( path.toArray() ); + } + + /** + * Overridden to return the parent group's + * sort ordering if useParentOrderings is true. + * useParentOrderings is true by default. + */ + public NSArray sortOrderings() + { + if ( ( useParentOrderings ) + && ( parentGroup != null ) ) + { + return parentGroup.sortOrderings(); + } + return super.sortOrderings(); + } + + /** + * Overridden to set useParentOrderings to false, + * or true if aList is null. + */ + public void setSortOrderings ( List aList ) + { + if ( aList == null ) + { + useParentOrderings = true; + } + else + { + useParentOrderings = false; + super.setSortOrderings( aList ); + } + } + + /** + * Overridden to return the parent group's + * qualifier if useParentQualifier is true. + * useParentQualifier is true by default. + */ + public EOQualifier qualifier() + { + if ( ( useParentQualifier ) + && ( parentGroup != null ) ) + { + return parentGroup.qualifier(); + } + return super.qualifier(); + } + + /** + * Overridden to set useParentQualifier to false, + * or true if aList is null. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + if ( aQualifier == null ) + { + useParentQualifier = true; + } + else + { + useParentQualifier = false; + super.setQualifier( aQualifier ); + } + } + + /** + * Overridden to set isFetched to true. + */ + public boolean fetch() + { +//System.out.println( "DisplayGroupNode.fetch: " + this + " : " ); +//if ( getClass().getName().indexOf( "Activity" ) != -1 ) +//{ +// new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() ); +//} + // set flag + setFetched( true ); + + // skip root node + if ( target == null ) return true; + + // requalify + dataSource().qualifyWithRelationshipKey( + parentAssociation.childrenKey, target ); + + // call to super + return super.fetch(); + +//boolean result = super.fetch(); +//System.out.println( displayedObjects() ); +//return result; + } + + /** + * Returns the object at the appropriate index + * in the parent display group. + */ + public Object object() + { + // if root node + if ( target == null ) + { + return parentAssociation.rootLabel(); + } + return target; + } + + /** + * Returns the string value of the title property + * on the object in the parent display group corresponding + * to this index. The tree renderer asks JTrees to + * call this method to retrieve a value for display. + */ + public String toString() + { + Object result = getUserObject(); + if ( result == null ) result = "[null]"; + return result.toString(); + } + + // parts of interface TreeNode + + public int getChildCount() + { + if ( ! isFetched() ) fetch(); +//if ( toString().indexOf("154.16406")!=-1){ +//System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this ); +//new RuntimeException().printStackTrace(); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//} + return displayedObjects.count(); + } + + public int getIndex(DisplayGroupNode node) + { + if ( ! isFetched() ) fetch(); + return displayedObjects.indexOfObject( + ((DisplayGroupNode)node).target ); + } + + public boolean getAllowsChildren() + { + return true; + } + + public boolean isLeaf() + { + // if not root node and isLeaf aspect is bound + if ( ( target != null ) + && ( parentGroup != null ) + && ( parentAssociation.leafKey != null ) ) + { + Object value; + if ( parentAssociation.leafDisplayGroup != null ) + { + value = parentGroup.valueForObject( + target, parentAssociation.leafKey ); + } + else + { + value = parentAssociation.leafKey; + } + + // getBoolean returns true for zero, among other things + Object result = ValueConverter.getBoolean( value ); + if ( result != null ) + { + return ((Boolean)result).booleanValue(); + } + } + + // otherwise, we have to fetch and return count + return ( getChildCount() == 0 ); + } + + public Enumeration children() + { + int count = getChildCount(); + Vector v = new Vector(); + for ( int i = 0; i < count; i++ ) + { + v.add( getChildNodeAt( i ) ); + } + return v.elements(); + } + + // parts of interface MutableTreeNode + + public void insert(DisplayGroupNode aChild, int anIndex) + { + insertObjectAtIndex( + ((DisplayGroupNode)aChild).object(), anIndex ); + } + + public void remove(int index) + { + deleteObjectAtIndex( index ); + } + + /** + * Removes the node at the index corresponding + * to the index of the object. + */ + public void remove(DisplayGroupNode node) + { + remove( getIndex( node ) ); + } + + /** + * Removes our object from the parent display group. + */ + public void removeFromParent() + { + int index = parentGroup.displayedObjects().indexOfIdenticalObject( target ); + if ( index != NSArray.NotFound ) + { + parentGroup.deleteObjectAtIndex( index ); + } + else + { + throw new WotonomyException( + "Object not found in parent group: " + target ); + } + } + + /** + * Removes our object from the parent display group + * and adds it to the end of the specified node's children. + */ + public void setParent(DisplayGroupNode newParent) + { + removeFromParent(); + newParent.insertObjectAtIndex( + object(), newParent.displayedObjects.size() ); + } + + /** + * Returns the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public Object getUserObject() + { + return valueForKey( parentAssociation.titlesKey ); + } + + /** + * Sets the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public void setUserObject( Object aValue ) + { + setValueForKey( aValue, parentAssociation.titlesKey ); + } + + /** + * Returns a value from the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, if the titles key is specified, the root + * label is returned, otherwise null is returned. + */ + public Object valueForKey( String aKey ) + { + // if root node + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + return parentAssociation.rootLabel(); + } + return null; + } + return parentGroup.valueForObject( target, aKey ); + } + + /** + * Sets a value on the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, this method only works if aKey is the + * titlesAspect's key, otherwise does nothing. + */ + public void setValueForKey(Object aValue, String aKey) + { + // if root node, return. + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + parentAssociation.setRootLabel( aValue ); + + // how to handle root node? tree event docs don't say. + fireNodesChanged ( treePath().getPath(), + new int[] { 0 }, + new Object[] { this } ); + } + return; + } + + parentGroup.setValueForObject( + aValue, target, aKey ); + } + + /** + * Perform any clean up in this method. + * The node will not be reused after this method is called. + * This implementation removes itself from the parent's + * set of child nodes, sets target and datasource to null, + * and then calls disposeChildNodes(). + */ + protected void dispose() + { //System.out.println( "dispose: " + this.getClass().getName() + " : " + this ); + if ( parentGroup != null ) + { + ((DisplayGroupNode)parentGroup).childNodes.remove( + new ReferenceKey( target ) ); + } + setTarget( (Object) null ); + setDataSource( null ); + disposeChildNodes(); + } + + /** + * Calls dispose() on all child nodes. + */ + protected void disposeChildNodes() + { + Iterator i = new LinkedList(childNodes.values()).iterator(); + while ( i.hasNext() ) + { + ((DisplayGroupNode) i.next()).dispose(); + } + } + + /** + * Called after the target object posts a change notification. + * This implementation re-fetches which triggers + * updateDisplayedObjects to broadcast any tree events. + * This method marks the parent object as changed if: + * (1) this object is not registered in the editing context + * of the titles display group's data source (if any), AND + * (2) the children key is not in the list of attributes + * of the parent object's EOClassDescription. + */ + public void targetChanged() + { + // if not root node + if ( target != null ) + { + // if we're not root and not fetched, stop here. + //FIXME: with this, some nodes have old values when moved. + //FIXME: without this, nodes are unnecessarily fetched. + //FIXME: might have parent modify isFetched of certain child nodes. + if ( isFetched() ) + { + fetch(); + } + else // not fetched - just update the display + { + updateDisplayedObjects(); + } +/* +//disabling this for performance reasons: +//might reenable later or find an alternate approach + // check to see if we need to mark the parent object as changed + EOEditingContext context = dataSource().editingContext(); + if ( ( context == null ) + || ( context.globalIDForObject( target ) == null ) ) + { + DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup; + if ( parentNode.target != null ) + { + // only notify if childrenKey is an attribute of parentDesc + // (and therefore not a toOne or toMany relationship) + EOClassDescription parentDesc = + EOClassDescription.classDescriptionForClass( + parentNode.target.getClass() ); + if ( parentDesc.attributeKeys().contains( parentAssociation.childrenKey ) ) + { + // only notify if no context is already observing the object + // and we are an attribute key + EOObserverCenter.notifyObserversObjectWillChange( parentNode.target ); + } + } + } +*/ + } + else // root node + { + setObjectArray( parentAssociation.titlesDisplayGroup.displayedObjects() ); + } + + // finally, broadcast change event for this node + // even though we're not sure if the displayed value changed. + fireNodeChanged(); + } + + /** + * Fires a change event for this node. + */ + public void fireNodeChanged() + { + // if not root node + if ( target != null ) + { + int index = ((DisplayGroupNode)parentGroup).getIndex( this ); + if ( ( index != -1 ) + && ( treePath().getParentPath() != null ) ) + { + fireNodesChanged ( + treePath().getParentPath().getPath(), + new int[] { index }, + new Object[] { this } ); + } + } + } + +Object[] previouslyDisplayedObjects = new Object[0]; + /** + * Overridden to call to super, fire any tree events, and then + * call updateDisplayedObjects on all fetched child nodes. + * This method compares this node's displayed objects against + * the list of child nodes, synchronizes them, and then broadcasts + * only the necessary events to bring the view component up to date. + */ + public void updateDisplayedObjects() + { +//System.out.println( "updateDisplayedObjects: " + " : " + this ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//new RuntimeException().printStackTrace(); + super.updateDisplayedObjects(); + + // diff lists + boolean proceed = true; + Object[] oldObjects = previouslyDisplayedObjects; + Object[] newObjects = displayedObjects.toArray(); + if ( oldObjects.length == newObjects.length ) + { + proceed = false; + for ( int i = 0; i < newObjects.length; i++ ) + { + if ( oldObjects[i] != newObjects[i] ) + { + proceed = true; + break; + } + } + } + + // this should be set before firing the change events + // in case some clients end up calling this again. + previouslyDisplayedObjects = newObjects; + + DisplayGroupNode node; + Iterator i = childNodes.values().iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + if ( !node.isFetchNeeded() ) + { + node.updateDisplayedObjects(); + } + } + + if ( proceed ) + { +//System.out.println( "DisplayGroupNode.firingEventsForChanges: " ); +//new RuntimeException().printStackTrace(); + fireEventsForChanges( oldObjects, newObjects ); + } + + } + + /** + * Called by processRecentChanges to analyze the + * differences between the lists and broadcast the + * appropriate events. + */ + protected void fireEventsForChanges( + Object[] oldObjects, Object[] newObjects ) + { + // structure changed causes havoc while + // establishing connection in some cases + //if ( oldObjects.length == 0 || newObjects.length == 0 ) + //{ + // fireStructureChanged( treePath().getPath(), null, null ); + // return; + //} + + int insertCount = 0; + int deleteCount = 0; + Object[] inserts = new Object[ newObjects.length ]; + Object[] deletes = new Object[ oldObjects.length ]; + + int i; + int n = -1, o = -1; // last match + int n1 = 0, o1 = 0; // current match test + int n2 = 0, o2 = 0; // scan ahead + + while ( o1 < oldObjects.length && n1 < newObjects.length ) + { + if ( newObjects[n1] == oldObjects[o1] ) + { + // mark as match and continue + o = o1; + n = n1; + } + else + { + // scan ahead for the next match, if any + o2 = o1; + n2 = n1; + + while ( o2 < oldObjects.length || n2 < newObjects.length ) + { + if ( o2 < oldObjects.length && newObjects[n1] == oldObjects[o2] ) + { + // run o1 to o2: mark as deletes + for ( i = o1; i < o2; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + o1 = o2; // reset test + o = o1; // set match + n = n1; // set match + break; + } + if ( n2 < newObjects.length && newObjects[n2] == oldObjects[o1] ) + { + // run n1 to n2: mark as inserts + for ( i = n1; i < n2; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + n1 = n2; // reset test + n = n1; // set match + o = o1; // set match + break; + } + o2++; + n2++; + } + } + if (n != n1) + { + inserts[n1] = newObjects[n1]; + insertCount++; + deletes[o1] = oldObjects[o1]; + deleteCount++; + //increment even though no match: + //the new object was marked as inserted and + //the old object was marked as deleted. + n = n1; + o = o1; + } + o1++; + n1++; + } + + // run o to end of oldObjects: mark as deletes + for ( i = o+1; i < oldObjects.length; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + + // run n to end of newObjects: mark as inserts + for ( i = n+1; i < newObjects.length; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + +//System.out.println( "done : " +//+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 ); +//System.out.println( new NSArray( newObjects ) ); +//System.out.println( new NSArray( inserts ) ); +//System.out.println( new NSArray( deletes ) ); +//System.out.println( new NSArray( oldObjects ) ); + + int c; + Object[] nodes; + int[] indices; + + // broadcast delete event + c = 0; + nodes = new Object[ deleteCount ]; + indices = new int[ deleteCount ]; + for ( i = 0; i < deletes.length; i++ ) + { + if ( deletes[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( deletes[i] ); + c++; + } + } + if ( c > 0 ) + { +// fireNodeChanged(); // force the jtree to get the correct child count + fireNodesRemoved( treePath().getPath(), indices, nodes ); + } + deletes = nodes; // retain for dispose check + + // broadcast insert event + c = 0; + nodes = new Object[ insertCount ]; + indices = new int[ insertCount ]; + for ( i = 0; i < inserts.length; i++ ) + { + if ( inserts[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( inserts[i] ); + if ( nodes[c] == null ) + { + nodes[c] = createChildNodeForObject( newObjects[i] ); + } + c++; + } + } + if ( c > 0 ) + { + fireNodesInserted( treePath().getPath(), indices, nodes ); + } + + // dispose any delete nodes not on insert list + int j; + boolean found; + for ( i = 0; i < deletes.length; i++ ) + { + for ( j = 0; j < nodes.length; j++ ) + { + if ( deletes[i] == nodes[j] ) break; + } + + // did not break early, so not found, so dispose + if ( j == nodes.length ) + { + ((DisplayGroupNode)deletes[i]).dispose(); + } + } + } + + /** + * Sets the target object and creates an registers a target observer. + * If target was not previously null, the existing observer is unregistered. + * Protected access so subclasses and TreeModelAssociation can update our target. + */ + public void setTarget( Object aTarget ) + { + if ( target != null ) + { + EOObserverCenter.removeObserver( targetObserver, target ); + targetObserver.discardPendingNotification(); + } + + if ( aTarget != null ) + { + target = aTarget; + targetObserver = new TargetObserver( this ); + EOObserverCenter.addObserver( targetObserver, target ); + } + } + + /** + * Returns the parent display group, or null if parent is root. + */ + public DisplayGroupNode getParentGroup() + { + if ( parentGroup instanceof DisplayGroupNode ) + { + return (DisplayGroupNode)parentGroup; + } + // presumably the root node + return null; + } + + /** + * Gets all descendants of the this node. + */ + public List getDescendants() + { + return getDescendants( this, true ); + } + + /** + * Gets only the descendants of the this node + * whose children has been loaded - no fetching + * will occur. Useful for load-on-demand trees. + */ + public List getLoadedDescendants() + { + return getDescendants( this, false ); + } + + // breadth first traversal implementation + + /** + * Returns a list of all descendants of the + * specified node. Unfetched nodes are traversed + * only if forceLoad is true. + * This implementation is a breadth-first traversal + * of the nodes starting at the specified node. + */ + static private List getDescendants( DisplayGroupNode aNode, boolean forceLoad ) + { + if ( !forceLoad && !aNode.isFetched ) return NSArray.EmptyArray; + + LinkedList result = new LinkedList(); + LinkedList queue = new LinkedList(); + + queue.add( aNode ); + while ( ! queue.isEmpty() ) + { + checkNode( (DisplayGroupNode) queue.removeFirst(), + queue, result, forceLoad ); + } + + return result; + } + + /** + * Adds each fetched child node of the specified node to + * the result set (optionally forcing the child node to load) + * and adding child node to the end of the queue. + */ + static private void checkNode( DisplayGroupNode aNode, + LinkedList aQueue, LinkedList aResult, boolean forceLoad ) + { + DisplayGroupNode child; + int count = aNode.getChildCount(); + + for ( int i = 0; i < count; i++ ) + { + child = aNode.getChildNodeAt( i ); + + // add to queue if node has fetched children + if ( ( !child.isFetched ) && ( forceLoad ) ) + { + child.fetch(); + } + if ( child.isFetched ) + { + aQueue.addLast( child ); + } + + aResult.add( child ); + } + } + + /** + * Overridden to not fetch on InvalidateAllObjectsInStoreNotification + * unless we've already been fetched, preserving the load-on-demand + * functionality. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + else + if ( ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + || ( EOEditingContext.EditingContextDidSaveChangesNotification + .equals( aNotification.name() ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // if our target object was deleted + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + if ( deletes.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was invalidated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + if ( invalidates != null && + invalidates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was updated, set fetchNeeded plus fire changed event + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + if ( updates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + if ( object() instanceof Component ) ((Component)object()).repaint(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + } + + super.objectsInvalidatedInEditingContext( aNotification ); + + } + + // inner classes + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + */ + private class ReferenceKey + { + private int hashCode; + private Object referent; + + public ReferenceKey( Object anObject ) + { + referent = anObject; + hashCode = anObject.hashCode(); + } + + /** + * Returns the actual key's hash code. + */ + public int hashCode() + { + return hashCode; + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( anObject instanceof ReferenceKey ) + { + return ((ReferenceKey)anObject).referent == referent; + } + return false; + } + } + + /** + * A private class to observe the target object of this node. + */ + private class TargetObserver extends EODelayedObserver + { + Reference ref; + + /** + * Pass in the display group node that will be updated + * when the target changes. + */ + public TargetObserver( DisplayGroupNode aDisplayGroup ) + { + ref = new WeakReference( aDisplayGroup ); + } + + /** + * Repopulate our display group, and calculate the deltas + * so we can broadcast appropriate events. + */ + public void subjectChanged () + { + DisplayGroupNode node = (DisplayGroupNode) ref.get(); + if ( node == null ) return; // node is null if gc'd. + //FIXME: should un-register self from observer center?? + + node.targetChanged(); + } + } + +} +/* + * $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.64 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.63 2003/06/06 14:20:07 mpowers + * getLoadedDescendants was forcing a fetch of the node it was called on. + * + * Revision 1.62 2003/06/03 14:48:33 mpowers + * Clean-up of notification handling for updates/invalidation/etc. + * Now fetching immediately on notification if the node is visible. + * This averts the infamous IndexOutOfBoundsException that occurs + * if fetching happens during repaint, because the BasicTreeUI is + * caching the number of child nodes before painting begins. + * + * Revision 1.61 2003/01/18 23:33:29 mpowers + * Fixing the build. + * + * Revision 1.60 2002/05/31 15:03:10 mpowers + * Fixes for the previous fix. Fat props to yjcheung. + * + * Revision 1.59 2002/05/28 15:31:36 mpowers + * Fix for updateDisplayedObjects for a subtle case where a node appears in + * the position that another node was moved from. + * + * Revision 1.58 2002/05/24 14:42:02 mpowers + * Prevent repeat events from firing if firing events loops back. + * + * Revision 1.57 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.56 2002/04/19 21:18:45 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.55 2002/04/19 20:53:22 mpowers + * Now firing event fewer events in fireEventsForChanges. + * + * Revision 1.54 2002/04/15 21:52:50 mpowers + * Tightening up TreeModelAssociation and DisplayGroupNode. + * Now only firing root structure changed once. + * Now disposing of root's children. + * Better event coalescing. + * + * Revision 1.53 2002/04/12 20:35:20 mpowers + * Now correctly setting parent display group and data source on creation. + * + * Revision 1.52 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.51 2002/04/03 20:13:36 mpowers + * Now differentiating between node instantiation caused by model expansion + * (user initiated) and by modifications to the model. + * Dispose now disposes all children. + * + * Revision 1.50 2002/03/23 16:20:27 mpowers + * Optimized processRecentChanges, minimized tree events. + * + * Revision 1.49 2002/03/11 03:15:06 mpowers + * Optimized processRecentChanges, minimize event firing, coalescing changes. + * Still need a better diff algorithm to avoid removing nodes. + * + * Revision 1.48 2002/03/10 00:59:39 mpowers + * Interim version: coalesces calls to process recent changes. + * Still does not handle rearranged nodes. + * + * Revision 1.47 2002/03/09 17:33:45 mpowers + * Nodes now track their child nodes by reference, not index. + * + * Revision 1.46 2002/03/08 23:19:07 mpowers + * Added getParentGroup to DisplayGroupNode. + * + * Revision 1.45 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.44 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.42 2002/02/19 22:28:46 mpowers + * DisplayGroupNodes immediately unregister themselves as editors. + * + * Revision 1.41 2002/02/13 16:27:38 mpowers + * Exposing setTarget. + * + * Revision 1.40 2001/11/02 20:55:46 mpowers + * Now using fixed index to send node removed events. This preserves the + * expanded state of the nodes in the corresponding jtree. + * + * Revision 1.39 2001/09/21 21:09:25 mpowers + * Exposed more fields as protected. + * + * Revision 1.38 2001/09/19 15:36:08 mpowers + * Refined behavior for isFetched after notification handling. + * + * Revision 1.37 2001/09/13 14:51:18 mpowers + * DisplayGroupNodes now dispose themselves and mark their parent for update + * when they receive notification that their target has been deleted. + * + * Revision 1.36 2001/09/10 14:10:24 mpowers + * Fix for notification handling. + * + * Revision 1.35 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.34 2001/07/18 22:13:39 mpowers + * getLoadedDescendants now works as advertised. + * Now correctly handling invalidateAllObjects notification. + * + * Revision 1.33 2001/07/18 13:03:32 mpowers + * TreeNodes now refetch only on demand. Previously, once a node had + * been fetched, it was always refetched after an invalidate, even if + * the node was not being displayed. + * + * Revision 1.32 2001/06/18 14:10:28 mpowers + * Cleaned up event firing: no longer firing insert or remove events twice. + * + * Revision 1.31 2001/06/09 16:15:39 mpowers + * Revised the targetChanged scheme because oldObjects and newObjects were + * identical after the target object is invalidated. + * + * Revision 1.30 2001/05/21 22:17:19 mpowers + * Fix for tree out-of-synch problems when nodes are inserted. + * + * Revision 1.29 2001/05/18 21:07:46 mpowers + * Playing with refresh options. + * + * Revision 1.28 2001/05/14 15:25:43 mpowers + * DisplayGroupNodes now only respond to InvalidateAllObjectsInStore + * if they are already fetched. + * + * Revision 1.27 2001/05/08 19:55:58 mpowers + * Fix for node children not refreshing after sibling was inserted. + * + * Revision 1.26 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.25 2001/05/06 22:22:55 mpowers + * Debugging. + * + * Revision 1.24 2001/05/04 14:42:58 mpowers + * Now getting stored values in KeyValueCoding. + * MasterDetail now marks dirty based on whether it's an attribute + * or relation. + * Implemented editing context marker. + * + * Revision 1.23 2001/05/02 18:00:43 mpowers + * Removed debug code. + * + * Revision 1.22 2001/05/02 17:31:20 mpowers + * DisplayGroupNode now does a better job determining when to mark its + * parent dirty. + * + * Revision 1.21 2001/05/01 00:52:32 mpowers + * Implemented breadth-first traversal of tree for node. + * + * Revision 1.20 2001/04/26 01:15:19 mpowers + * Major clean-up of DisplayGroupNode: fitter, happier, more productive. + * + * Revision 1.19 2001/04/22 23:13:35 mpowers + * Minor bug. + * + * Revision 1.18 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.17 2001/04/21 23:05:12 mpowers + * A fairly major revisiting. I've decided to scrap the pass-thru approach + * where every node simply represents an index and not an object. + * The next update will have each node correspond to a specific object. + * + * Revision 1.16 2001/04/13 16:37:37 mpowers + * Handling bounds checking. + * + * Revision 1.15 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.14 2001/03/27 17:45:51 mpowers + * More index bounds checking. + * + * Revision 1.13 2001/03/22 21:25:42 mpowers + * Fixed some nasty issues with jtree's internal state and array bounds. + * + * Revision 1.12 2001/03/19 22:18:58 mpowers + * Root node now mirrors contents of titles display group. + * + * Revision 1.11 2001/03/19 21:38:36 mpowers + * Improved redisplay after edit. Editing nodes off root now works. + * + * Revision 1.10 2001/03/09 22:08:38 mpowers + * Removed unused line. + * + * Revision 1.9 2001/03/07 16:41:04 mpowers + * Now checking size of parent displayed objects array so that we don't + * get array out of bounds execeptions from isLeaf() or object() when + * those messages are called after the TreeAssociation fires a + * nodesDeleted event. I believe that JTree is mistakenly rendering + * those nodes one last time before erasing them. + * + * Revision 1.8 2001/03/06 23:21:27 mpowers + * Now only notifying parent if the object is not registered in the + * editing context, if any. + * + * Revision 1.7 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.6 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.5 2001/01/31 17:59:52 mpowers + * Fixed isLeaf aspect of TreeAssociation. + * + * Revision 1.4 2001/01/25 02:16:25 mpowers + * TreeAssociation now returns DisplayGroupNode.getUserObject. + * + * Revision 1.3 2001/01/24 18:14:40 mpowers + * Fixed problem with leaving children aspect unspecified. + * + * Revision 1.2 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.1 2001/01/24 14:17:12 mpowers + * Major revision to TreeAssociation. Can now add and remove nodes. + * DisplayGroupNode is now it's own class. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java new file mode 100644 index 0000000..ec22bfd --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java @@ -0,0 +1,368 @@ +/* +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.util.Enumeration; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.SwingUtilities; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TextAssociation binds a JList to a display group's +* list of displayable objects. Bindings are: +* <ul> +* <li>titles: a property convertable to a string for +* display in the cells of the list</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ListAssociation extends EOAssociation + implements ListSelectionListener, + ListDataListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "items" + } ); + + protected DefaultListModel model; + + /** + * Constructor expecting a JList. Throws an + * exception if it doesn't receive one. + * Note: This sets the JList's model to a DefaultListModel. + */ + public ListAssociation ( Object anObject ) + { + super( anObject ); + model = new DefaultListModel(); + ((JList)anObject).setModel( model ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof JList ); + } + + /** + * 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 TitlesAspect; + } + + /** + * 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. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + addAsListener(); + super.establishConnection(); + populateFromDisplayGroup(); + selectFromDisplayGroup(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { // System.out.println( "ListAssociation.addAsListener: " + ++count ); + component().getModel().addListDataListener( this ); + component().addListSelectionListener( this ); + } + + protected void removeAsListener() + { // System.out.println( "ListAssociation.removeAsListener: " + --count ); + component().getModel().removeListDataListener( this ); + component().removeListSelectionListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + EODisplayGroup displayGroup; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { +//System.out.println( "subjectChanged: " + +//displayGroup.contentsChanged() + " : " + displayGroup.selectionChanged() +//+ " : " + displayGroup.updatedObjectIndex() ); +//new net.wotonomy.ui.swing.util.StackTraceInspector(); + if ( displayGroup.contentsChanged() ) + { + populateFromDisplayGroup(); + } + + if ( displayGroup.selectionChanged() ) + { + selectFromDisplayGroup(); + } + } + + } + + private void populateFromDisplayGroup() + { + JList component = component(); + EODisplayGroup displayGroup = displayGroupForAspect( TitlesAspect ); + String key = displayGroupKeyForAspect( TitlesAspect ); + + removeAsListener(); + + // remember selection + int[] selectedIndices = component().getSelectedIndices(); + + // clear the model + model.removeAllElements(); + + // populate the model + Object value; + int size = displayGroup.displayedObjects().count(); +//System.out.println( "populateFromDisplayGroup: " + size ); + for ( int i = 0; i < size; i++ ) + { + value = displayGroup.valueForObjectAtIndex( i, key ); + if ( value == null ) value = "[null]"; + model.addElement( value ); + } + + // select the same indexes + for ( int i = 0; i < selectedIndices.length; i++ ) + { + component.addSelectionInterval( + selectedIndices[i], selectedIndices[i] ); // adds one row + } + + addAsListener(); + } + + private void selectFromDisplayGroup() + { + JList component = component(); + EODisplayGroup displayGroup = displayGroupForAspect( TitlesAspect ); + + removeAsListener(); + + int index; + component.clearSelection(); + Enumeration e = + displayGroup.selectionIndexes().objectEnumerator(); + + while ( e.hasMoreElements() ) + { // add selections one-by-one to support non-contiguous + index = ((Number)e.nextElement()).intValue(); + component.addSelectionInterval( + index, index ); // adds one row + } + + addAsListener(); + } + + // interface ListSelectionListener + + public void valueChanged(ListSelectionEvent e) + { + final EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( ( displayGroup != null ) && ( ! e.getValueIsAdjusting() ) ) + { + int[] selectedIndices = component().getSelectedIndices(); + final NSMutableArray indexList = new NSMutableArray(); + for ( int i = 0; i < selectedIndices.length; i++ ) + { + indexList.addObject( new Integer( selectedIndices[i] ) ); + } + + // invoke later so the component is repainted before + // any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + SwingUtilities.invokeLater( new Runnable() { + public void run() + { + displayGroup.setSelectionIndexes( indexList ); + } + }); + } + } + + // interface ListDataListener + + public void intervalAdded(ListDataEvent e) + { + // System.out.println( "intervalAdded" ); + contentsChanged(e); + } + public void intervalRemoved(ListDataEvent e) + { + // System.out.println( "intervalRemoved" ); + contentsChanged(e); + } + public void contentsChanged(ListDataEvent e) + { + // System.out.println( "contentsChanged" ); + + // if we were editing a property, + // we'd notify our display group now. + } + + // convenience + + private JList component() + { + return (JList) object(); + } +} + +/* + * $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.5 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/05/15 14:05:55 mpowers + * Now appropriately selectingFromDisplayGroup on establishConnection. + * + * Revision 1.3 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:48:49 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java new file mode 100644 index 0000000..f1568ec --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java @@ -0,0 +1,216 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import javax.swing.JTree; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; + +/** +* A DisplayGroupNode that exposes the MutableTreeNode interface. +* This was required so that other subclasses of DisplayGroupNode +* could opt out of supporting MutableTreeNode (so that they can +* implement IlvActivity, for example). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + public class MutableDisplayGroupNode + extends DisplayGroupNode implements MutableTreeNode + { + /** + * Constructor for all nodes. + * Root node must have a null delegate. + */ + public MutableDisplayGroupNode( + TreeModelAssociation aParentAssociation, + EODisplayGroup aParentGroup, + Object anObject ) + { + super( aParentAssociation, aParentGroup, anObject ); + } + + public int getIndex(TreeNode node) + { + return getIndex( (DisplayGroupNode) node ); + } + + public TreeNode getChildAt(int childIndex) + { + return (TreeNode) getChildNodeAt( childIndex ); + } + + public TreeNode getParent() + { + Object parent = getParentGroup(); + if ( parent instanceof TreeNode ) + { + return (TreeNode) parent; + } + return null; + } + + public void insert(MutableTreeNode aChild, int anIndex) + { + if ( aChild instanceof DisplayGroupNode ) + { + insertObjectAtIndex( + ((DisplayGroupNode)aChild).object(), anIndex ); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot insert nodes of type: " + aChild ); + } + } + + /** + * Removes the node at the index corresponding + * to the index of the object. + */ + public void remove(MutableTreeNode node) + { + if ( node instanceof DisplayGroupNode ) + { + remove((DisplayGroupNode)node); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot insert nodes of type: " + node ); + } + } + + /** + * Removes the value in the parent display group + * at the index that corresponds to the index of this node + * and add it to the end of the display group that corresponds + * to the user value of the specified node. + */ + public void setParent(MutableTreeNode newParent) + { + if ( newParent instanceof DisplayGroupNode ) + { + setParent((DisplayGroupNode)newParent); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot set parent to nodes of type: " + newParent ); + } + } + + /** + * Overridden to remember expanded state for nodes + * after nodes have been rearranged. + */ + protected void fireEventsForChanges( + Object[] oldObjects, Object[] newObjects ) + { + if ( !( parentAssociation.object() instanceof JTree ) ) + { + super.fireEventsForChanges( oldObjects, newObjects ); + return; + } + + JTree tree = (JTree) parentAssociation.object(); + Map expansionMap = new HashMap(); + DisplayGroupNode node; + TreePath path; + for ( int i = 0; i < oldObjects.length; i++ ) + { + node = (DisplayGroupNode) + getChildNodeForObject( oldObjects[i] ); + if ( node != null && ! node.isLeaf() ) + { + expansionMap.put( node, new Boolean( + tree.isExpanded( node.treePath() ) ) ); + } + } + + super.fireEventsForChanges( oldObjects, newObjects ); + + Object value; + Iterator iterator = new LinkedList( childNodes.values() ).iterator(); + while ( iterator.hasNext() ) + { + node = (DisplayGroupNode) iterator.next(); + value = expansionMap.get( node ); + if ( value != null ) + { + if ( Boolean.TRUE.equals( value ) ) + { + tree.expandPath( node.treePath() ); + } + else + { + tree.collapsePath( node.treePath() ); + } + } + } + } + } +/* + * $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.8 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.7 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.6 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.5 2002/04/03 20:01:47 mpowers + * Now remembers expanded state. + * + * Revision 1.4 2002/03/08 23:19:07 mpowers + * Added getParentGroup to DisplayGroupNode. + * + * Revision 1.3 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.2 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.1 2001/04/21 23:05:56 mpowers + * Contributing the tree-specific concrete subclass of DisplayGroupNode. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java new file mode 100644 index 0000000..d105d89 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java @@ -0,0 +1,333 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.BorderLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.FormattedCellRenderer; +import net.wotonomy.ui.swing.util.ObjectInspector; +import net.wotonomy.ui.swing.util.StackTraceInspector; +import net.wotonomy.ui.swing.util.WindowUtilities; +import net.wotonomy.control.internal.Surrogate; + +/** +* The NotificationInspector displays a JFrame that +* displays notifications as they occur. <br><br> +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class NotificationInspector + implements MouseListener, ActionListener +{ + protected JTable table; + protected JPopupMenu popupMenu; + protected EODisplayGroup displayGroup; + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + protected static final String CLEAR_ALL = "Clear All"; + protected static final String CLEAR_SELECTED = "Clear Selected"; + + +/** +* Displays all notifications on the default notification center. +*/ + public NotificationInspector() + { + this( NSNotificationCenter.defaultCenter() ); + } + +/** +* Displays all notifications from the specified notification center. +*/ + public NotificationInspector( NSNotificationCenter aCenter ) + { + this( aCenter, null, null ); + } + +/** +* Displays notifications from the default notification center +* using the specified name and object filters. +*/ + public NotificationInspector( String notificationName, Object anObject ) + { + this( NSNotificationCenter.defaultCenter(), + notificationName, anObject ); + } + +/** +* Displays notifications on the specified notification center +* using the specified name and object filters. +*/ + public NotificationInspector( NSNotificationCenter aCenter, + String notificationName, Object anObject ) + { + // show stack traces + NSNotification.showStack = true; + + // register for notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + aCenter.addObserver( + this, handleNotification, notificationName, anObject ); + + table = new JTable(); + + popupMenu = new JPopupMenu(); + JMenuItem menuItem = popupMenu.add( CLEAR_SELECTED ); + menuItem.addActionListener( this ); + menuItem = popupMenu.add( CLEAR_ALL ); + menuItem.addActionListener( this ); + + displayGroup = new EODisplayGroup(); + + TableColumn column; + TableColumnAssociation assoc; + + column = new TableColumn(); + column.setHeaderValue( "Time" ); + column.setCellRenderer( new FormattedCellRenderer( new SimpleDateFormat( "hh:mm:ss:SS" ) ) ); + column.setPreferredWidth( 90 ); + column.setMaxWidth( 90 ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "time" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Type" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "name" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Object" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "objectString" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Info" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "userInfoString" ); + assoc.setTable( table ); + assoc.establishConnection(); + + initLayout(); + } + + protected void initLayout() + { + table.addMouseListener( this ); // listen for double-clicks + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + //scrollPane.setPreferredSize( new Dimension( 500, 250 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Notification Inspector" ); + window.getContentPane().add( panel ); + + //window.pack(); + window.setSize( 800, 400 ); + WindowUtilities.cascade( window ); + + // size the columns. +// table.getColumnModel().getColumn( 0 ).setPreferredWidth( 100 ); + + window.show(); + } + +/** +* Handles the notification. +*/ + public void handleNotification( NSNotification aNotification ) + { + Surrogate s = new Surrogate( new Object[] { aNotification } ); + s.directPut( "time", new Date() ); + s.directPut( "objectString", ""+aNotification.object() ); // snapshot of state + s.directPut( "userInfoString", ""+aNotification.userInfo() ); // snapshot of info + displayGroup.insertObjectAtIndex( s, 0 ); + } + + // interface ActionListener + + /** + * Method used to listen for the action event from the popup menu items. + */ + public void actionPerformed( ActionEvent e ) + { + if ( CLEAR_SELECTED.equals( e.getActionCommand() ) ) + { + NSMutableArray objects = new NSMutableArray( displayGroup.allObjects() ); + objects.removeAll( displayGroup.selectedObjects() ); + displayGroup.setObjectArray( objects ); + displayGroup.updateDisplayedObjects(); + } + else if ( CLEAR_ALL.equals( e.getActionCommand() ) ) + { + displayGroup.setObjectArray( null ); + displayGroup.updateDisplayedObjects(); + } + } + + // interface MouseListener + + /** + * Double click to launch object inspector. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + col = table.convertColumnIndexToModel( col ); + + if ( row == -1 ) return; + + if ( col == 0 ) // time + { + new StackTraceInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).stackTrace() ); + } + else + if ( col == 2 ) // object + { + new ObjectInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).object() ); + } + else + if ( col == 3 ) // info + { + new ObjectInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).userInfo() ); + } + else + { + new ObjectInspector( ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ); + } + } + else + { + // Click count is 1 then, check for popup trigger. + if ( e.isPopupTrigger() ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + } + + public void mouseReleased(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.isPopupTrigger() && ( e.getClickCount() == 1 ) ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + + public void mousePressed(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.isPopupTrigger() && ( e.getClickCount() == 1 ) ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.5 2002/11/18 22:11:51 mpowers + * rglista's long-overdue enhancements: can now clear the display! + * + * Revision 1.4 2002/10/24 18:19:03 mpowers + * Now telling NSNotification to generate stack traces. + * + * Revision 1.3 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.2 2001/04/09 21:40:25 mpowers + * Numerous usability enhancements. + * + * Revision 1.1 2001/04/08 20:58:45 mpowers + * Contributing notification inspector. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java new file mode 100644 index 0000000..d2e51ed --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java @@ -0,0 +1,457 @@ +/* +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.event.ActionEvent; +import java.awt.event.ActionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.RadioButtonPanel; + +/** +* RadioPanelAssociation binds RadioButtonPanels to +* display groups. It works exactly like a +* ComboBoxAssociation. Bindings are: +* <ul> +* +* <li>value: a property of the selected object in the +* display group that will be bind to the item the user +* selects or the text that the user enters in the field.</li> +* +* <li>titles: a property of the objects in the bound +* display group that will appear in the list. If the +* objects aspect is not bound, this property is also +* used to populate the value binding.</li> +* +* <li>objects: optional - if specified, when the user +* selects an title in the list, the property of the +* object at the corresponding index of the bound display +* group will be used to populate the value binding.</li> +* +* <li>enabled: a boolean property of the selected object in the +* display group that determines whether +* the user can edit the field.</li> +* +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class RadioPanelAssociation extends EOAssociation + implements ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ValueAspect, + ObjectsAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public RadioPanelAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof RadioButtonPanel ); + } + + /** + * 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. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + super.establishConnection(); + + // prepopulate titles + EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + populateValue(); + addAsListener(); + } + + protected void addAsListener() + { + component().addActionListener( this ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void removeAsListener() + { + component().removeActionListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + removeAsListener(); + + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + // if backing group has changed + if ( displayGroup.contentsChanged() ) + { + key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + } + + // value aspect + populateValue(); + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != component.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + addAsListener(); + } + + /** + * Called to repopulate the title list from the + * specified display group. + */ + protected void populateTitles( + EODisplayGroup displayGroup, String key ) + { + Object value; + int count = displayGroup.displayedObjects().count(); + String[] titles = new String[ count ]; + for ( int i = 0; i < count; i++ ) + { + value = displayGroup.valueForObjectAtIndex( i, key ); + if ( value != null ) + { + titles[i] = value.toString(); + } + else + { + titles[i] = ""; + } + } + component().setLabels( titles ); + } + + /** + * Called to populate the value from the display group. + */ + protected void populateValue() + { + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + component.setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + + Object value = displayGroup.selectedObjectValueForKey( key ); + + // objects aspect + EODisplayGroup objectsDisplayGroup = + displayGroupForAspect( ObjectsAspect ); + if ( ( objectsDisplayGroup != null ) && ( value != null ) ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + Object match; + int index = NSArray.NotFound; + int count = objectsDisplayGroup.displayedObjects().count(); + for ( int i = 0; i < count; i++ ) + { + match = objectsDisplayGroup.valueForObjectAtIndex( i, objectKey ); + if ( value.equals( match ) ) + { + index = i; + } + } + if ( index == NSArray.NotFound ) + { + if ( component.getValue() != null ) + { + component.setValue( null ); + } + } + else + { + String[] titles = component().getLabels(); + component.setValue( titles[ index ] ); + } + } + else + { + if ( value != null ) value = value.toString(); + component.setValue( (String) value ); + } + } + } + + /** + * 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() + { + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // selected title aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + Object value = null; + + // selected object aspect, if any + EODisplayGroup objectsGroup = + displayGroupForAspect( ObjectsAspect ); + if ( objectsGroup != null ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + String selectedValue = component.getValue(); + if ( selectedValue != null ) + { + String[] titles = component.getLabels(); + int index = -1; + for ( int i = 0; i < titles.length; i++ ) + { + if ( selectedValue.equals( titles[i] ) ) + { + index = i; + } + } + if ( index != -1 ) + { + value = objectsGroup + .valueForObjectAtIndex( index, objectKey ); + } + } + } + else // just use the selected item + { + value = component.getValue(); + } + + return displayGroup.setSelectedObjectValue( value, key ); + } + + return false; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + writeValueToDisplayGroup(); + } + + // convenience + + private RadioButtonPanel component() + { + return (RadioButtonPanel) object(); + } +} + +/* + * $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.4 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/02/16 17:48:07 mpowers + * Populating titles or data not longer marks target object as changed. + * + * Revision 1.1.1.1 2000/12/21 15:48:52 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java new file mode 100644 index 0000000..7194f23 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java @@ -0,0 +1,283 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* ReferenceInspector tracks objects until they are garbage collected. +* Use it to track objects that you suspect are not being GCed. +* ReferenceInspector retains only weak references to the objects that +* it tracks, so when those weak references cannot be resolved, the +* object has been garbage collected. Note that under some GC +* implementations, adding a weak reference to an object will delay +* garbage collection for that object. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class ReferenceInspector + implements MouseListener, ActionListener +{ + protected JTable table; + protected JLabel memoryLabel; + + static protected EODisplayGroup displayGroup; + static protected JFrame window; + + /* Reference queue for cleared WeakKeys */ + private static ReferenceQueue queue = new ReferenceQueue(); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + +/** +* Launches a new ReferenceInspector if one does not already exist. +*/ + public ReferenceInspector() + { + if ( window == null ) + { + table = new JTable(); + memoryLabel = new JLabel(); + + displayGroup = new EODisplayGroup(); + + TableColumn column; + TableColumnAssociation assoc; + + column = new TableColumn(); + column.setHeaderValue( "Object" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Address" ); + column.setMaxWidth( 100 ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "identityHashCode" ); + assoc.setTable( table ); + assoc.establishConnection(); + + initLayout(); + } + window.show(); + } + +/** +* Adds the specified object to the ReferenceInspector, launching +* a new ReferenceInspector if one does not already exist. +*/ + public ReferenceInspector( Object anObject ) + { + this(); + displayGroup.insertObjectAtIndex( new ExtendedWeakReference( anObject, queue ), 0 ); + window.show(); + } + + protected void initLayout() + { + table.addMouseListener( this ); // listen for double-clicks + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 500, 250 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + ButtonPanel buttonPanel = new ButtonPanel( new String[] { "Update" } ); + buttonPanel.addActionListener( this ); + + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout( new BorderLayout() ); + bottomPanel.add( buttonPanel, BorderLayout.EAST ); + bottomPanel.add( memoryLabel, BorderLayout.CENTER ); + panel.add( bottomPanel, BorderLayout.SOUTH ); + + window = new JFrame(); + window.setTitle( "Reference Inspector" ); + window.getContentPane().add( panel ); + +// javax.swing.Timer timer = new javax.swing.Timer( 10000, this ); +// timer.restart(); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + /* Remove all invalidated entries from the map, that is, remove all entries + whose keys have been discarded. This method should be invoked once by + each public mutator in this class. We don't invoke this method in + public accessors because that can lead to surprising + ConcurrentModificationExceptions. */ + private static void processQueue() + { +// System.out.println( "ReferenceInspector.processQueue:"); + synchronized ( displayGroup ) + { + int idx; + WeakReference rk; + while ((rk = (WeakReference)queue.poll()) != null) + { +// System.out.println( "ReferenceInspector.processQueue: removing object: " + rk ); + if ( rk != null ) + { + idx = displayGroup.displayedObjects().indexOfIdenticalObject( rk ); + if ( idx != NSArray.NotFound ) + { + displayGroup.deleteObjectAtIndex( idx ); + } + } + } + displayGroup.updateDisplayedObjects(); + } + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + Runtime runtime = Runtime.getRuntime(); + runtime.gc(); + processQueue(); + + long totalMemory = runtime.totalMemory() / 1024; + long freeMemory = runtime.freeMemory() / 1024; + memoryLabel.setText( + Long.toString( totalMemory - freeMemory ) + "K / " + + Long.toString( totalMemory ) + "K" ); + } + + // interface MouseListener + + /** + * Double click to launch object inspector. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + col = table.convertColumnIndexToModel( col ); + + if ( row == -1 ) return; + + if ( col == 0 ) // time + { + } + else + { + } + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + public class ExtendedWeakReference extends WeakReference + { + public ExtendedWeakReference(Object referent, ReferenceQueue q) + { + super( referent, q ); + } + + public String toString() + { + if ( get() != null ) + { + return get().toString(); + } + return null; + } + + public String identityHashCode() + { + if ( get() != null ) + { + return Integer.toHexString( System.identityHashCode( get() ) ); + } + return null; + } + + } +} + +/* + * $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.5 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/03/22 22:39:59 mpowers + * Now shows the window even if previously hidden. + * + * Revision 1.3 2001/10/02 14:22:39 mpowers + * Now shows used and heap memory usage. + * + * Revision 1.2 2001/07/10 16:39:32 mpowers + * Removed printlns. + * + * Revision 1.1 2001/07/10 16:32:50 mpowers + * Adding the reference inspector. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java new file mode 100644 index 0000000..b836dcc --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java @@ -0,0 +1,419 @@ +/* +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.util.Iterator; + +import javax.swing.JSlider; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* SliderAssociation binds a JSlider component to +* a display group. Bindings are: +* <ul> +* <li>value: a property convertable to/from a string</li> +* <li>enabled: a boolean property that determines whether +* the user can select the text in the field</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class SliderAssociation extends EOAssociation + implements ChangeListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "value" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public SliderAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof JSlider ); + } + + /** + * 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 as a FocusListener to the specified object. + */ + public void establishConnection () + { + component().addChangeListener( this ); + 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 () + { + component().removeChangeListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + JSlider component = component(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + 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 int + value = ValueConverter.convertObjectToClass( value, Integer.class ); + + int intValue; + if ( value == null ) + { + intValue = 0; + } + else + { + intValue = ((Integer)value).intValue(); + } + + if ( component.getValue() != intValue ) + { + component().removeChangeListener( this ); + component.setValue( intValue ); + component().addChangeListener( this ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( displayGroup != null ) || ( key != null ) ) + { + 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.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + key = displayGroupKeyForAspect( VisibleAspect ); + if ( ( displayGroup != null ) || ( key != null ) ) + { + 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 ) + { + if ( converted.booleanValue() != component.isVisible() ) + { + component.setVisible( converted.booleanValue() ); + } + } + } + + } + + /** + * 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() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Integer( component().getValue() ); + + 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 ChangeListener + + /** + * Updates object on change. + */ + public void stateChanged(ChangeEvent e) + { + writeValueToDisplayGroup(); + } + + private JSlider component() + { + return (JSlider) object(); + } +} + +/* + * $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.8 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2002/10/11 20:12:58 mpowers + * Updated aspect signature. + * + * Revision 1.5 2002/10/11 20:08:14 mpowers + * Added visible aspect to slider association. + * + * Revision 1.4 2001/11/01 15:54:37 mpowers + * Minor update to aspect signature. + * + * Revision 1.3 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.2 2001/02/16 15:03:34 mpowers + * Fixed: slider sets value in display group after selection changed. + * + * Revision 1.1.1.1 2000/12/21 15:48:55 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java new file mode 100644 index 0000000..bc02f7d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java @@ -0,0 +1,927 @@ +/* +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.EventQueue; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Enumeration; + +import javax.swing.CellEditor; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; + +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TableAssociation binds one or more TableColumnAssociations +* to a display group. You should not instantiate this class +* directly; use TableColumnAssociation.setTable() instead. +* Note that TableAssociation inserts itself as the controlled +* JTable's TableModel. +* +* Bindings are: +* <ul> +* <li>source: a property convertable to a string for +* display in the cells of the table column</li> +* <li>enabled: a property convertable to a string for +* display in the cells of the table column. +* Note that you can bind this aspect to a key equal to +* "true" or "false" and leave the display group null.</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TableAssociation extends EOAssociation + implements ActionListener, ListSelectionListener, FocusListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + SourceAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "tableModel", "tableHeader" + } ); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + EODisplayGroup source; + EODisplayGroup sortTarget; // used by TreeColumnAssociation + NSMutableArray columns; + JTableHeader tableHeader; + + boolean pleaseIgnore; + boolean selectionPaintedImmediately; + boolean selectionTracking; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. setTable() must be called before + * establishing the connection. + */ + public TableAssociation ( Object anObject ) + { + super( anObject ); + source = null; + columns = new NSMutableArray(); + JTable aTable = ((JTable)anObject); + aTable.addFocusListener( this ); + aTable.setModel( + new TableAssociationModel( this ) ); + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + + // why did sun make this harder to use? + //aTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( + // KeyStroke.getKeyStroke( KeyEvent.VK_C, java.awt.Event.CTRL_MASK ), COPY); + //aTable.getActionMap().put(COPY, this); + + aTable.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + aTable.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + tableHeader = new SortedTableHeader(); + tableHeader.setColumnModel( aTable.getColumnModel() ); + aTable.setTableHeader( tableHeader ); + tableHeader.addMouseListener( new TableHeaderListener() ); + pleaseIgnore = false; + selectionPaintedImmediately = false; + selectionTracking = false; + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof JTable ); + } + + /** + * 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 ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( SourceAspect.equals( anAspect ) ) + { + source = aDisplayGroup; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + if ( source == null ) + { + throw new WotonomyException( "No display group: " + + "please ensure that the TableColumnAssociation " + + "has a display group bound to the ValueAspect" ); + } + super.establishConnection(); + selectFromDisplayGroup(); + addAsListener(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { + component().getSelectionModel() + .addListSelectionListener( this ); + } + + protected void removeAsListener() + { + component().getSelectionModel() + .removeListSelectionListener( this ); + } + + /** + * 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 () + { + // stop any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + return editor.stopCellEditing(); + } + return true; + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + if ( source != null ) + { + if ( source.contentsChanged() ) + { + removeAsListener(); + ((TableAssociationModel)component().getModel()). + fireTableDataChanged(); + selectFromDisplayGroup(); + addAsListener(); + + // if we caused this change, do nothing + if ( pleaseIgnore ) + { + pleaseIgnore = false; + } + else // otherwise, update the sort indicator + { + tableHeader.repaint(); + + // cancel any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + editor.cancelCellEditing(); + } + } + } + else + if ( source.selectionChanged() ) + { + removeAsListener(); + selectFromDisplayGroup(); + addAsListener(); + } + } + + } + + private void selectFromDisplayGroup() + { + JTable component = component(); + + int index; + component.getSelectionModel().clearSelection(); + Enumeration e = + source.selectionIndexes().objectEnumerator(); + + while ( e.hasMoreElements() ) + { // add selections one-by-one to support non-contiguous + index = ((Number)e.nextElement()).intValue(); + component.getSelectionModel().addSelectionInterval( + index, index ); // adds one row + } + } + + // interface ListSelectionListener + + public void valueChanged(ListSelectionEvent e) + { + if ( source != null ) + { + if ( selectionTracking || !e.getValueIsAdjusting() ) + { + int[] selectedIndices = component().getSelectedRows(); + final NSMutableArray indexList = new NSMutableArray(); + for ( int i = 0; i < selectedIndices.length; i++ ) + { + indexList.addObject( new Integer( selectedIndices[i] ) ); + } + + // invoke later so the component is repainted before + // any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + Runnable select = new Runnable() + { + public void run() + { + pleaseIgnore = true; + source.setSelectionIndexes( indexList ); + } + }; + if ( selectionPaintedImmediately ) + { + EventQueue.invokeLater( select ); + } + else + { + select.run(); + } + } + } + } + + /** + * Determines whether the selection should be painted + * immediately after the user clicks and therefore + * before the children display group is updated. + * When the children group is bound to many associations + * or is bound to a master-detail association, updating + * the display group can take a perceptibly long time. + * This property defaults to false. + * @see #setSelectionPaintedImmediately + */ + public boolean isSelectionPaintedImmediately() + { + return selectionPaintedImmediately; + } + + /** + * Sets whether the selection should be painted immediately. + * Setting this property to true will let the table paint + * first before the display group is updated. + */ + public void setSelectionPaintedImmediately( boolean isImmediate ) + { + selectionPaintedImmediately = isImmediate; + } + + /** + * Determines whether the selection is actively tracking + * the selection as the user moves the mouse. + * If true, selection will not be updated while the + * list selection event returns true for isValueAdjusting(). + * This property defaults to false. + * @see #setSelectionTracking + */ + public boolean isSelectionTracking() + { + return selectionTracking; + } + + /** + * Sets whether the selection is actively tracking + * the selection as the user moves the mouse. + * Setting this property to true will update the display + * group with each change to the selection. + * This means that any tree selection listers will + * also be notified before the display group is updated + * and will have to invokeLater if they want to see the + * updated display group. + */ + public void setSelectionTracking( boolean isTracking ) + { + selectionTracking = isTracking; + } + + // convenience + private JTable component() + { + return (JTable) object(); + } + + /** + * Copies the contents of the table to the clipboard as a tab-delimited string. + */ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getTabDelimitedString() ); + clipboard.setContents( selection, selection ); + } + + /** + * Converts the contents of the table to a tab-delimited string. + * @return A String containing the text contents of the table. + */ + public String getTabDelimitedString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = component().getModel(); + int cols = model.getColumnCount(); + int rows = model.getRowCount(); + + Object o = null; + for ( int y = 0; y < rows; y++ ) + { + for ( int x = 0; x < cols; x++ ) + { + o = model.getValueAt( y, x ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\t' ); + } + result.append( '\n' ); + } + + return result.toString(); + } + + // interface ActionEventListener + + public void actionPerformed(ActionEvent evt) + { + String cmd = evt.getActionCommand(); + + if ( COPY.equals( cmd ) ) + { + copyToClipboard(); + return; + } + } + + /** + * Used to render the little triangle over the sorted column(s). + */ + class SortedTableHeader extends JTableHeader + { + public void paint(Graphics g) + { + super.paint( g ); + Rectangle r; + TableColumnAssociation association; + int size = columns.size(); + NSArray orderings; + if ( sortTarget != null ) + { + orderings = sortTarget.sortOrderings(); + } + else + { + orderings = source.sortOrderings(); + } + for ( int i = 0; i < size; i++ ) + { + r = getHeaderRect( component().convertColumnIndexToView( i ) ); + association = (TableColumnAssociation) columns.objectAtIndex( i ); + association.drawSortIndicator( r, g, orderings ); + } + } + } + + /** + * Used to listen for clicks on the table header. + */ + class TableHeaderListener extends MouseAdapter + { + public void mouseClicked( MouseEvent evt ) + { + EODisplayGroup displayGroup = sortTarget; + if ( displayGroup == null ) displayGroup = source; + + if ( evt.getClickCount() > 0 ) + { + int columnClicked = tableHeader.columnAtPoint( evt.getPoint() ); + if ( columnClicked != -1 ) + { + columnClicked = component().convertColumnIndexToModel( columnClicked ); + TableColumnAssociation association = (TableColumnAssociation) + columns.objectAtIndex( columnClicked ); + if ( association.isSortable() ) + { + NSMutableArray newOrder = + new NSMutableArray( displayGroup.sortOrderings() ); + + // click once = asc, twice = desc, thrice = clear + EOSortOrdering sortOrdering; + int index = association.getIndexOfMatchingOrdering( newOrder ); + if ( index == -1 ) sortOrdering = null; + else if ( index == 1 ) sortOrdering = association.getSortOrdering( false ); + else sortOrdering = association.getSortOrdering( true ); + + pleaseIgnore = true; + tableHeader.repaint(); + + // stop any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + editor.stopCellEditing(); + } + + // remove existing key + if ( index != 0 ) + { + newOrder.removeObjectAtIndex( Math.abs( index ) - 1 ); + } + + // put new key at front of list + if ( sortOrdering != null ) + { + newOrder.insertObjectAtIndex( sortOrdering, 0 ); + } + + displayGroup.setSortOrderings( newOrder ); + displayGroup.updateDisplayedObjects(); + } + } + } + } + } + + /** + * 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 ( ! component().isEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + + /** + * Used as the model for the controlled table. + * Package access so TableColumnAssociation can recognize + * it and use the addColumnAssociation() method. + * Extends AbstractTableModel just so we get event + * broadcasting for free. + */ + static class TableAssociationModel extends AbstractTableModel + { + private TableAssociation parent; + + private TableAssociationModel( TableAssociation aParent ) + { + parent = aParent; + } + + public TableAssociation getAssociation() + { + return parent; + } + + /** + * Adds the column to the list of ColumnAssociations, + * and adds the corresponding column to the table + * at the next available index, setting the value of + * the column's model index accordingly. + * Establishes connection if no columns are currently + * associated. + */ + public void addColumnAssociation( + TableColumnAssociation aColumnAssociation ) + { + // establish connection if necessary + if ( parent.columns.size() == 0 ) + { + parent.establishConnection(); + } + + int newIndex = parent.columns.count(); + parent.columns.add( aColumnAssociation ); + + // add column to table + TableColumn column = (TableColumn) aColumnAssociation.object(); + column.setModelIndex( newIndex ); + parent.component().addColumn( column ); + + } + + /** + * Removes the column from the list of ColumnAssociations, + * and removes the corresponding column from the table. + * Breaks connection if no more columns are associated. + */ + public void removeColumnAssociation( + TableColumnAssociation aColumnAssociation ) + { + int index = parent.columns.indexOfIdenticalObject( aColumnAssociation ); + if ( index == NSArray.NotFound ) return; + + parent.columns.removeObjectAtIndex( index ); + + // remove column from table + TableColumn column = (TableColumn) aColumnAssociation.object(); + parent.component().removeColumn( column ); + + // break connection if necessary + if ( parent.columns.size() == 0 ) + { + parent.breakConnection(); + } + } + + public int getRowCount() + { + if ( parent.source == null ) return 0; + return parent.source.displayedObjects().count(); + } + + public int getColumnCount() + { + return parent.columns.count(); + } + + /** + * Attempts to retrieve the header value from the specified column, + * or returns " " if the value is null. + */ + public String getColumnName(int columnIndex) + { + TableColumnAssociation association = (TableColumnAssociation) + parent.columns.objectAtIndex( columnIndex ); + Object value = ((TableColumn)association.object()).getHeaderValue(); + if ( value != null ) return value.toString(); + return " "; + } + + /** + * Returns the class of the first item in the + * display group bound to the column. + */ + public Class getColumnClass(int columnIndex) + { + Object value; + int count = getRowCount(); + for( int i = 0; i < count; i++ ) + { //First row in column is null find one that is not. + value = ((TableColumnAssociation)parent.columns. + objectAtIndex( columnIndex ) ).valueAtIndex( i ); + if ( value != null ) return value.getClass(); + } + return Object.class; + } + + /** + * Calls the column association's isEditableAtRow method. + */ + public boolean isCellEditable(int rowIndex, + int columnIndex) + { + return + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).isEditableAtRow( rowIndex ); + } + + /** + * Calls the column association's valueAtIndex method. + */ + public Object getValueAt(int rowIndex, + int columnIndex) + { + return + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).valueAtIndex( rowIndex ); + } + + /** + * Calls the column association's setValueAtIndex method. + */ + public void setValueAt(Object aValue, + int rowIndex, + int columnIndex) + { + Object existingValue = getValueAt( rowIndex, columnIndex ); + + // don't update display group if value is the same as before + if ( aValue == existingValue ) return; // handles null case + if ( existingValue != null ) // both are not null + { + Object newValue = aValue; + + // if different types + if ( newValue.getClass() != existingValue.getClass() ) + { + // convert to string since most editors do anyway + newValue = newValue.toString(); + existingValue = existingValue.toString(); + } + if ( newValue.equals( existingValue ) ) + { + // same value - do nothing + return; + } + } + + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).setValueAtIndex( aValue, rowIndex ); + } + + } + +} + +/* + * $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.31 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.30 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.29 2002/05/24 14:41:29 mpowers + * Throwing an exception for clarity. + * + * Revision 1.28 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.27 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.25 2002/03/04 22:49:53 mpowers + * Now working with TreeColumnAssociation to delegate sorting behavior. + * + * Revision 1.23 2002/03/04 03:58:17 mpowers + * Refined table header click behavior. + * + * Revision 1.22 2002/03/01 23:41:39 mpowers + * Added facility to get table association from model. + * + * Revision 1.21 2002/03/01 20:41:22 mpowers + * Cleaned up unused code. + * + * Revision 1.20 2002/03/01 15:42:00 mpowers + * Table column headers now always show their sort indicator. + * A third table-column click clears the sort for that column. + * + * Revision 1.19 2002/02/28 23:01:39 mpowers + * TableColumnAssociations add and remove themselves from the TableAssociation + * when their connection is established and broken respectively. + * TableAssociations now break connection if they have no column associations. + * + * Revision 1.18 2002/02/28 22:45:27 mpowers + * Now registers as editing association when table gets focus, so that + * endEditing can appropriate commit any table cell editors. + * + * Revision 1.17 2001/10/26 20:01:50 mpowers + * We're again cancelling instead of stopping editing on subjectChanged. + * I believe that the introduction of NSRunLoop has allowed us to return + * to the correct behavior. + * + * Revision 1.16 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.15 2001/07/25 15:03:01 mpowers + * getColumnName now checks for a column header value before defaulting " ". + * + * Revision 1.14 2001/06/05 19:09:25 mpowers + * Fixed broken build. + * + * Revision 1.13 2001/06/05 19:08:12 mpowers + * Better determination of column class. Previously, if the first object + * was null, Object.class was used. Now we get the first non-null object. + * + * Revision 1.12 2001/05/02 17:39:01 mpowers + * Now selects from display group after initial population. + * + * Revision 1.11 2001/03/29 23:05:22 mpowers + * Fixes for editing table cells. + * + * Revision 1.10 2001/03/09 22:09:22 mpowers + * Now better handling jdk1.1 for rendering the column header. + * + * Revision 1.9 2001/02/27 02:10:38 mpowers + * No longer updating values to the display group if the value + * has not changed. + * + * Revision 1.8 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.7 2001/02/16 17:47:17 mpowers + * Now cancelling any cell editing on subjectChanged(). + * + * Revision 1.6 2001/01/12 19:11:56 mpowers + * Fixed table column click sorting. + * + * Revision 1.5 2001/01/12 17:20:30 mpowers + * Moved EOSortOrdering creation to ColumnAssociation. + * + * Revision 1.4 2001/01/11 21:55:57 mpowers + * Implemented sort indicator for table column headers. + * + * Revision 1.3 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.2 2001/01/08 20:40:51 mpowers + * JTables in 1.3 clear their selection when the data model changes, + * which sends a list selection event. We now reset the selection again + * after the data changes so we're compatible across 1.2 and 1.3. + * + * Revision 1.1.1.1 2000/12/21 15:49:00 mpowers + * Contributing wotonomy. + * + * Revision 1.7 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java new file mode 100644 index 0000000..e1c32f3 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java @@ -0,0 +1,708 @@ +/* +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.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.util.Iterator; +import java.util.List; + +import javax.swing.JTable; +import javax.swing.table.TableColumn; + +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TableAssociation.TableAssociationModel; + + +/** +* TableColumnAssociation binds a column of a JTable +* to a property of the elements of a display group. +* Bindings are: +* <ul> +* <li>value: a property convertable to a string for +* display in the cells of the table column</li> +* <li>editable: a property convertable to a boolean +* that determines the editability of the corresponding +* cells in the column.</li> +* </ul> + +* Because TableColumns do not have a handle to their +* containing JTable, setTable() must be called before +* calling establishConnection(). This will add the +* controlled TableColumn to the specified JTable. +* +* Columns appear in the table in the order in which +* setTable is called on the corresponding association. +* The original table model index is ignored. +* +* Column names appear in the table based on the value +* of TableColumn.getHeaderValue(). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TableColumnAssociation extends EOAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EditableAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "table" + } ); + + static Color[] sortIndicatorColorList; + + EODisplayGroup valueDisplayGroup, editableDisplayGroup; + String valueKey, editableKey; + boolean sortable; + boolean sortCaseSensitive; + JTable table; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. setTable() must be called before + * establishing the connection. + */ + public TableColumnAssociation ( Object anObject ) + { + super( anObject ); + valueDisplayGroup = null; + valueKey = null; + editableDisplayGroup = null; + editableKey = null; + sortable = true; + sortCaseSensitive = true; + table = null; + } + + /** + * Sets the table to be used for this column association. + * If no TableAssociation exists for the specified table, + * one will be created automatically. The controlled + * table column will be added to the table. Note that + * the table column's model index is ignored: table columns + * appear in the table in the order in which setTable is + * called on their corresponding associations. + */ + public void setTable( JTable aTable ) + { + table = aTable; + if ( table == null ) return; + + // creates table association if not existing + getTableAssociation(); + } + + /** + * Returns the table association for this table column, + * or null if no table has been set. This method will + * create the association if none exists for the table. + */ + public TableAssociation getTableAssociation() + { + if ( table == null ) return null; + + TableAssociation result; + if ( ! ( table.getModel() instanceof TableAssociationModel ) ) + { + result = new TableAssociation( table ); + result.bindAspect( + SourceAspect, displayGroupForAspect( ValueAspect ), "" ); + } + else + { + result = ((TableAssociationModel)table.getModel()).getAssociation(); + } + return result; + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 ( anObject instanceof TableColumn ); + } + + /** + * 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 ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + if ( EditableAspect.equals( anAspect ) ) + { + editableDisplayGroup = aDisplayGroup; + editableKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + addAsListener(); + + if ( table == null ) throw new WotonomyException( + "A table must be specified by calling setTable()" ); + + // add association to model + TableAssociationModel model = + (TableAssociationModel) table.getModel(); + model.addColumnAssociation( this ); + + super.establishConnection(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + + if ( table == null ) throw new WotonomyException( + "TableColumnAssociation's table may not be null" ); + + // remove association from model + TableAssociationModel model = + (TableAssociationModel) table.getModel(); + model.removeColumnAssociation( this ); + + super.breakConnection(); + } + + protected void addAsListener() + { + } + + protected void removeAsListener() + { + } + + /** + * Returns the value to be displayed at the specified index. + * This method is called by the TableAssocation to populate + * the table model. + * This implementation simply retrieves the value from the + * display group bound to the value aspect. + */ + public Object valueAtIndex( int aRowIndex ) + { + if ( valueDisplayGroup != null ) + { + return valueDisplayGroup.valueForObjectAtIndex( + aRowIndex, valueKey ); + } + return null; + } + + /** + * Sets a value for the specified index. This method is + * called by the TableAssocation after a cell has been + * edited. + * This implementation simply sets the value in the + * display group bound to the value aspect. + */ + public void setValueAtIndex( Object aValue, int aRowIndex ) + { + if ( valueDisplayGroup != null ) + { + valueDisplayGroup.setValueForObjectAtIndex( + aValue, aRowIndex, valueKey ); + } + } + + /** + * Returns whether this column should be sorted when the + * user clicks on the column header. Defaults to true. + */ + public boolean isSortable() + { + return sortable; + } + + /** + * Sets whether this column should be sorted when the + * user clicks on the column header. + */ + public void setSortable( boolean isSortable ) + { + sortable = isSortable; + } + + /** + * Returns whether this column should be sorted + * in a case sensitive manner. Defaults to true. + */ + public boolean isSortCaseSensitive() + { + return sortCaseSensitive; + } + + /** + * Sets whether this column should be sorted when + * in a case sensitive manner. + * If false, the column contents should be string values. + */ + public void setSortCaseSensitive( boolean isCaseSensitive ) + { + sortCaseSensitive = isCaseSensitive; + } + + /** + * Called by the TableAssociation to determine whether + * the value at the specified row is editable. + * This is determined by the binding of the Editable aspect, + * looking at the value of the corresponding index in that + * display group. Note: because the display group may + * not have the same number if items, the selected index is + * used if the editable display group is not the same as the + * the value display group. + */ + public boolean isEditableAtRow( int aRowIndex ) + { + if ( editableKey == null ) return false; + Object value = null; + if ( editableDisplayGroup != null ) + { + // if using the same group for both, return the value for the index + if ( editableDisplayGroup.equals( valueDisplayGroup ) ) + { + value = + editableDisplayGroup.valueForObjectAtIndex( aRowIndex, editableKey ); + } + else // using an external display group to determine editability + { + // ignore index and use the selected object value from display group + value = + editableDisplayGroup.selectedObjectValueForKey( editableKey ); + } + } + else + { + // treat bound key without display group as a value + value = editableKey; + } + if ( value == null ) return false; // null defaults to false + Boolean result = (Boolean) + ValueConverter.convertObjectToClass( value, Boolean.class ); + if ( result == null ) return true; // non-null defaults to true + return result.booleanValue(); + } + + // convenience + + private TableColumn component() + { + return (TableColumn) object(); + } + + /** + * Called by TableAssociation to get a EOSortOrdering suitable + * for the information in this column. + * This implementation returns a EOSortOrdering with the key + * equal to the value aspect's key and the appropriate selector + * for the specified ascending value and the case sensitivity + * of this column. + * Override to customize the sort for your column. + */ + public EOSortOrdering getSortOrdering( boolean isAscending ) + { + if ( isAscending ) + { + if ( isSortCaseSensitive() ) + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareAscending ) ; + } + else + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareCaseInsensitiveAscending ) ; + } + } + else + { + if ( isSortCaseSensitive() ) + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareDescending ) ; + } + else + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareCaseInsensitiveDescending ) ; + } + } + } + + /** + * Returns the one-based index of this assocation's sort ordering + * in the specified list of orderings. If the sign of the returned + * value is negative, the ordering is descending. If the return + * value is zero, no matching ordering was found. + */ + protected int getIndexOfMatchingOrdering( List orderings ) + { + // find index of matching ordering + int index = 0; + EOSortOrdering ordering = null; + Iterator i = orderings.iterator(); + while ( i.hasNext() ) + { + index++; + ordering = (EOSortOrdering) i.next(); + if ( ordering.key().equals( valueKey ) ) + { + // determine ascending or descending + if ( getSortOrdering( true ).equals( ordering ) ) + { + return index; + } + else + if ( getSortOrdering( false ).equals( ordering ) ) + { + return -index; + } + } + } + return 0; + + } + + /** + * Called by TableAssociation to draw some indicator in the + * specified rectangle using the specified graphics to indicate + * the specified sort state. The rectangle corresponds to the + * bounds of the column header. + * This implementation draws a small transparent gray triangle at + * the right edge of the bounding rectangle. + * Override to do something different or to do nothing at all. + */ + protected void drawSortIndicator( Rectangle aBoundingRectangle, + Graphics aGraphicsContext, List orderings ) + { + int index = getIndexOfMatchingOrdering( orderings ); + if ( index == 0 ) return; + + boolean isAscending = ( index > 0 ); + index = Math.abs( index ); + + // turn on anti-aliasing + if ( aGraphicsContext instanceof Graphics2D ) + { + ((Graphics2D)aGraphicsContext).setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + } + + Rectangle r = new Rectangle( aBoundingRectangle ); + + // resize to a right-justified square, sides equal to height + r.setBounds( r.x + r.width - r.height, r.y, r.height, r.height ); + + // resize to about a third smaller + int portion = r.height / 3; + r.grow( -portion, -portion ); + + // transparencies cause java2d printing to rasterize, + // resulting in excessive memory usage and print time. + // aGraphicsContext.setColor( new Color( 0, 0, 0, 255 / (index*2) ) ); + aGraphicsContext.setColor( getSortIndicatorColor( index ) ); + + Polygon triangle; + if ( !isAscending ) + { + triangle = new Polygon( + new int[] { r.x, r.x+r.width/2, r.x+r.width }, + new int[] { r.y, r.y+r.height, r.y }, 3 ); + } + else + { + triangle = new Polygon( + new int[] { r.x, r.x+r.width/2, r.x+r.width }, + new int[] { r.y+r.height, r.y, r.y+r.height }, 3 ); + } + aGraphicsContext.fillPolygon( triangle ); + } + + /** + * Returns a color to be used by the sort indicator based on the index + * of the sorting column. The goal of this method is to make the color + * appear lighter and lighter, the "less" primary the sort order for this + * column is. This can be acheives simply though a "transparent" color, + * however, during printing of the corresponding table, java print + * kicks into "raster" based printing when printing a component with + * a transparent color instead of "vector" based printing. Raster + * based printing can take up to 20-30 times longer to print than + * vector printing and consume several times the amount of memory. + * Raster-based printing should be avoided at all costs if the a component + * is to be printed (as of Java 1.3.1). + * @param index The "sort" index of the associated table column. The higher + * the index, the lighter the color will be. An index of 0 will + * return null. + * @return The color to use when rendering the sort indicator. + */ + protected static Color getSortIndicatorColor( int index ) + { + if ( index == 0 ) return null; + + // Create the color list if not already created. + if ( sortIndicatorColorList == null ) + { + // Default size to 13 elements, it would be extremely rare that a + // user sorts more than 12 columns at a time (although possible). + // (Index 0 is not used.) + sortIndicatorColorList = new Color[ 13 ]; + } + + // Get the color out of the color list. Use the index directly as + // an index into an ordered list. If the color has already been + // created for that index, then return it, otherwise create the color. + if ( ( index < sortIndicatorColorList.length ) && + ( sortIndicatorColorList[ index ] != null ) ) + { + return sortIndicatorColorList[ index ]; + } + + // The following logic performs the same affect as the above + // transparent color, without actually using a transparent color. + // Start with the table header's background color and derive a color + // that is "darker" than that color. Any color this logic creates will + // be between those two colors. + Color lightColor = java.awt.SystemColor.control; + Color darkColor = lightColor.darker().darker(); + + // Make the light color (the upper bound) a little darker, so that even + // the lightest triangle will still be slightly visible. + lightColor = new Color( + Math.max( ( int )( lightColor.getRed() * 0.9), 0 ), + Math.max( ( int )( lightColor.getGreen() * 0.9), 0 ), + Math.max( ( int )( lightColor.getBlue() * 0.9), 0) ); + + // Subtract the light color from the dark color. This is the range + // between the two colors. + Color difference = new Color( lightColor.getRed() - darkColor.getRed(), + lightColor.getGreen() - darkColor.getGreen(), + lightColor.getBlue() - darkColor.getBlue() ); + + // If the index is 1, user the dark color as is. Otherwise scale the + // color closer and closer to the lighter color as the index gets + // biggger and bigger. + if ( index > 1 ) + { + float factor = ( float )Math.pow( 0.5, ( index - 1 ) ); + darkColor = new Color( + Math.max( lightColor.getRed() - ( int )( difference.getRed() * factor ), 0 ), + Math.max( lightColor.getGreen() - ( int )( difference.getGreen() * factor ), 0 ), + Math.max( lightColor.getBlue() - ( int )( difference.getBlue() * factor ), 0 ) ); + } + + // Cache the created color in the color list for this index. + if ( index >= sortIndicatorColorList.length ) + { + // The color list is too small, create a new larger list with + // some padding for even larger indicies. + Color[] oldList = sortIndicatorColorList; + sortIndicatorColorList = new Color[ index + 5 ]; + System.arraycopy( oldList, 0, sortIndicatorColorList, 0, oldList.length ); + } + sortIndicatorColorList[ index ] = darkColor; + + return darkColor; + } +} + +/* + * $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.16 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.15 2002/08/22 15:42:49 mpowers + * No longer using transparency to render sort indicator (see comments). + * + * Revision 1.14 2002/04/12 21:05:57 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.13 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.12 2002/03/04 22:11:43 mpowers + * Darkened the sort indicator to better differentiate the first sort. + * + * Revision 1.11 2002/03/04 03:58:17 mpowers + * Refined table header click behavior. + * + * Revision 1.10 2002/03/01 15:42:00 mpowers + * Table column headers now always show their sort indicator. + * A third table-column click clears the sort for that column. + * + * Revision 1.9 2002/02/28 23:01:39 mpowers + * TableColumnAssociations add and remove themselves from the TableAssociation + * when their connection is established and broken respectively. + * TableAssociations now break connection if they have no column associations. + * + * Revision 1.8 2001/06/05 16:03:56 mpowers + * Flipped the triangle to be consistent with Aqua. + * + * Revision 1.7 2001/03/09 22:09:22 mpowers + * Now better handling jdk1.1 for rendering the column header. + * + * Revision 1.6 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.5 2001/01/12 19:11:56 mpowers + * Fixed table column click sorting. + * + * Revision 1.4 2001/01/12 17:20:30 mpowers + * Moved EOSortOrdering creation to ColumnAssociation. + * + * Revision 1.3 2001/01/11 21:55:57 mpowers + * Implemented sort indicator for table column headers. + * + * Revision 1.2 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.1.1.1 2000/12/21 15:49:03 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java new file mode 100644 index 0000000..6aa27c3 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java @@ -0,0 +1,1212 @@ +/* +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.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.text.Format; +import java.text.ParseException; +import java.util.Enumeration; +import java.util.Iterator; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSNotificationQueue; +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; + +/** +* TextAssociation binds JTextComponents and other objects +* with getText() and setText() methods to a display group. +* Note that JLabels are supported with both the Text and +* Icon aspects. +* Bindings are: +* <ul> +* <li>value: a property convertable to/from a string</li> +* <li>editable: a boolean property that determines whether +* the user can edit the text in the field</li> +* <li>enabled: a boolean property that determines whether +* the user can select the text in the field</li> +* <li>visible: a boolean property that determines whether +* the field is visible</li> +* <li>label: a boolean property that determines whether +* field should appear as a read-only, selectable label</li> +* <li>icon: a property that returns a Swing icon, for use +* with JLabels and other components with setIcon() methods. +* If bound to a static string, the string will be used to +* load an image resource from the selected object's class.</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TextAssociation extends EOAssociation + implements FocusListener, ActionListener, DocumentListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, EditableAspect, VisibleAspect, LabelAspect, IconAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text", "enabled", "editable", "visible" + } ); + + private final static NSSelector getText = + new NSSelector( "getText" ); + private final static NSSelector setText = + new NSSelector( "setText", + new Class[] { String.class } ); + private final static NSSelector getDocument = + new NSSelector( "getDocument" ); + private final static NSSelector setIcon = + new NSSelector( "setIcon", + new Class[] { Icon.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 } ); + + // null handling + protected boolean wasNull; + protected static final String EMPTY_STRING = ""; + + // dirty handling + protected boolean needsUpdate; + protected boolean hasDocument; + protected boolean isListening; + + // formatting + protected Format format; + + // on-the-fly validation + protected boolean activeUpdate; + + // type conversion + protected Class lastKnownType; + + // cache the value aspect + private EODisplayGroup valueDisplayGroup; + private String valueKey; + + // hacky flags needed for no activeUpdate + private boolean pleaseIgnoreNextChange = false; + private boolean pleaseAcceptNextChange = false; + private boolean externallyChanged = true; + + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public TextAssociation ( Object anObject ) + { + super( anObject ); + wasNull = false; + needsUpdate = false; + activeUpdate = true; + hasDocument = false; + isListening = true; + valueDisplayGroup = null; + valueKey = null; + format = null; + lastKnownType = null; + + // register for idle notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + NSNotificationCenter.defaultCenter().addObserver( + this, handleNotification, null, this ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 setText.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 ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener 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 ); + } + hasDocument = false; + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).addDocumentListener( this ); + hasDocument = true; + } + } + } + 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 ); + } + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).removeDocumentListener( this ); + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while breaking connection", exc ); + } + super.breakConnection(); + } + + public void objectWillChange( Object anObject ) + { + super.objectWillChange( anObject ); + externallyChanged = true; + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged() + { + if ( pleaseIgnoreNextChange ) + { + pleaseIgnoreNextChange = false; + externallyChanged = false; + return; + } + + externallyChanged = true; + + Object component = object(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) ); + } + + // if activeUpdate or we are not the editing association + if ( activeUpdate || displayGroup.editingAssociation() != this || pleaseAcceptNextChange ) + { + pleaseAcceptNextChange = false; + key = valueKey; + + 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 = previousValue; + + // 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 && previousValue != null + && !currentValue.toString().equals( previousValue.toString() ) ) { + value = null; + break; + } + + } // end while + + } else { + + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + // null handling + if ( value == null ) + { + wasNull = true; + value = EMPTY_STRING; + lastKnownType = null; + } + else + { + wasNull = false; + lastKnownType = value.getClass(); + if ( format() != null ) + { + try + { + value = format().format( value ); + } + catch ( IllegalArgumentException exc ) + { + value = value.toString(); + } + } + } + + + try + { + if ( needToReadValueFromDisplayGroup( value.toString(), getText ) ) + { + // No need to listen for any events that might get fired + // while setting the text since we are the one setting it. + boolean wasListening = isListening; + isListening = false; + + // setText is an expensive operation + setText.invoke( component, value.toString() ); + + isListening = wasListening; + needsUpdate = false; + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + } + + // icon aspect + displayGroup = displayGroupForAspect( IconAspect ); + key = displayGroupKeyForAspect( IconAspect ); + if ( key != null ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group + // as a resource to be loaded from the selected class. + value = null; + Object o = displayGroup.selectedObject(); + if ( o != null ) + { + URL url = o.getClass().getResource( key ); + if ( url != null ) + { + value = new ImageIcon( url ); + } + } + } + + try + { + setIcon.invoke( component, value ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( 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 ( ((Component)component).isEnabled() != converted.booleanValue() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + // editable aspect + displayGroup = displayGroupForAspect( EditableAspect ); + key = displayGroupKeyForAspect( EditableAspect ); + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + 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 ) + { + if ( converted.booleanValue() != ((JTextComponent)component).isEditable() ) + { + ((JTextComponent)component).setEditable( converted.booleanValue() ); + } + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + key = displayGroupKeyForAspect( VisibleAspect ); + if ( ( 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 = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() != ((Component)component).isVisible() ) + { + ((Component)component).setVisible( converted.booleanValue() ); + } + } + } + + // label aspect + displayGroup = displayGroupForAspect( LabelAspect ); + key = displayGroupKeyForAspect( LabelAspect ); + + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + 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 ) + { + if ( converted.booleanValue() ) + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + areaToLabel( (JTextArea) component ); + } + else + { + fieldToLabel( (JTextComponent) component ); + } + } + } + else + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + labelToArea( (JTextArea) component ); + } + else + { + labelToField( (JTextComponent ) component ); + } + } + } + } + } + } + + private void fieldToLabel( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(false); + aTextField.setOpaque(false); + + // Set the border, colors and font to that of a label + + //LookAndFeel.installBorder(aTextField, "Label.border"); + aTextField.setBorder( null ); + + LookAndFeel.installColorsAndFont(aTextField, + "Label.background", + "Label.foreground", + "Label.font"); + } + + private void labelToField( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(true); + aTextField.setOpaque(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "TextField.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "TextField.background", + "TextField.foreground", + "TextField.font"); + } + + private void areaToLabel( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setLineWrap(true); + aTextArea.setWrapStyleWord(true); + aTextArea.setEditable(false); + + // Set the text area's border, colors and font to + // that of a label + + //LookAndFeel.installBorder(aTextArea, "Label.border"); + aTextArea.setBorder( null ); + + LookAndFeel.installColorsAndFont(aTextArea, + "Label.background", + "Label.foreground", + "Label.font"); + + } + + private void labelToArea( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setEditable(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextArea, "TextArea.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "TextArea.background", + "TextArea.foreground", + "TextArea.font"); + } + + + /** + * 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() + { + pleaseAcceptNextChange = true; + pleaseIgnoreNextChange = false; + 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() + { + boolean returnValue = true; + if ( hasDocument && !needsUpdate ) return true; + + EODisplayGroup displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + String key = valueKey; + Object component = object(); + Object value = null; + try + { + //if ( getText.implementedByObject( component ) ) + //{ + value = getText.invoke( component ); + //} + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) ) + { + value = null; + } + else + if ( format() != null ) + { + try + { + value = format().parseObject( value.toString() ); + } + catch ( ParseException exc ) + { + String message = exc.getMessage(); + //"That format was not recognized."; + if ( displayGroup.associationFailedToValidateValue( + this, value.toString(), key, exc, message ) ) + { + boolean wasListening = isListening; + isListening = false; + JOptionPane.showMessageDialog( + (Component)component, message ); + isListening = wasListening; + } + needsUpdate = false; + return false; + } + } + + if ( ( lastKnownType != null ) && ( value != null ) ) + { + // convert back to last known type, if necessary/possible + Class type = value.getClass(); + if ( ( type != null ) && ( type != lastKnownType ) ) + { + Object converted = + ValueConverter.convertObjectToClass( + value, lastKnownType ); + if ( converted != null ) + { + value = converted; + } + // else: not possible, ignore + } + } + + needsUpdate = false; + + // only update if the value is different from the one in the display group + if ( ! needToWriteValueToDisplayGroup( value, displayGroup ) ) return true; + + // we might lose focus if display group displays a validation message + boolean wasListening = isListening; + isListening = false; + + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + needsUpdate = false; + } + else + { + needsUpdate = false; + returnValue = false; + } + } + isListening = wasListening; + + } + return returnValue; + } + + /** + * Called to determine whether the display group needs to be + * updated. This implementation reads the value from the display + * group and only returns true if the specified value is different. + * This is done as an optimization since writes are more expensive + * than reads. Override to customize this behavior. + */ + protected boolean needToWriteValueToDisplayGroup( + Object aValue, EODisplayGroup aDisplayGroup ) + { + Object existingValue = aDisplayGroup.selectedObjectValueForKey( valueKey ); + if ( aDisplayGroup.selectedObjects().size() == 1 ) + { + if ( existingValue == aValue ) return false; + if ( ( existingValue != null ) && ( existingValue.equals( aValue ) ) ) return false; + if ( ( aValue != null ) && ( aValue.equals( existingValue ) ) ) return false; + } + return true; + } + + /** + * Called to determine whether the controlled component needs to be + * updated. This implementation reads the value from the selector + * and only returns true if the specified value is different. + * This is done as an optimization since updating the component + * can be an expensive operation. Override to customize this behavior. + */ + protected boolean needToReadValueFromDisplayGroup( + Object aValue, NSSelector aSelector ) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException + { + return !aValue.toString().equals( aSelector.invoke( object() ) ); + } + + /** + * Sets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public void setFormat( Format aFormat ) + { + format = aFormat; + } + + /** + * Gets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public Format format() + { + return format; + } + + /** + * Returns whether the text association is configured to actively + * update the model in response to changes in the component. + */ + public boolean isActiveUpdate() + { + return activeUpdate; + } + + /** + * Sets whether the text association should actively + * update the model in response to changes in the component. + * Default is true. False indicates that the model will be updated + * only when the component loses focus or fires an action event. + */ + public void setActiveUpdate( boolean isActiveUpdate ) + { + activeUpdate = isActiveUpdate; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + if ( ! isListening ) return; + if ( needsUpdate ) + { + pleaseAcceptNextChange = true; // needed if activeUpdate = false + writeValueToDisplayGroup(); + } + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + if ( ! isListening ) return; + + pleaseAcceptNextChange = true; + externallyChanged = true; + + 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 ( ! isListening ) return; + 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, + } + } + + /** + * Queues a notification to PostWhenIdle. + */ + protected void queueUpdate(DocumentEvent e) + { + if ( e.getDocument() instanceof DefaultStyledDocument ) + { + if ( e instanceof AbstractDocument.DefaultDocumentEvent ) + { + int docLength = e.getDocument().getLength(); + + if ( ( e.getType().equals( DocumentEvent.EventType.CHANGE ) ) ) + { + if ( e.getOffset() == 0 && e.getLength() == docLength ) + { + // ignore document events for the whole document + // since default styled document broadcasts these + // using invokeLater, and we've already received + // notification about the actual style change. + // see: DefaultStyledDocument.ChangeUpdateRunnable + return; + } + } + } + } + + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( "TextAssociation.DocumentChanged", this, + new NSDictionary( new Object[] { "event" }, new Object[] { e } ) ), + NSNotificationQueue.PostWhenIdle ); + } + + /** + * Handles idle notification. + */ + public void handleNotification( NSNotification aNotification ) + { + if ( activeUpdate ) + { + writeValueToDisplayGroup(); + } + } + + // interface DocumentListener + + public void insertUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + + public void removeUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + + public void changedUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + +} + +/* + * $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.41 2004/02/05 02:18:18 mpowers + * Now setting border to null to new Aqua LAF behaves. + * + * Revision 1.40 2004/01/28 22:47:56 mpowers + * un-activeUpdate was brokne. + * + * Revision 1.39 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.38 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.37 2003/02/06 16:21:34 mpowers + * Fix for activeUpdate: no longer bothering with editing context's changes. + * + * Revision 1.36 2002/10/24 18:19:24 mpowers + * Bug fix - thanks to dwang. + * + * Revision 1.35 2002/08/02 19:19:30 mpowers + * Added control points for when to read or write from the display group. + * Added flags needed to fix problems with non-activeUpdate and commit key. + * + * Revision 1.33 2002/03/08 23:18:01 mpowers + * Added visible aspect. + * + * Revision 1.32 2002/03/06 16:13:53 mpowers + * Yet another fix for style document changes: swing's DefaultStyledDocument + * using an invoke later to launch a final StyleChanged event, which occurs + * after the TextAssociation has reestablished itself as a document listener, + * causing the item to be marked dirty. We're now handling this case. + * + * Revision 1.31 2002/03/04 22:10:37 mpowers + * Supressing active update only marks dirty when contents have changed. + * + * Revision 1.30 2002/02/23 16:19:12 mpowers + * Now only marking an editing context as dirty if it's not already dirty. + * + * Revision 1.29 2002/02/19 18:38:29 mpowers + * Minor optimization: activeUpdate now checked in handleNotification. + * + * Revision 1.28 2002/02/19 16:36:47 mpowers + * Better support for active update: objects are now marked as changed + * even though the model itself is not updated -- this allows editing + * context itself to be marked as having changes to be saved. + * + * Revision 1.27 2002/01/23 19:50:11 mpowers + * Fix for a null pointer when value is null and last known type is not. + * (from dwang) + * + * Revision 1.26 2002/01/14 19:37:22 mpowers + * Fix for NPE when value is null and auto update is false. + * + * Revision 1.25 2001/12/10 03:16:11 mpowers + * Fixed bug with isListening when no items are in display group. + * + * Revision 1.24 2001/11/16 19:14:51 mpowers + * Brought back the idea of configuring whether updates occur on each change. + * + * Revision 1.23 2001/11/08 20:06:06 mpowers + * Now performing type-conversion as a convenience. + * + * Revision 1.22 2001/11/04 18:24:20 mpowers + * Better handling for non-string values when bulk-editing. + * + * Revision 1.21 2001/11/01 15:53:34 mpowers + * Now that NSNotificationQueue correctly implements PostWhenIdle, we can + * finally discard our use of Swing's Timer in favor of using the queue + * to coalesce document changed events. + * + * Revision 1.20 2001/10/26 19:58:06 mpowers + * Better handling for non-string types. We were testing with equals with the + * new value against the existing value in the component. Now we convert + * the new value to a string before comparing. Fixes case for properties + * of non-String types, like StringBuffer. + * + * Revision 1.19 2001/09/30 21:57:14 mpowers + * Timers were not getting cleaned up if breakConnection was called + * before the timer got a chance to fire. + * + * Revision 1.18 2001/08/22 15:42:26 mpowers + * Added support for JTextComponent label-izing. + * + * Revision 1.17 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.16 2001/07/17 19:53:37 mpowers + * Made some private fields protected for benefit of subclassers. + * + * Revision 1.15 2001/06/30 14:59:36 mpowers + * LabelAspect now sets the text field's opaque setting. + * + * Revision 1.14 2001/06/29 14:54:08 mpowers + * Another fix for timers - timers were definitely causing a memory leak. + * + * Revision 1.13 2001/06/26 21:37:19 mpowers + * Fixed a null pointer in the new key timer scheme. + * + * Revision 1.12 2001/06/25 14:46:03 mpowers + * Fixed a memory leak involving the use of timers. + * + * Revision 1.11 2001/06/01 19:14:59 mpowers + * Text association's enabled aspect is now more discriminating. + * + * Revision 1.10 2001/05/18 21:07:24 mpowers + * Changed the way we handle failure to update object value. + * + * Revision 1.9 2001/03/13 21:39:58 mpowers + * Improved validation handling. + * + * Revision 1.8 2001/03/12 12:49:10 mpowers + * Improved validation handling. + * Having a formatter disables auto-updating. + * + * Revision 1.7 2001/03/09 22:08:13 mpowers + * Now handling any objects that have a valid Document. + * No longer checking enabled before updating the enabled state. + * + * Revision 1.6 2001/03/07 19:57:32 mpowers + * Fixed paste error in IconAspect. + * + * Revision 1.4 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.3 2001/01/31 19:12:33 mpowers + * Implemented auto-updating in TextComponent. + * + * Revision 1.2 2001/01/10 15:53:58 mpowers + * Preventing a null pointer exception if getText were to return null, + * which doesn't happen for JTextFields but might happen for other objects. + * + * Revision 1.1.1.1 2000/12/21 15:49:08 mpowers + * Contributing wotonomy. + * + * Revision 1.13 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java new file mode 100644 index 0000000..49879e9 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java @@ -0,0 +1,1029 @@ +/* +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.net.URL; +import java.text.Format; +import java.text.ParseException; +import java.util.Enumeration; +import java.util.Iterator; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; + +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; + +/** +* TimedTextAssociation works like TextAssociation, +* but instead of using a delayed event to update the +* model, it uses a timer so that the model is only +* updated if the user pauses typing for some short interval. +* This is useful when the update and/or re-read of the model +* is a costly operation. +* Bindings are: +* <ul> +* <li>value: a property convertable to/from a string</li> +* <li>editable: a boolean property that determines whether +* the user can edit the text in the field</li> +* <li>enabled: a boolean property that determines whether +* the user can select the text in the field</li> +* <li>label: a boolean property that determines whether +* field should appear as a read-only, selectable label</li> +* <li>icon: a property that returns a Swing icon, for use +* with JLabels and other components with setIcon() methods. +* If bound to a static string, the string will be used to +* load an image resource from the selected object's class.</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TimedTextAssociation extends EOAssociation + implements FocusListener, ActionListener, DocumentListener +{ + //TODO: need to refactor this so that it can subclass text association. + //This implementation is basically a branch from the v1.20 TextAssociation. + + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, EditableAspect, LabelAspect, IconAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text", "enabled", "editable" + } ); + + private final static NSSelector getText = + new NSSelector( "getText" ); + private final static NSSelector setText = + new NSSelector( "setText", + new Class[] { String.class } ); + private final static NSSelector getDocument = + new NSSelector( "getDocument" ); + private final static NSSelector setIcon = + new NSSelector( "setIcon", + new Class[] { Icon.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 } ); + + // null handling + protected boolean wasNull; + protected static final String EMPTY_STRING = ""; + + // dirty handling + protected boolean needsUpdate; + protected boolean hasDocument; + protected boolean isListening; + + // formatting + protected Format format; + + // cache the value aspect + private EODisplayGroup valueDisplayGroup; + private String valueKey; + + // coalescing document events + protected boolean autoUpdating; + protected int interval = 400; // adjust as needed + protected Timer keyTimer; + + // NOTE: a new key timer is created for each use and + // is disposed when the timer is stopped. + // Swing's Timer class is kept in a static list of timers + // and each retains a strong reference to their listeners. + // This caused a memory leak as associations typically + // refer to their controlled component which is referred + // to by its parents and so on until no application window + // will ever get garbage collected. yikes. + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public TimedTextAssociation ( Object anObject ) + { + super( anObject ); + wasNull = false; + needsUpdate = false; + hasDocument = false; + isListening = true; + valueDisplayGroup = null; + valueKey = null; + + autoUpdating = true; + keyTimer = 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 setText.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 ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener 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 ); + } + hasDocument = false; + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).addDocumentListener( this ); + hasDocument = true; + } + } + } + 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 ); + } + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).removeDocumentListener( 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 = valueDisplayGroup; + if ( displayGroup != null ) + { + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) ); + } + + key = valueKey; + + 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 { + + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + // convert value to string + if ( value == null ) + { + wasNull = true; + value = EMPTY_STRING; + } + else + { + wasNull = false; + if ( format() != null ) + { + try + { + value = format().format( value ); + } + catch ( IllegalArgumentException exc ) + { + value = value.toString(); + } + } + } + + + try + { + if ( ! value.toString().equals( getText.invoke( component ) ) ) + { + // No need to listen for any events that might get fired + // while setting the text since we are the one setting it. + boolean wasListening = isListening; + isListening = false; + + // setText is an expensive operation + setText.invoke( component, value.toString() ); + + isListening = wasListening; + needsUpdate = false; + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // icon aspect + displayGroup = displayGroupForAspect( IconAspect ); + key = displayGroupKeyForAspect( IconAspect ); + if ( key != null ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group + // as a resource to be loaded from the selected class. + value = null; + Object o = displayGroup.selectedObject(); + if ( o != null ) + { + URL url = o.getClass().getResource( key ); + if ( url != null ) + { + value = new ImageIcon( url ); + } + } + } + + try + { + setIcon.invoke( component, value ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( 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 ( ((Component)component).isEnabled() != converted.booleanValue() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + // editable aspect + displayGroup = displayGroupForAspect( EditableAspect ); + key = displayGroupKeyForAspect( EditableAspect ); + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + 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 ) + { + if ( converted.booleanValue() != ((JTextComponent)component).isEditable() ) + { + ((JTextComponent)component).setEditable( converted.booleanValue() ); + } + } + } + + // label aspect + displayGroup = displayGroupForAspect( LabelAspect ); + key = displayGroupKeyForAspect( LabelAspect ); + + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + 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 ) + { + if ( converted.booleanValue() ) + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + areaToLabel( (JTextArea) component ); + } + else + { + fieldToLabel( (JTextComponent) component ); + } + } + } + else + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + labelToArea( (JTextArea) component ); + } + else + { + labelToField( (JTextComponent ) component ); + } + } + } + } + } + + } + + private void fieldToLabel( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(false); + aTextField.setOpaque(false); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "Label.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "Label.background", + "Label.foreground", + "Label.font"); + } + + private void labelToField( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(true); + aTextField.setOpaque(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "TextField.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "TextField.background", + "TextField.foreground", + "TextField.font"); + } + + private void areaToLabel( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setLineWrap(true); + aTextArea.setWrapStyleWord(true); + aTextArea.setEditable(false); + + // Set the text area's border, colors and font to + // that of a label + + LookAndFeel.installBorder(aTextArea, "Label.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "Label.background", + "Label.foreground", + "Label.font"); + + } + + private void labelToArea( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setEditable(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextArea, "TextArea.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "TextArea.background", + "TextArea.foreground", + "TextArea.font"); + } + + + /** + * 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 () + { + if ( keyTimer != null ) + { + keyTimer.stop(); + keyTimer.removeActionListener( this ); + keyTimer = null; + } + 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() + { + boolean returnValue = true; + if ( hasDocument && !needsUpdate ) return true; + + EODisplayGroup displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + String key = valueKey; + Object component = object(); + Object value = null; + try + { + //if ( getText.implementedByObject( component ) ) + //{ + value = getText.invoke( component ); + //} + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) ) + { + value = null; + } + else + if ( format() != null ) + { + try + { + value = format().parseObject( value.toString() ); + } + catch ( ParseException exc ) + { + String message = exc.getMessage(); + //"That format was not recognized."; + if ( displayGroup.associationFailedToValidateValue( + this, value.toString(), key, exc, message ) ) + { + boolean wasListening = isListening; + isListening = false; + JOptionPane.showMessageDialog( + (Component)component, message ); + isListening = wasListening; + } + needsUpdate = false; + return false; + } + } + + needsUpdate = false; + + // only update if the value is different from the one in the display group + Object existingValue = displayGroup.selectedObjectValueForKey( key ); + if ( displayGroup.selectedObjects().size() == 1 ) + { + if ( existingValue == value ) return true; + if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true; + if ( ( value != null ) && ( value.equals( existingValue ) ) ) return true; + } + + // we might lose focus if display group displays a validation message + boolean wasListening = isListening; + isListening = false; + + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + isListening = wasListening; + needsUpdate = false; + } + else + { + isListening = wasListening; + needsUpdate = false; + returnValue = false; + } + } + + } + return returnValue; + } + + /** + * Sets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + * Having a formatter disables auto-updating. + */ + public void setFormat( Format aFormat ) + { + format = aFormat; + } + + /** + * Gets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public Format format() + { + return format; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + if ( keyTimer != null ) + { + keyTimer.stop(); + keyTimer.removeActionListener( this ); + keyTimer = null; + } + if ( ! isListening ) return; + if ( needsUpdate ) + { + writeValueToDisplayGroup(); + } + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + if ( ! isListening ) return; + 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 ( ! isListening ) return; + 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, + } + } + + /** + * Returns whether the data model is updated for every change + * in the controlled component. If false, the data is only + * updated on focus lost or the enter key. Default is true. + */ + public boolean isAutoUpdating() + { + if ( format() != null ) return false; + return autoUpdating; + } + + /** + * Sets whether the data model is updated for every change + * in the controlled component. + */ + public void setAutoUpdating( boolean isAutoUpdating ) + { + autoUpdating = isAutoUpdating; + } + + /** + * Triggers the key timer to start. + */ + protected void queueUpdate() + { + if ( isAutoUpdating() ) + { + if ( keyTimer == null ) + { + keyTimer = new Timer( interval, this ); + } + keyTimer.restart(); + } + } + + // interface DocumentListener + + public void insertUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + + public void removeUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + + public void changedUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + +} + +/* + * $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.3 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/12/20 18:57:24 mpowers + * (Re-)Contributing TimedTextAssociation. Just like TA, except uses timers. + * + * Revision 1.20 2001/10/26 19:58:06 mpowers + * Better handling for non-string types. We were testing with equals with the + * new value against the existing value in the component. Now we convert + * the new value to a string before comparing. Fixes case for properties + * of non-String types, like StringBuffer. + * + * Revision 1.19 2001/09/30 21:57:14 mpowers + * Timers were not getting cleaned up if breakConnection was called + * before the timer got a chance to fire. + * + * Revision 1.18 2001/08/22 15:42:26 mpowers + * Added support for JTextComponent label-izing. + * + * Revision 1.17 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.16 2001/07/17 19:53:37 mpowers + * Made some private fields protected for benefit of subclassers. + * + * Revision 1.15 2001/06/30 14:59:36 mpowers + * LabelAspect now sets the text field's opaque setting. + * + * Revision 1.14 2001/06/29 14:54:08 mpowers + * Another fix for timers - timers were definitely causing a memory leak. + * + * Revision 1.13 2001/06/26 21:37:19 mpowers + * Fixed a null pointer in the new key timer scheme. + * + * Revision 1.12 2001/06/25 14:46:03 mpowers + * Fixed a memory leak involving the use of timers. + * + * Revision 1.11 2001/06/01 19:14:59 mpowers + * Text association's enabled aspect is now more discriminating. + * + * Revision 1.10 2001/05/18 21:07:24 mpowers + * Changed the way we handle failure to update object value. + * + * Revision 1.9 2001/03/13 21:39:58 mpowers + * Improved validation handling. + * + * Revision 1.8 2001/03/12 12:49:10 mpowers + * Improved validation handling. + * Having a formatter disables auto-updating. + * + * Revision 1.7 2001/03/09 22:08:13 mpowers + * Now handling any objects that have a valid Document. + * No longer checking enabled before updating the enabled state. + * + * Revision 1.6 2001/03/07 19:57:32 mpowers + * Fixed paste error in IconAspect. + * + * Revision 1.4 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.3 2001/01/31 19:12:33 mpowers + * Implemented auto-updating in TextComponent. + * + * Revision 1.2 2001/01/10 15:53:58 mpowers + * Preventing a null pointer exception if getText were to return null, + * which doesn't happen for JTextFields but might happen for other objects. + * + * Revision 1.1.1.1 2000/12/21 15:49:08 mpowers + * Contributing wotonomy. + * + * Revision 1.13 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java new file mode 100644 index 0000000..728643b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java @@ -0,0 +1,582 @@ +/* +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.EventQueue; +import java.awt.Rectangle; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Vector; + +import javax.swing.JTree; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreePath; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TreeAssociation is a TreeModelAssociation further +* customized for JTrees. It binds a JTree to a display group's +* list of displayable objects, each of which may have +* a list of child objects managed by another display +* group, and so on. TreeAssociation works exactly +* like a ListAssociation, with the additional capability +* to specify a "Children" aspect, that will allow child +* objects to be retrieved from a parent display group. +* Note that the children aspect requires the bound +* display group to have a DataSource that can vend a +* DataSource appropriate for the bound key That data +* source is then used to create data sources for +* child nodes, and so on. +* +* <ul> +* +* <li>titles: a property convertable to a string for +* display in the nodes of the tree. The objects in +* the bound display group will be used to populate the +* initial root nodes of the tree (more accurately, +* children of the offscreen root node in the tree).</li> +* +* <li>children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* The data source of the bound display group is replaced +* a data source that populates the display group with +* the visible nodes in the tree component as determined by +* calling fetchObjectsIntoChildrenGroup. +* If this aspect is not bound, the tree behaves like a list. +* <br><br> +* Binding this aspect with a null display group is the same +* as binding it with the titles display group. +* In this configuration the contents of the titles +* display group will be replaced with the visible nodes in the +* tree component, as specified above, replacing the existing +* data source. +* <br><br> +* In that case, the display groups for the nodes in +* the tree will still use the original data source +* for resolving their children key, and programmatically +* setting the contents of the display group will still +* repopulate the root nodes of the tree. +* </li> +* +* <li>isLeaf: a property of a node value that returns +* a value convertable to a boolean value (aside from +* an actual boolean value, zeroes evaluate to true, +* as does any String containing "yes" or "true" or that +* is convertable to a number equal to zero; other values +* evaluate to false). +* <br><br> +* If the isLeaf aspect is not bound, +* the tree must force nodes to load their children to +* determine whether they are leaf nodes (in effect +* loading the grandchildren for any expanded node). +* If bound, child loading is deferred until the node +* is actually expanded. +* <br><br> +* For example, binding this value to a null +* display group and the key "false" will result in a +* deferred-loading tree that works much like Windows +* Explorer's network volume browser - all nodes appear +* with "pluses" until they are expanded. +* <br><br> +* Note that the display group is ignored: the property +* will be applied directly to the object corresponding +* to the node.</li> +* +* </ul> +* +* All other usage is as TreeModelAssociation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeAssociation extends TreeModelAssociation + implements FocusListener, TreeExpansionListener, TreeWillExpandListener, Runnable +{ + private boolean isExpanding; + private Vector nodeQueue; + private Vector structureQueue; + private boolean isRunning; + + /** + * Constructor expecting a JTree. + */ + public TreeAssociation ( Object anObject ) + { + super( anObject ); + init(); + } + + /** + * Constructor expecting a JTree or similar component + * and specifying a label for the root node. + */ + public TreeAssociation( Object anObject, Object aRootLabel ) + { + super( anObject ); + init(); + rootLabel = aRootLabel; + rootNode.setUserObject( aRootLabel ); + } + + // convenience + private JTree component() + { + return (JTree) object(); + } + + /** + * Called by both constructors. + */ + protected void init() + { + isExpanding = false; + isRunning = false; + nodeQueue = new Vector(); + structureQueue = new Vector(); + component().addFocusListener( this ); + component().addTreeExpansionListener( this ); + component().addTreeWillExpandListener( this ); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JTree ); + } + + /** + * Overridden to not fire events during initial population. + */ + public void establishConnection () + { + isExpanding = true; + super.establishConnection(); + isExpanding = false; + } + + // interface TreeSelectionListener + + public void valueChanged(TreeSelectionEvent e) + { + if ( ! isListening ) return; +// NOTE: This approach causes focus rectangle to perceptibly trail +// the selection rectangle, presumably because we're called after the +// new selection has been processed but before the focus is processed. +// Users don't like it, so we're going with a preference to use +// invokeLater. +/* + // paint immediately before updating the display group and + // before any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + if ( object() instanceof javax.swing.JComponent ) + { + javax.swing.JComponent component = (javax.swing.JComponent)object(); + component.paintImmediately( component.getBounds() ); + } + selectFromSelectionModel(); +*/ + +// NOTE: This approach uses invoke later to cause the update of +// the display group (which could be lengthly if that in turn +// causes other things to update) to happen after the tree repaints. +// Users like this because it "feels faster", but developers have +// to remember that if they listen to tree selection events they +// will have to do a similar invoke later if they check the display +// group. + + Runnable select = new Runnable() + { + public void run() + { + selectFromSelectionModel(); + } + }; + + if ( selectionPaintedImmediately ) + { + if ( object() instanceof java.awt.Component ) + { + ((java.awt.Component)object()).repaint(); + } + EventQueue.invokeLater( select ); + } + else + { + select.run(); + } + } + + /** + * Overridden to check whether the node is visible + * in the tree on screen. Offscreen in a scrollpane + * does not count. + */ + public boolean isVisible(Object node) + { + JTree tree = (JTree) object(); + TreePath path = ((DisplayGroupNode)node).treePath(); + if ( tree.isVisible( path ) ) + { + Rectangle rowRect = tree.getPathBounds( path ); + if ( rowRect != null ) + { + Rectangle visible = tree.getVisibleRect(); + if ( visible != null ) + { +//System.out.println( "isVisible: intersects: " + visible.intersects( rowRect ) ); + return visible.intersects( rowRect ); + } + } + } +//System.out.println( "isVisible: false" ); + return false; + } + + /** + * Fires a tree nodes changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesChanged(final Object source, + final Object[] path, + final int[] childIndices, + final Object[] children) + { + if ( !isExpanding ) + { + for ( int i = 0; i < children.length; i++ ) + { + nodeQueue.add( children[i] ); + } + if ( !isRunning ) + { + isRunning = true; + EventQueue.invokeLater( this ); + } + } + } + + /** + * Fires a tree nodes inserted event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesInserted(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + super.fireTreeNodesInserted( source, path, childIndices, children ); + EventQueue.invokeLater( this ); + } + } + + /** + * Fires a tree nodes removed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesRemoved(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + super.fireTreeNodesRemoved( source, path, childIndices, children ); + EventQueue.invokeLater( this ); + } + } + + /** + * Fires a tree structure changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeStructureChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + structureQueue.add( path[path.length-1] ); + if ( !isRunning ) + { + isRunning = true; + EventQueue.invokeLater( this ); + } + } + } + + /** + * Overridden to return all visible rows in the tree. + */ + public NSArray objectsFetchedIntoChildrenGroup() + { + JTree tree = (JTree) object(); + NSMutableArray objectList = new NSMutableArray(); + + int count = tree.getRowCount(); + for ( int i = 0; i < count; i++ ) + { + objectList.add( + ((DisplayGroupNode) tree.getPathForRow( i ).getLastPathComponent()).object() ); + } + +//new net.wotonomy.ui.swing.util.StackTraceInspector( Integer.toString( objectList.size() ) ); + return objectList; + } + + // 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 ( ! component().isEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + + // interface TreeWillExpandListener + + public void treeWillExpand(TreeExpansionEvent event) + throws ExpandVetoException + { + isExpanding = true; + } + + public void treeWillCollapse(TreeExpansionEvent event) + throws ExpandVetoException + { + // do nothing + } + + // interface TreeExpansionListener + + /** + * Updates the children display group, if any. + */ + public void treeExpanded(TreeExpansionEvent event) + { //System.out.println( "treeExpanded: " + event.getPath().getLastPathComponent() ); + isExpanding = false; + if ( childrenDisplayGroup != null ) + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + } + } + + /** + * Updates the children display group, if any. + */ + public void treeCollapsed(TreeExpansionEvent event) + { + if ( childrenDisplayGroup != null ) + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + } + } + + // interface Runnable + + /** + * Fires any queued node changed and structure changed events. + * Typically invoked on a delayed event loop. + */ + public void run() + { + DisplayGroupNode node; + int index; + Iterator i; + + i = nodeQueue.iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + index = ((DisplayGroupNode)node.parentGroup).getIndex( node ); + if ( ( index != -1 ) + && ( node.treePath().getParentPath() != null ) ) + { + super.fireTreeNodesChanged( + node, + node.treePath().getParentPath().getPath(), + new int[] { index }, + new Object[] { node } ); + } + } + nodeQueue.clear(); + + i = structureQueue.iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + super.fireTreeStructureChanged( + node, + node.treePath().getPath(), + null, + null ); + } + structureQueue.clear(); + + isRunning = false; +/* + EventQueue.invokeLater( new Runnable() { public void run() { + ((JTree)object()).treeDidChange(); + ((JTree)object()).getParent().invalidate(); + ((JTree)object()).getParent().validate(); + ((JTree)object()).getParent().update( ((JTree)object()).getGraphics() ); + +// ((JTree)object()).getParent().doLayout(); +// ((JTree)object()).getParent().repaint(); +// ((JTree)object()).repaint(); +// ((JTree)object()).updateUI(); + } } ); +*/ + } +} + +/* + * $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.55 2004/02/05 02:18:50 mpowers + * Super was calling back into this class before init() was called. + * + * Revision 1.54 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.53 2003/06/03 14:49:48 mpowers + * Now correctly calculating isVisible based on the component visible rect. + * Now deferring node changed events to a later event queue to allow repaints + * to happen after all changes have taken effect. + * + * Revision 1.52 2002/05/03 21:41:18 mpowers + * No longer clearing the selection model when updating from display group: + * we now only modify if a change needs to be made. + * No longer listening for selection change during firing of delete events: + * delete events cause JTree's to update their selection model. + * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges() + * must happen after the screen is painted, or the selection is not displayed. + * + * Revision 1.51 2002/04/19 21:18:45 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.49 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.48 2002/04/12 20:36:31 mpowers + * Now distinguishing between changes made on titles group by tree expansion + * versus external changes which should cause us to repopulate root nodes. + * + * Revision 1.47 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.46 2002/03/27 20:44:53 mpowers + * Added isVisible test for node. + * + * Revision 1.45 2002/03/08 23:19:57 mpowers + * Refactoring of DelegatingTreeDataSource to facilitate binding of titles + * and children aspects to the same display group. + * + * Revision 1.44 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.43 2002/03/06 13:04:15 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.42 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.41 2002/03/01 23:42:08 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * Revision 1.40 2002/03/01 20:41:39 mpowers + * Now a focus listener and an expansion listener. + * + * Revision 1.39 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java new file mode 100644 index 0000000..f6c90d0 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java @@ -0,0 +1,331 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 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 javax.swing.JTree; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.TreeTableCellRenderer; + +/** +* TreeColumnAssociation is a TableColumnAssocation +* that works like a TreeAssociation, allowing any +* table to display hierarchical data in a tabular format. +* This class is mainly a convenience for connecting a +* TreeAssociation to a JTree to a TreeTableCellRenderer +* to a TableColumn.<br><br> +* +* Like TableColumnAssociation, you must call setTable() +* to specify the JTable to be used. (The corresponding +* table association will direct all column header sorting +* to the root node of the tree association.) +* +* You may also optionally call setTree() to specify a +* customized JTree to be used. If not specified, a +* slightly customized JTree will be used (see createTree() +* and configureColumn()). +* +* TreeColumnAssociation supports the following bindings, +* just as TableColumnAssociation does: +* <ul> +* <li>value: a property convertable to a string for +* display in the cells of the table column. This +* binding is equivalent to the titles binding of +* TreeAssociation.</li> +* +* <li>editable: a property convertable to a boolean +* that determines the editability of the corresponding +* cells in the column.</li> +* </ul> +* +* TreeColumnAssociation additionally supports the following +* bindings, just as TreeAssociation does, except that the +* value binding is used instead of the titles binding. +* <ul> +* <li>children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* If this aspect is not bound, the tree behaves like a list.</li> +* +* <li>isLeaf: a property of a node value that returns +* a value convertable to a boolean value. +* If the isLeaf aspect is not bound, the tree will force +* nodes to load their children to determine whether they +* are leaf nodes (in effect loading the grandchildren for +* any expanded node). +* </li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeColumnAssociation extends TableColumnAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EditableAspect, ChildrenAspect, IsLeafAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "table" + } ); + + EODisplayGroup childrenDisplayGroup, leafDisplayGroup; + String childrenKey, leafKey; + + TreeModelAssociation treeAssociation; + JTree tree; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. + */ + public TreeColumnAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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; + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ChildrenAspect.equals( anAspect ) ) + { + childrenDisplayGroup = aDisplayGroup; + childrenKey = aKey; + } + if ( IsLeafAspect.equals( anAspect ) ) + { + leafDisplayGroup = aDisplayGroup; + leafKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Overridden to call createTree if necessary, + * call configureColumn, call createTreeAssociation + * if necessary, and then call to super. + */ + public void establishConnection () + { + if ( tree == null ) + { + tree = createTree(); + } + + configureColumn( (TableColumn) object() ); + + if ( treeAssociation == null ) + { + treeAssociation = createTreeAssociation( tree ); + } + + treeAssociation.bindAspect( TitlesAspect, valueDisplayGroup, valueKey ); + if ( childrenKey != null ) + { + treeAssociation.bindAspect( ChildrenAspect, childrenDisplayGroup, childrenKey ); + } + if ( leafKey != null ) + { + treeAssociation.bindAspect( IsLeafAspect, leafDisplayGroup, leafKey ); + } + + // ensure table association's source is tree asssociation's child group + getTableAssociation().bindAspect( + SourceAspect, treeAssociation.childrenDisplayGroup, "" ); + + treeAssociation.establishConnection(); + + table.setRowHeight( tree.getRowHeight() ); + + super.establishConnection(); + + // cause sort ordering to apply to root node of tree + if ( childrenKey != null ) + { + getTableAssociation().sortTarget = + (EODisplayGroup) treeAssociation.getRoot(); + } + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + super.breakConnection(); + treeAssociation.breakConnection(); + + // restore original source display group + getTableAssociation().sortTarget = null; + } + + /** + * Called by establishConnection if setTree was not called. + * This implementation returns a stock JTree. Override + * to provide your own customized JTree. Note that + * TreeTableCellRenderer will further customize this tree + * when configureColumn is called. + */ + protected JTree createTree() + { + return new JTree(); + } + + /** + * Called by establishConnection to create a tree association + * only if no tree association has already been created. + * This implementation returns a stock TreeAssociation. + * Override to return your own customized TreeAssociation. + */ + protected TreeModelAssociation createTreeAssociation( JTree aTree ) + { + return new TreeAssociation( aTree ); + } + + /** + * Called by establishConnection to configure the column + * with a TreeTableCellRenderer using the current JTree. + * Override to further customize the column, or customize + * your column yourself after the call to establishConnection. + */ + protected void configureColumn( TableColumn aColumn ) + { + aColumn.setCellRenderer( new TreeTableCellRenderer( tree ) ); + } + + /** + * Gets the JTree currently used for the column renderer. + * If not specified, returns null. + */ + public JTree getTree() + { + return tree; + } + + /** + * Gets the TreeModelAssociation currently used for the tree. + * If not tree is not specified, returns null. + */ + public TreeModelAssociation getTreeModelAssociation() + { + if ( tree == null ) return null; + if (!( tree.getModel() instanceof TreeModelAssociation )) return null; + return (TreeModelAssociation) tree.getModel(); + } + + /** + * Sets the JTree to be used for the column renderer. + * If not specified, createTree() will be called to create a JTree. + */ + public void setTree( JTree aTree ) + { + tree = aTree; + } + +} + +/* + * $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.9 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.8 2002/05/03 21:31:50 mpowers + * Actually works better without selectionPaintedImmediately. + * + * Revision 1.7 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.6 2002/03/08 23:18:48 mpowers + * Added accessor for tree association. + * + * Revision 1.5 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.4 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.3 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.2 2002/03/04 22:48:22 mpowers + * Now working with table association to sort the root node. + * + * Revision 1.1 2002/03/01 23:42:09 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java new file mode 100644 index 0000000..86bfa69 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java @@ -0,0 +1,1751 @@ +/* +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.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import javax.swing.SwingUtilities; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserverProxy; +import net.wotonomy.control.OrderedDataSource; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TreeModelAssociation binds a JTree or similar component +* that uses a TreeModel to a display group's +* list of displayable objects, each of which may have +* a list of child objects managed by another display +* group, and so on. TreeModelAssociation works exactly +* like a ListAssociation, with the additional capability +* to specify a "Children" aspect, that will allow child +* objects to be retrieved from a parent display group. +* +* <ul> +* +* <li>titles: a property convertable to a string for +* display in the nodes of the tree. The objects in +* the bound display group will be used to populate the +* initial root nodes of the tree (more accurately, +* children of the offscreen root node in the tree).</li> +* +* <li>children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* The data source of the bound display group is replaced +* a data source that populates the display group with +* the selection in the tree component as determined by +* calling fetchObjectsIntoChildrenGroup. +* If this aspect is not bound, the tree behaves like a list. +* <br><br> +* Binding this aspect with a null display group is the same +* as binding it with the titles display group. +* In this configuration the contents of the titles +* display group will be replaced with the selection in the +* tree component, as specified above, replacing the existing +* data source. +* <br><br> +* In that case, the display groups for the nodes in +* the tree will still use the original data source +* for resolving their children key, and programmatically +* setting the contents of the display group will still +* repopulate the root nodes of the tree. +* </li> +* +* <li>isLeaf: a property of a node value that returns +* a value convertable to a boolean value (aside from +* an actual boolean value, zeroes evaluate to true, +* as does any String containing "yes" or "true" or that +* is convertable to a number equal to zero; other values +* evaluate to false). +* <br><br> +* If the isLeaf aspect is not bound, +* the tree must force nodes to load their children to +* determine whether they are leaf nodes (in effect +* loading the grandchildren for any expanded node). +* If bound, child loading is deferred until the node +* is actually expanded. +* <br><br> +* For example, binding this value to a null +* display group and the key "false" will result in a +* deferred-loading tree that works much like Windows +* Explorer's network volume browser - all nodes appear +* with "pluses" until they are expanded. +* <br><br> +* Note that the display group is ignored: the property +* will be applied directly to the object corresponding +* to the node.</li> +* +* </ul> +* +* This class acts as the TreeModel for the controlled +* component: calling yourcomponent.getModel() will +* return this association. The tree model methods on +* this class are public and may be used to affect changes +* on the controlled components.<br><br> +* +* The titles display group's contents are inserted +* into a new display group that acts as the root node. +* After that point, changes in the titles display group +* will cause the tree model to reset itself, creating +* a new display group for the root node. +* <br><br> +* +* If a separate display group is bound to the children +* aspect, it will +* be used to hold the selected objects and their siblings +* and selection will be maintained there, and the titles +* display group selection will not be updated. +* Any editing or detail associations should in that case +* be attached to the children display group, not the titles +* group. <br><br> +* +* Each node in the tree is an EODisplayGroup that +* contains the child objects of the object it represents +* in the tree. These objects can be programmatically +* inserted, updated, or removed using DisplayGroup +* methods. Each node's takes its parent group's +* sortOrderings until a sort ordering is explicitly +* specified - setting a sort ordering to null will resume +* using the parent group's sort ordering.<br><br> +* +* Each node in the tree also implements MutableTreeNode. +* The value that a node represents is the titles property +* value of the object in the parent's displayed objects +* list at the index corresponding to the index of the node. +* Calling toString on a node returns the string representation +* of the titles property value, and setUserObject will update +* that value directly in the corresponding object. +* Moving a node from one parent to another will remove the +* actual object in the parent display group and insert it +* into the destination display group.<br><br> +* +* In short, any nodes obtained from this class' +* implementation of TreeModel may be cast as either +* EODisplayGroup or MutableTreeNode and maybe be +* programmatically manipulated in either manner. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeModelAssociation extends EOAssociation + implements TreeModel, TreeSelectionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ChildrenAspect, IsLeafAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "model" + } ); + + private final static NSSelector getSelectionModel = + new NSSelector( "getSelectionModel" ); + private final static NSSelector setModel = + new NSSelector( "setModel", + new Class[] { TreeModel.class } ); + + EODisplayGroup titlesDisplayGroup, childrenDisplayGroup, leafDisplayGroup; + String titlesKey, childrenKey, leafKey; + DisplayGroupNode rootNode; + Vector listeners; + Object rootLabel; + + TreeSelectionModel selectionModel; + boolean selectionPaintedImmediately; + + boolean insertingChild; + boolean insertingAfter; + + EOObserverProxy recentChangesObserver; + + private boolean pleaseSelectRootNode; + + /** + * Constructor expecting a JTree or any other object + * that has void setModel(TreeModel) and TreeModel getSelectionModel() + * methods. This tree association will be used for the TreeModel. + * The root node will be labeled "Root". <br><br> + * + * As an alternate way to use a TreeModelAssociation, you may pass a + * TreeSelectionModel to the constructor and then manually set your + * component to use this class as its TreeModel. + */ + public TreeModelAssociation ( Object anObject ) + { + super( anObject ); + + titlesDisplayGroup = null; + titlesKey = null; + childrenDisplayGroup = null; + childrenKey = null; + leafDisplayGroup = null; + leafKey = null; + listeners = new Vector(); + + selectionPaintedImmediately = false; + + // after display group nodes process recent changes + recentChangesObserver = new EOObserverProxy( + this, new NSSelector( "processRecentChanges" ), + EODelayedObserver.ObserverPrioritySixth ); + EOObserverCenter.addObserver( recentChangesObserver, this ); + + insertingChild = true; + insertingAfter = true; + + pleaseSelectRootNode = false; + + rootLabel = "Root"; + rootNode = createNode( null, null ); + } + + /** + * Constructor expecting a JTree or similar component + * and specifying a label for the root node. + */ + public TreeModelAssociation( Object anObject, Object aRootLabel ) + { + this( anObject ); + rootLabel = aRootLabel; + rootNode.setUserObject( aRootLabel ); + } + + /** + * Gets the current root label. + */ + public Object rootLabel() + { + return rootLabel; + } + + /** + * Gets the current root label. + */ + public Object getRootLabel() + { + return rootLabel(); + } + + /** + * Sets the root label. + */ + public void setRootLabel( Object aLabel ) + { + rootLabel = aLabel; + } + + /** + * 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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 setModel.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 TitlesAspect; + } + + /** + * 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 ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( TitlesAspect.equals( anAspect ) ) + { + titlesDisplayGroup = aDisplayGroup; + titlesKey = aKey; + } + if ( ChildrenAspect.equals( anAspect ) ) + { + childrenDisplayGroup = aDisplayGroup; + childrenKey = aKey; + } + if ( IsLeafAspect.equals( anAspect ) ) + { + leafDisplayGroup = aDisplayGroup; + leafKey = aKey; + } + if ( childrenDisplayGroup == null ) + { + childrenDisplayGroup = titlesDisplayGroup; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + if ( titlesDisplayGroup == null ) + { + throw new WotonomyException( + "TreeModelAssociation: Titles aspect must be bound" ); + } + + // populate the root node + rootNode = createNode( titlesDisplayGroup, null ); + rootNode.setObjectArray( titlesDisplayGroup.displayedObjects() ); + rootNode.setSortOrderings( titlesDisplayGroup.sortOrderings() ); + + EODataSource dataSource = childrenDisplayGroup.dataSource(); + if ( dataSource == null ) dataSource = titlesDisplayGroup.dataSource(); + while ( dataSource instanceof DelegatingTreeDataSource ) + { // unwrap any existing delegating data sources + dataSource = ((DelegatingTreeDataSource)dataSource).delegateDataSource; + } + // create a new delegating data source + childrenDisplayGroup.setDataSource( + new DelegatingTreeDataSource( this, dataSource ) ); + + //TODO: find out why omitting this line causes weird selection behavior + childrenDisplayGroup.setSortOrderings( new NSArray() ); + + // check for alternate usage + if ( object() instanceof TreeSelectionModel ) + { + selectionModel = (TreeSelectionModel) object(); + } + else // use specified object + { + try + { + setModel.invoke( object(), new Object[] { this } ); + selectionModel = (TreeSelectionModel) + getSelectionModel.invoke( object(), new Object[] {} ); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + addAsListener(); + super.establishConnection(); +/* + fireRootStructureChanged(); + +// titlesGroupChanged = true; +// subjectChanged(); + + // update the children group + removeAsListener(); + childrenDisplayGroup.fetch(); + addAsListener(); + + // update selection + selectFromDisplayGroup( titlesDisplayGroup ); +*/ + } + + protected void fireRootStructureChanged() + { + int count = rootNode.displayedObjects().count(); + int[] childIndices = new int[ count ]; + Object[] children = new Object[ count ]; + for ( int i = 0; i < count; i++ ) + { + childIndices[i] = i; + children[i] = rootNode.getChildNodeAt( i ); + } + + // must fire a tree structure changed with children, + // otherwise the tree gets weird selection behavior + fireTreeStructureChanged( this, new Object[] { rootNode }, + childIndices, children ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + if ( childrenDisplayGroup != null ) + { + if ( childrenDisplayGroup.dataSource() instanceof DelegatingTreeDataSource ) + { + if ( titlesDisplayGroup == childrenDisplayGroup ) + { + titlesDisplayGroup.setDataSource( ((DelegatingTreeDataSource) + childrenDisplayGroup.dataSource()).delegateDataSource ); + } + else + { + childrenDisplayGroup.setDataSource( null ); + } + } + } + + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { + isListening = true; + selectionModel.addTreeSelectionListener( this ); + } + + protected void removeAsListener() + { + isListening = false; + selectionModel.removeTreeSelectionListener( this ); + } + + protected boolean isListening = false; + private boolean pleaseIgnore = false; + protected boolean titlesGroupChanged = false; + protected boolean childrenGroupChanged = false; + + /** + * Overridden to better discriminate what is changed. + */ + public void objectWillChange( Object anObject ) + { + if ( ! isListening ) return; + + if ( anObject == titlesDisplayGroup ) + { + titlesGroupChanged = true; + } + if ( anObject == childrenDisplayGroup ) + { + childrenGroupChanged = true; + if ( childrenDisplayGroup.qualifier() != null ) + { + if ( ( rootNode.qualifier() == null ) || + ! childrenDisplayGroup.qualifier().equals( rootNode.qualifier() ) ) + { + // quietly move qualifier from children group to root node + rootNode.setQualifier( childrenDisplayGroup.qualifier() ); + childrenDisplayGroup.setQualifier( null ); + rootNode.updateDisplayedObjects(); + } + } + } + super.objectWillChange( anObject ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + // titles aspect + if ( titlesGroupChanged ) + { + if ( titlesDisplayGroup.contentsChanged() ) + { + NSArray displayedObjects = titlesDisplayGroup.displayedObjects(); + NSArray childrenObjects; + if ( titlesDisplayGroup != childrenDisplayGroup + || displayedObjects.count() + != (childrenObjects = objectsFetchedIntoChildrenGroup()).count() + || ! displayedObjects.containsAll( childrenObjects ) ) + { + populateFromDisplayGroup( displayedObjects ); + } + } + } + + if ( childrenDisplayGroup.selectionChanged() && !childrenDisplayGroup.contentsChanged() ) + { + selectFromDisplayGroup( childrenDisplayGroup ); + } + + titlesGroupChanged = false; + childrenGroupChanged = false; + } + + /** + * Called by subjectChanged() in response to an external change in the titles display group. + */ + void populateFromDisplayGroup( List displayedObjects ) + { + // trigger processRecentChanges + willChange(); + + // workaround: see below + int previousCount = rootNode.previouslyDisplayedObjects.length; + + // update the root node + rootNode.setObjectArray( displayedObjects ); + + //FIXME: workaround for what appears to be a bug in JTree: + // if root node is not visible and has no children, insert events are ignored + if ( previousCount == 0 ) + { + fireRootStructureChanged(); + } + } + + /** + * Package access so DisplayGroupNode can replace the selection after an update. + */ + void selectFromDisplayGroup( EODisplayGroup aDisplayGroup ) + { // System.out.println( "selectFromDisplayGroup: " + aDisplayGroup.selectedObjects() ); + + removeAsListener(); + + TreePath[] paths = selectionModel.getSelectionPaths(); + NSArray selectedObjects = aDisplayGroup.selectedObjects(); + + // assemble current selection list + List treeSelection = new LinkedList(); + if ( paths != null ) + { + for ( int i = 0; i < paths.length; i ++ ) + { + treeSelection.add( + ((DisplayGroupNode)paths[i].getLastPathComponent()).getUserObject() ); + } + } + + if ( ! ( selectedObjects.size() == treeSelection.size() + && treeSelection.containsAll( selectedObjects ) ) ) + { + selectionModel.clearSelection(); + + // workaround to select root node from valueChanged() + if ( pleaseSelectRootNode ) + { + selectionModel.addSelectionPath( new TreePath( this.getRoot() ) ); + pleaseSelectRootNode = false; + } + + //FIXME: display group is assumed to have only one instance of each object + for ( int i = 0; i < selectedObjects.count(); i++ ) + { + //FIXME: selects only the first instance for now + //selectionModel.addSelectionPaths( + // getPathsForObject( + // selectedObjects.objectAtIndex( i ) ) ); + selectionModel.addSelectionPath( + getPathForObject( + selectedObjects.objectAtIndex( i ) ) ); + } + } + + addAsListener(); + } + + /** + * Returns the first node found that represents the + * specified object, or null if not found. + * This implementation simply calls getPathForObject. + */ + public Object getNodeForObject( Object anObject ) + { + TreePath result = getPathForObject( anObject ); + if ( result != null ) + { + return result.getLastPathComponent(); + } + return null; + } + + /** + * Returns the object represented by the specified node + * which must be a display group node from this tree. + */ + public Object getObjectForNode( Object aNode ) + { + if ( aNode instanceof DisplayGroupNode ) + { + return ((DisplayGroupNode)aNode).getUserObject(); + } + + // not a display group node + throw new WotonomyException( + "Not a display group node: " + aNode ); + } + + /** + * Returns the tree path for the specified node, + * which must be a display group node from this tree. + */ + public TreePath getPathForNode( Object aNode ) + { + if ( aNode instanceof DisplayGroupNode ) + { + return ((DisplayGroupNode)aNode).treePath(); + } + + // not a display group node + throw new WotonomyException( + "Not a display group node: " + aNode ); + } + + /** + * Returns the first tree path for the node that represents + * the specified object, or null if the object does not exist in this tree. + * This implementation does a breadth-first search of the tree + * for the object, looking only at nodes that have been loaded. + * This means that if the object does not exist in the tree, + * the entire tree must be traversed. + */ + public TreePath getPathForObject( Object anObject ) + { + return getPathForObjectInPath( anObject, new TreePath( this.getRoot() ) ); + } + + /** + * Returns the tree path for the node that represents + * the specified object, + * or null if the object does not exist in this tree. + * This implementation does a breadth-first search of the tree + * for the object, looking only at nodes that have been loaded. + * This means that the entire tree is traversed. + */ + public TreePath[] getPathsForObject( Object anObject ) + { + return getPathsForObjectInPath( anObject, new TreePath( this.getRoot() ) ); + } + + /** + * A breadth-first search of the tree starting + * at the specified tree path, comparing by reference. + * Returns immediately with the first match. + */ + private TreePath getPathForObjectInPath( Object anObject, TreePath aPath ) + { + LinkedList queue = new LinkedList(); + + // add the specified path + queue.addLast( aPath ); + + return processQueue( anObject, queue, null ); + } + + /** + * A breadth-first search of the tree starting + * at the specified tree path, comparing by reference. + * The entire branch is searched before returning + * an array of all matches. + */ + private TreePath[] getPathsForObjectInPath( Object anObject, TreePath aPath ) + { + LinkedList queue = new LinkedList(); + + // add the specified path + queue.addLast( aPath ); + + List result = new LinkedList(); + processQueue( anObject, queue, result ); + TreePath[] paths = new TreePath[ result.size() ]; + for ( int i = 0; i < paths.length; i++ ) + { + paths[i] = (TreePath) result.get(i); + } + return paths; + } + + /** + * Processes the specified queue, appending results to aResult if it exists, + * or returning immediately with a TreePath is aResult is null. + */ + private TreePath processQueue( Object anObject, LinkedList aQueue, List aResult ) + { + TreePath path; + while ( ! aQueue.isEmpty() ) + { + path = (TreePath) aQueue.removeFirst(); + path = checkNode( anObject, path, aQueue ); + if ( path != null ) + { + if ( aResult != null ) + { + aResult.add( path ); + } + else + { + return path; + } + } + } + return null; + } + + /** + * Compares the specified object by reference each of the children of + * the node at the end of the specified tree path, adding nodes that + * do not match but have fetched object to the end of the specified queue. + * Returns the path of the first child node that matches the specified object, + * or null if no match was found. + */ + private TreePath checkNode( Object anObject, TreePath aPath, LinkedList aQueue ) + { + TreePath result = null; + Object child; + Object parent = aPath.getLastPathComponent(); + int count = getChildCount( parent ); + + for ( int i = 0; i < count; i++ ) + { + child = getChild( parent, i ); + + // add to queue if node has fetched children + if ( ((DisplayGroupNode)child).isFetched ) + { + aQueue.addLast( aPath.pathByAddingChild( child ) ); + } + + // compares by reference + if ( ((DisplayGroupNode)child).object() == anObject ) + { + // assumes same object cannot be in display group twice + result = aPath.pathByAddingChild( child ); +//System.out.println( "TRUE: " + ((DisplayGroupNode)child).object() + " == " + anObject ); + } +// else +// { +//System.out.println( ((DisplayGroupNode)child).object() + " != " + anObject ); +// } + } + return result; + } + + // interface TreeSelectionListener + + public void valueChanged(TreeSelectionEvent e) + { + if ( ! isListening ) return; + selectFromSelectionModel(); + } + + /** + * Determines whether the selection should be painted + * immediately after the user clicks and therefore + * before the children display group is updated. + * When the children group is bound to many associations + * or is bound to a master-detail association, updating + * the display group can take a perceptibly long time. + * This property defaults to false. + * @see #setSelectionPaintedImmediately + */ + public boolean isSelectionPaintedImmediately() + { + return selectionPaintedImmediately; + } + + /** + * Sets whether the selection should be painted immediately. + * Setting this property to true will let the tree paint + * first before the display group is updated. + * This means that any tree selection listers will + * also be notified before the display group is updated + * and will have to invokeLater if they want to see the + * updated display group. + */ + public void setSelectionPaintedImmediately( boolean isImmediate ) + { + selectionPaintedImmediately = isImmediate; + } + + /** + * Package access so DisplayGroupNode can replace the selection. + * Returns the display group containing the current selection: titles or children. + */ + EODisplayGroup selectFromSelectionModel() + { // System.out.print( "selectFromSelectionModel: " ); + removeAsListener(); + DisplayGroupNode node; + TreePath parentPath; + TreePath[] selectedPaths = selectionModel.getSelectionPaths(); + NSMutableArray selectionList = new NSMutableArray(); + if ( selectedPaths != null ) + { + for ( int i = 0; i < selectedPaths.length; i++ ) + { + // root node is zero - ignore root node + if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) ) + { + // select root in selectFromDisplayGroup() + pleaseSelectRootNode = true; + } + else + { + node = (DisplayGroupNode) + selectedPaths[i].getLastPathComponent(); + Object o = node.object(); + + if ( selectionList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + selectionList.addObject( o ); + } + } + } + } + childrenDisplayGroup.fetch(); //note that we're not currently listening for changes + if ( ! childrenDisplayGroup.selectObjectsIdenticalTo( selectionList ) ) + { + addAsListener(); // because we don't have a listener stack + selectFromDisplayGroup( childrenDisplayGroup ); + removeAsListener(); + } + addAsListener(); + return childrenDisplayGroup; // titles is now children if children not explicitly set + } + + // interface TreeModel + + public Object getRoot() + { + return rootNode; + } + + public Object getChild(Object parent, int index) + { + // interestingly, this gets called by + // BasicTreeUI.paintVerticalPartOfLeg for + // the last child of each expanded tree node. + Object result = ((DisplayGroupNode)parent).getChildNodeAt( index ); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getChildNodeAt( index ); + } + + public int getChildCount(Object parent) + { + int result = ((DisplayGroupNode)parent).getChildCount(); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getChildCount(); + } + + public boolean isLeaf(Object node) + { + boolean result = ((DisplayGroupNode)node).isLeaf(); +//((DisplayGroupNode)node).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)node).isLeaf(); + } + + /** + * Returns whether this node is visible in the UI. + * This implementation returns true. + * <br><br> + * Subclasses should return false if they can + * determine that the node is not displayed or + * expanded or otherwise visible. Non-visible + * nodes will fetch only when they are shown. + */ + public boolean isVisible(Object node) + { + return true; + } + + public void valueForPathChanged(TreePath path, Object newValue) + { + ((DisplayGroupNode)path.getLastPathComponent()).setUserObject( newValue ); + } + + public int getIndexOfChild(Object parent, Object child) + { + int result = ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child ); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child ); + } + + public void addTreeModelListener(TreeModelListener aListener) + { + listeners.add( aListener ); + } + public void removeTreeModelListener(TreeModelListener aListener) + { + listeners.remove( aListener ); + } + + /** + * Fires a tree nodes changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesChanged: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + ((TreeModelListener)it.nextElement()).treeNodesChanged( event ); + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesChanged: caught: " + exc ); + System.out.println( "Source:" + source ); + System.out.println( "Path:" ); + for ( int i = 0; i < path.length; i++ ) + { + System.out.print( path[i] + "-" ); + } + System.out.println(); + System.out.println( "Indices:" ); + for ( int i = 0; i < childIndices.length; i++ ) + { + System.out.print( childIndices[i] + "-" ); + } + System.out.println(); + System.out.println( "Children:" ); + for ( int i = 0; i < children.length; i++ ) + { + System.out.print( children[i] + "-" ); + } + System.out.println(); + exc.printStackTrace(); + } + } + } + + /** + * Fires a tree nodes inserted event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesInserted(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesInserted: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + ((TreeModelListener)it.nextElement()).treeNodesInserted( event ); + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesInserted: caught: " + exc ); + } + } + } + + /** + * Fires a tree nodes removed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesRemoved(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesRemoved: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + //NOTE: removing nodes causes tree to fire selection change event + // which confuses us if we're rearranging nodes (when sorting, for example). + boolean wasListening = isListening; + if ( wasListening ) isListening = false; + ((TreeModelListener)it.nextElement()).treeNodesRemoved( event ); + if ( wasListening ) isListening = true; + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesRemoved: caught: " + exc ); + } + } + } + + /** + * Fires a tree structure changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeStructureChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireStructureChanged: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + ((TreeModelListener)it.nextElement()).treeStructureChanged( event ); + } + } + + /** + * Creates and returns a new display group node. + */ + public DisplayGroupNode createNode( EODisplayGroup aParentGroup, Object anObject ) + { + return new MutableDisplayGroupNode( this, aParentGroup, anObject ); + } + + /** + * Gets whether new objects programmatically inserted into the children + * display group should be inserted as a child of the first selected node. + * If false, new objects are inserted as siblings of the first selected node. + * Default value is true. + */ + public boolean isInsertingChild() + { + return insertingChild; + } + + /** + * Sets whether new objects programmatically inserted into the children + * display group should be inserted as a child of the first selected node. + * If false, new objects are inserted as siblings of the first selected node. + * Default value is true. + */ + public void setInsertingChild( boolean asChild ) + { + insertingChild = asChild; + } + + /** + * Determines where new objects programmatically inserted into the children + * display group should be inserted, based on the value of insertingChild. + * If insertingChild, isInsertingAfter causes objects to be inserted at + * the end of the selected node's child list; otherwise, objects are inserted + * at the beginning of the list. + * If inserting as a sibling, isInsertingAfter causes objects to be inserted + * before the selected node in the selected node's parent's child list; + * otherwise, objects are inserted after the selected node in the child list. + * Default value is true. + */ + public boolean isInsertingAfter() + { + return insertingAfter; + } + + /** + * Determines where new objects programmatically inserted into the children + * display group should be inserted, based on the value of insertingChild. + * If insertingChild, isInsertingAfter causes objects to be inserted at + * the end of the selected node's child list; otherwise, objects are inserted + * at the beginning of the list. + * If inserting as a sibling, isInsertingAfter causes objects to be inserted + * before the selected node in the selected node's parent's child list; + * otherwise, objects are inserted after the selected node in the child list. + * Default value is true. + */ + public void setInsertingAfter( boolean after ) + { + insertingAfter = after; + } + + /** + * Called to by the children group's data source when it receives + * an insertObject message, usually after an object has been inserted + * into the children display group. + * Return the object that should be passed to the titles display + * group's data source's implementation of insertObject, or return + * null to prevent that method from being called. <br><br> + * This implementation inserts the specified object into the tree + * as determined by calling isInsertingChild and isInsertingAfter, + * then returns the unmodified object. If there's no selection, or + * no selection model, the root node is assumed to be selected. + * And if the root node is selected, the new node will obviously be + * inserted as a child. Override to customize. + */ + protected Object objectInsertedIntoChildrenGroup( Object anObject ) + { + // determine selection + DisplayGroupNode selectedNode = (DisplayGroupNode) getRoot(); + if ( selectionModel != null ) + { + // get selected path + TreePath path = selectionModel.getSelectionPath(); + + // get selected node + if ( path != null ) + { + selectedNode = (DisplayGroupNode) path.getLastPathComponent(); + } + } + // determine location of insertion + int index = 0; + if ( ( isInsertingChild() ) || ( selectedNode == getRoot() ) ) + { + if ( isInsertingAfter() ) + { + index = selectedNode.getChildCount(); + } + } + else // inserting as sibling + { + DisplayGroupNode parentNode = selectedNode.getParentGroup(); + index = parentNode.getIndex( selectedNode ); + if ( isInsertingAfter() ) + { + index++; + } + selectedNode = parentNode; + } + + // insert and return + selectedNode.insertObjectAtIndex( anObject, index ); + return anObject; + } + + /** + * Called to by the children group's data source when it receives + * a deleteObject message, usually after an object has been deleted + * from the children display group. + * Return the object that should be passed to the titles display + * group's data source's implementation of deleteObject, or return + * null to prevent that method from being called. <br><br> + * This implementation deletes all instances of the selected object + * from the tree nodes that are currently loaded, and returns the + * unmodified object. Override to customize. + */ + protected Object objectDeletedFromChildrenGroup( Object anObject ) + { + TreePath[] paths = getPathsForObject( anObject ); + if ( paths != null ) + { + for ( int i = 0; i < paths.length; i++ ) + { + ((DisplayGroupNode)paths[i].getLastPathComponent()).removeFromParent(); + } + } + return anObject; + } + + /** + * Called to by the children group's data source to populate it + * with all selected nodes and their siblings. To customize, + * override this method, or specify a different data source for + * the children display group. + */ + protected NSArray objectsFetchedIntoChildrenGroup() + { + DisplayGroupNode node; + TreePath parentPath; + TreePath[] selectedPaths = selectionModel.getSelectionPaths(); + NSMutableArray objectList = new NSMutableArray(); + if ( selectedPaths != null ) + { + for ( int i = 0; i < selectedPaths.length; i++ ) + { + // root node is zero - ignore root node + if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) ) + { + // select root in selectFromDisplayGroup() + pleaseSelectRootNode = true; + } + else + { + node = (DisplayGroupNode) + selectedPaths[i].getLastPathComponent(); + Object o = node.object(); + + // add all children of parent to object list - includes self + if ( node.parentGroup != null ) + { + Enumeration e = + node.parentGroup.displayedObjects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + // add only if not already in list + o = e.nextElement(); + if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + objectList.addObject( o ); + } + } + } + else // no parent node - add the node by itself + { + // add only if not already in list + if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + objectList.addObject( o ); + } + } + } + } + } + + // if no selection + if ( objectList.size() == 0 ) + { + // populate with children of root + objectList.addAll( rootNode.displayedObjects() ); + } + return objectList; + } + + /** + * Queues processRecentChanges to be run in the event queue. + */ + private void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + /** + * Tells the children display group to refetch, so that it reflects + * any changes that were made in the node tree, + * and then updates the selection in the selection model. + * Triggered in response to willChange(). + */ + public void processRecentChanges() + { + Runnable update = new Runnable() + { + public void run() + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + selectFromDisplayGroup( childrenDisplayGroup ); + } + }; + if ( isListening ) + { + if ( selectionPaintedImmediately ) + { + // if painting selection immediately, run even later + // so that AWT's repaint event fires before we do. + SwingUtilities.invokeLater( update ); + } + else + { + // otherwise run now + update.run(); + } + } + } + + /** + * Delegates most behaviors to the specified data source, + * except fetchObjects, which calls fetchObjectsIntoChildrenGroup + * on the tree model association. If delegate is null, + * calls are passed to the superclass which is a PropertyDataSource. + */ + static class DelegatingTreeDataSource extends PropertyDataSource + { + TreeModelAssociation parentAssociation; + EODataSource delegateDataSource; + + public DelegatingTreeDataSource( + TreeModelAssociation aTreeModelAssociation, EODataSource aDataSource ) + { + parentAssociation = aTreeModelAssociation; + delegateDataSource = aDataSource; + } + + /** + * Calls to delegateDataSource if it exists, otherwise + * calls to super. + */ + public Object createObject() + { + if ( delegateDataSource != null ) + { + return delegateDataSource.createObject(); + } + return super.createObject(); + } + + /** + * Calls objectInsertedIntoChildrenGroup, and if not null + * calls to delegateDataSource.insertObject if it exists, + * and super.insertObjectAtIndex if not. + */ + public void insertObjectAtIndex( Object anObject, int anIndex ) + { + anObject = + parentAssociation.objectInsertedIntoChildrenGroup( + anObject ); + if ( anObject != null ) + { + if ( delegateDataSource != null ) + { + if ( delegateDataSource instanceof OrderedDataSource ) + { + ((OrderedDataSource)delegateDataSource).insertObjectAtIndex( anObject, anIndex ); + } + else + { + delegateDataSource.insertObject( anObject ); + } + } + else + { + super.insertObjectAtIndex( anObject, anIndex ); + } + } + } + + /** + * Calls objectDeletedIntoChildrenGroup, and if not null + * calls to delegateDataSource if it exists. + */ + public void deleteObject( Object anObject ) + { + anObject = + parentAssociation.objectDeletedFromChildrenGroup( + anObject ); + if ( anObject != null ) + { + if ( delegateDataSource != null ) + { + delegateDataSource.deleteObject( anObject ); + } + super.deleteObject( anObject ); + } + } + + /** + * Overridden to return the delegate's editing context, + * the titles display group's editing context, + * and failing that calling to super. + */ + public EOEditingContext editingContext () + { + EOEditingContext result = null; + if ( delegateDataSource != null ) + { + result = delegateDataSource.editingContext(); + } + if ( result == null ) + { + EODataSource parentDataSource = + parentAssociation.titlesDisplayGroup.dataSource(); + if ( parentDataSource != this && parentDataSource != null ) + { + result = parentAssociation.titlesDisplayGroup. + dataSource().editingContext(); + } + } + if ( result == null ) + { + result = super.editingContext(); + } + return result; + } + + /** + * Returns a List containing the objects in this + * data source. + */ + public NSArray fetchObjects () + { + // if titles group is doing double-duty as children group + if ( parentAssociation.titlesDisplayGroup == parentAssociation.childrenDisplayGroup ) + { + // if we're not initiating this fetch + if ( parentAssociation.isListening ) + { + // need to call to delegate to see if we should update values + if ( delegateDataSource != null ) + { +// System.out.println( "fetching from delegate (slow!)" ); + NSArray result = delegateDataSource.fetchObjects(); + NSArray rootObjects = parentAssociation.rootNode.displayedObjects(); + // if titles data source has different objects, return them + if ( rootObjects.count() != result.count() + || ! rootObjects.containsAll( result ) ) + { + // this will force the root node to repopulate in subjectChanged() +//System.out.println( "fetchObjects: data source" ); + return result; + } + } + } + } + // otherwise: just repopulate the titles group +//System.out.println( "fetchObjects: objectsFetchedIntoChildrenGroup" ); + return parentAssociation.objectsFetchedIntoChildrenGroup(); + } + + /** + * Returns a data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * @see #qualifyWithRelationshipKey + */ + public EODataSource + dataSourceQualifiedByKey ( String aKey ) + { + if ( delegateDataSource != null ) + { + return delegateDataSource.dataSourceQualifiedByKey( aKey ); + } + return null; + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public void + qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + if ( delegateDataSource != null ) + { + delegateDataSource.qualifyWithRelationshipKey( aKey, anObject ); + } + } + + /** + * Returns the value from the delegateDataSource, if it exists. + * Otherwise calls super. + */ + public EOClassDescription classDescriptionForObjects() + { + if ( delegateDataSource != null ) + { + return delegateDataSource.classDescriptionForObjects(); + } + return super.classDescriptionForObjects(); + } + + } + +} + +/* + * $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.20 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.19 2002/05/03 21:41:18 mpowers + * No longer clearing the selection model when updating from display group: + * we now only modify if a change needs to be made. + * No longer listening for selection change during firing of delete events: + * delete events cause JTree's to update their selection model. + * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges() + * must happen after the screen is painted, or the selection is not displayed. + * + * Revision 1.18 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.17 2002/04/19 21:18:46 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.16 2002/04/18 20:36:11 mpowers + * TreeModelAssociation now populates children group before selected objects. + * Got rid of the forceOnSync workaround for cancelled selection change. + * + * Revision 1.15 2002/04/15 21:52:50 mpowers + * Tightening up TreeModelAssociation and DisplayGroupNode. + * Now only firing root structure changed once. + * Now disposing of root's children. + * Better event coalescing. + * + * Revision 1.14 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.11 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.10 2002/04/03 20:01:24 mpowers + * Removed printlns. + * + * Revision 1.8 2002/03/11 03:16:28 mpowers + * Better handling of change events; coalescing changes to children group. + * + * Revision 1.7 2002/03/08 23:19:57 mpowers + * Refactoring of DelegatingTreeDataSource to facilitate binding of titles + * and children aspects to the same display group. + * + * Revision 1.6 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.5 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.4 2002/03/04 22:47:48 mpowers + * Fixed sort ordering for titles group. Optimization for delegate selection. + * + * Revision 1.3 2002/03/04 12:28:47 mpowers + * Revised case where children and titles are bound to same display group. + * + * Revision 1.2 2002/03/01 23:42:09 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * Revision 1.1 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.38 2002/02/18 03:46:08 mpowers + * Implemented TreeTableCellRenderer. + * + * Revision 1.37 2002/02/13 21:20:15 mpowers + * Updated comments. + * + * Revision 1.36 2001/11/21 15:13:25 mpowers + * Better repainting for selectionPaintedImmediately. + * Better handling for selection with multiple instances of the same + * object in the tree (from yjcheung). + * + * Revision 1.35 2001/11/20 19:13:51 mpowers + * Finished implementation of children group's specialized data source. + * + * Revision 1.34 2001/11/19 16:30:37 mpowers + * Tree repaint strategy is now a preference: selectionPaintedImmediately. + * + * Revision 1.33 2001/11/15 17:56:41 mpowers + * Initial implementation of data source for the children display group. + * + * Revision 1.32 2001/11/14 00:05:54 mpowers + * Eliminated the run later in favor of repainting the component immediately. + * This makes things more predictable for users of the association that + * want to listen to mouse or selection events on the tree. + * + * Revision 1.31 2001/11/02 20:43:15 mpowers + * Fixes for delegate's shouldChangeSelection veto (from yjcheung). + * + * Revision 1.30 2001/10/29 20:42:56 mpowers + * On selection change, repainting tree before notifying display group; + * using NSRunLoop instead of SwingUtilities. + * + * Revision 1.29 2001/10/12 20:12:53 mpowers + * Better handling of selection change vetoing when changing selection + * to a node that is not the sibling of the originally selected node. + * + * Revision 1.28 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.27 2001/09/10 14:10:03 mpowers + * Tree now handles multiple instances of the same object. + * + * Revision 1.26 2001/07/18 13:03:32 mpowers + * TreeNodes now refetch only on demand. Previously, once a node had + * been fetched, it was always refetched after an invalidate, even if + * the node was not being displayed. + * + * Revision 1.25 2001/05/14 15:25:35 mpowers + * No longer copying titles group's data source to children group. + * + * Revision 1.24 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.23 2001/05/01 00:52:32 mpowers + * Implemented breadth-first traversal of tree for node. + * + * Revision 1.22 2001/04/26 01:15:19 mpowers + * Major clean-up of DisplayGroupNode: fitter, happier, more productive. + * + * Revision 1.21 2001/04/22 23:13:35 mpowers + * Minor bug. + * + * Revision 1.20 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.19 2001/04/21 23:06:33 mpowers + * A major revisiting to support the revising of DisplayGroupNode. + * + * Revision 1.18 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.17 2001/03/29 21:35:08 mpowers + * Now handling circular references in the graph. + * + * Revision 1.16 2001/03/22 21:25:42 mpowers + * Fixed some nasty issues with jtree's internal state and array bounds. + * + * Revision 1.15 2001/03/19 21:37:58 mpowers + * Improved refresh of titles display group. + * Fixed dangling selection problem after refresh. + * + * Revision 1.14 2001/03/09 22:08:57 mpowers + * Trying to handle the dangling reference problem after an update. + * + * Revision 1.13 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.12 2001/01/25 02:16:25 mpowers + * TreeModelAssociation now returns DisplayGroupNode.getUserObject. + * + * Revision 1.11 2001/01/24 18:14:40 mpowers + * Fixed problem with leaving children aspect unspecified. + * + * Revision 1.10 2001/01/24 17:49:15 mpowers + * Added getObjectForNode and getNodeForObject convenience methods. + * + * Revision 1.9 2001/01/24 17:44:11 mpowers + * Renamed getPathForNode to getPathForObject to be more precise. + * And created a new getPathForNode method. + * + * Revision 1.8 2001/01/24 17:20:29 mpowers + * Children display group now holds siblings of selected objects + * in addition to the selected objects. + * + * Revision 1.5 2001/01/19 23:21:15 mpowers + * Fine tuning events broadcast from TreeModelAssociation. + * + * Revision 1.4 2001/01/18 21:27:29 mpowers + * Major rework of TreeModelAssociation. + * + * Revision 1.2 2001/01/11 20:29:19 mpowers + * Expanded access to tree event firing methods. + * + * Revision 1.1.1.1 2000/12/21 15:49:18 mpowers + * Contributing wotonomy. + * + * Revision 1.20 2000/12/20 16:25:42 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java new file mode 100644 index 0000000..1fef587 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java @@ -0,0 +1,74 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager; +import java.io.Serializable; + +/** + * AbsoluteLayout specifies that all components in the + * container will be placed according to their size + * and their location relative to the container's origin. <br><br> + * + * You can achieve the same effect by setting a container's + * layout manager to null, but this class allows you to subclass + * it if you need specific control or functionality. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public class AbsoluteLayout implements LayoutManager, Serializable +{ + public void addLayoutComponent(String name, + Component comp) + { + } + + public void removeLayoutComponent(Component comp) + { + } + + public Dimension preferredLayoutSize(Container parent) + { + return minimumLayoutSize( parent ); + } + + public Dimension minimumLayoutSize(Container parent) + { + int width = 0; + int height = 0; + + Component[] c = parent.getComponents(); + for ( int i = 0; i < c.length; i++ ) + { + width = Math.max( width, c[i].getLocation().x + c[i].getBounds().width ); + height = Math.max( height, c[i].getLocation().y + c[i].getBounds().height ); + } + + return new Dimension( width, height ); + } + + public void layoutContainer(Container parent) + { + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java new file mode 100644 index 0000000..c36f5e2 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java @@ -0,0 +1,335 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +/** +* AlphaTextField is a "smart" text field that restricts the user's input. The +* input can be restricted to alphabetic, alphanumeric, or all characters. The +* maximum number of characters can also be limited. +* The defaults for this component is alphabetic only string of unlimited length. +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class AlphaTextField extends SmartTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Sets the input to alphabetic characters only. The characters "a-z" and "A-Z" +* are the only valid characters. All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHABETIC = 0; + +/** +* Sets the input to alphanumeric characters only. The characters "a-z", "A-Z" +* and "0-9" are the only valid characters. All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHANUMERIC = 1; + +/** +* Sets the input to alphanumeric characters and a few special characters only. +* The valid characters are "a-z", "A-Z", "0-9", space, "-", "_", "\", and ":". +* This is helpful for file names (with paths) as input strings. +* All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHANUMERIC_PLUS = 2; + +/** +* Sets the input to all characters without restriction. +* @see #getAlphaType() +*/ + public static final int ALL = 3; + + +/******************************* +* DATA MEMBERS +*******************************/ + + // The level of input restrictions, defaults to ALPHABETIC + private int alphaType; + + // The maximum length of the input string, defaults to 0, no maximum + private int stringLength; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* The default constructor of this class. The default string of this text +* field is set to the empty string (""). The maximum length is set to 0, +* which specifies no limit. +*/ + public AlphaTextField() + { + this("", 0); + } + +/** +* Constructor of this class with the initial text of the text field specified. +* The maximum length is set to 0, which specifies no limit. +* @param text Initial text of the text field. +*/ + public AlphaTextField(String text) + { + this(text, 0); + } + +/** +* Constructor of this class with width (in columns) of the text field specified. +* The initial text is set to the empty string (""). The maximum length is set +* to 0, which specifies no limit. +* @param columns The width of the text field in characters. +*/ + public AlphaTextField(int columns) + { + this("", columns); + } + +/** +* Constructor of this class with width (in columns) and initial text of the +* text field specified. The maximum length is set to 0, which specifies no limit. +* @param text Initial text of the text field. +* @param columns The width of the text field in characters. +*/ + public AlphaTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Constructor that allows the user to set the Alpha type of the text field +* and the maximum string length. +* @param anAlphaType The character restriction type. +* @param aLength The maximum number of characters allowed in the string. +*/ + public AlphaTextField(int anAlphaType, int aLength) + { + super( "", 0 ); + setAlphaType( anAlphaType ); + setStringLength( aLength ); + } + +/** +* Gets the current restriction type of this text field. +* @see #ALPHABETIC +* @see #ALPHANUMERIC +* @see #ALPHANUMERIC_PLUS +* @see #ALL +* @return The current restriction type as defined by the constansts of this class. +*/ + public int getAlphaType() + { + return alphaType; + } + +/** +* Sets the restriction type of this text field. +* @see #ALPHABETIC +* @see #ALPHANUMERIC +* @see #ALPHANUMERIC_PLUS +* @see #ALL +* @param newAlphaType The restriction of this text field. +*/ + public void setAlphaType(int newAlphaType) + { + switch (newAlphaType) + { + case ALPHABETIC: + case ALPHANUMERIC: + case ALPHANUMERIC_PLUS: + case ALL: + { + alphaType = newAlphaType; + break; + } + default: + { + alphaType = ALPHABETIC; + break; + } + } + } + +/** +* Sets the maximum string length of this text field. If the length is set to +* zero, then there is no limit. The default string length is zero. Negative +* sizes will set the length to zero. +* @param newStringLength The maximum length of the string that the user can input. +*/ + public void setStringLength(int newStringLength) + { + if (newStringLength < 0) + { + stringLength = 0; + } + else + { + stringLength = newStringLength; + } + } + +/** +* Gets the current length of the maximum string size the user can enter. +* @return The maximum length the string of the text field can be. +*/ + public int getStringLength() + { + return stringLength; + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + + protected boolean isValidCharacter(char aChar) + { + // if its a non-printable character, then its ok + if ((aChar < ' ') || (aChar > '~')) + { + return true; + } + + // can only be a printable character now, check it for validation + return isValidCharacterType(aChar); + } + + protected boolean isValidString(String aString) + { + if (aString.length() > stringLength) + { + return false; + } + + for (int i = 0; i < aString.length(); ++i) + { + if (!(isValidCharacterType(aString.charAt(i)))) + { + return false; + } + } + + return true; + } + + protected void postProcessing() + { + // No need to do anything. + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + + private boolean isValidCharacterType(char aChar) + { + switch (alphaType) + { + case ALPHABETIC: + { + if (!(isValidAlphabeticCharacter(aChar))) + { + return false; + } + break; + } + case ALPHANUMERIC: + { + if (!(isValidAlphanumericCharacter(aChar))) + { + return false; + } + break; + } + case ALPHANUMERIC_PLUS: + { + if (!(isValidAlphanumericPlusCharacter(aChar))) + { + return false; + } + break; + } + case ALL: + { + if (!(isValidAllCharacter(aChar))) + { + return false; + } + break; + } + default: + { + return false; + } + } + + return true; + } + + private boolean isValidAlphabeticCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z'))) + { + return false; + } + return true; + } + + private boolean isValidAlphanumericCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z')) && ((aChar < '0') || (aChar > '9'))) + { + return false; + } + return true; + } + + private boolean isValidAlphanumericPlusCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z')) && ((aChar < '0') || (aChar > '9'))) + { + if ((aChar != ' ') && (aChar != '_') && (aChar != '-') && (aChar != ':') && (aChar != '\\')) + { + return false; + } + } + return true; + } + + private boolean isValidAllCharacter(char aChar) + { + if ((aChar < ' ') || (aChar > '~')) + { + return false; + } + return true; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java new file mode 100644 index 0000000..46d2693 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java @@ -0,0 +1,129 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.UIManager; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that wraps another TableCellRenderer +* and sets the background to the specified color for odd-numbered rows. +* This makes every other row appear to be a different color, +* which helps users distinguish rows of data in densely-packed +* tables. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class AlternatingRowCellRenderer implements TableCellRenderer { + + protected TableCellRenderer wrappedRenderer; + protected Color alternateColor; + + /** + * Default constructor uses a lighter shade of the system control color + * and wraps a DefaultTableCellRenderer. + */ + public AlternatingRowCellRenderer() + { + this( new DefaultTableCellRenderer() ); + } + + /** + * Uses the specified color for the background of the alternating rows, + * and wraps a DefaultTableCellRenderer. + */ + public AlternatingRowCellRenderer( + Color aColor ) + { + this( aColor, new DefaultTableCellRenderer() ); + } + + /** + * Uses the uses a lighter shade of the system control color + * for the background of the alternating rows, + * and wraps the specified TableCellRenderer. + */ + public AlternatingRowCellRenderer( + TableCellRenderer aRenderer ) + { + Color c = UIManager.getColor( "control" ); + c = new Color( // lighten this color just slightly + (int) ( c.getRed() + ( ( 255 - c.getRed() ) / 1.5 ) ), + (int) ( c.getGreen() + ( ( 255 - c.getGreen() ) / 1.5 ) ), + (int) ( c.getBlue() + ( ( 255 - c.getBlue() ) / 1.5 ) ) ); + + alternateColor = c; + wrappedRenderer = aRenderer; + } + + /** + * Uses the specified color for the background of the alternating rows, + * and wraps the specified TableCellRenderer. + */ + public AlternatingRowCellRenderer( + Color aColor, TableCellRenderer aRenderer ) + { + alternateColor = aColor; + wrappedRenderer = aRenderer; + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + Component result = wrappedRenderer.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column ); + if ( ! isSelected ) + { + if ( row % 2 == 0 ) + { + if ( ! result.getBackground().equals( table.getBackground() ) ) + { + result.setBackground( table.getBackground() ); + } + } + else + { + if ( ! result.getBackground().equals( alternateColor ) ) + { + // jdk1.3's default renderer is opaque + if ( result instanceof JComponent ) + { + ((JComponent)result).setOpaque( true ); + } + + result.setBackground( alternateColor ); + } + } + } + return result; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java new file mode 100644 index 0000000..1c438b6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java @@ -0,0 +1,515 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +/** + * BetterFlowLayout works just like FlowLayout, except that + * you can specify a vertical orientation in addition to the + * usual horizontal orientations. You can also specify that + * all the components be sized to the same height and/or width. + * By default, the behavior is identical to FlowLayout. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class BetterFlowLayout extends FlowLayout { + + /** + * This value indicates vertical orientation and + * that each column of components should be top-justified. + */ + public static final int TOP = 32; + + /** + * This value indicates vertical orientation and + * that each column of components should be centered. + */ + public static final int CENTER_VERTICAL = 16; + + /** + * This value indicates vertical orientation and + * that each column of components should be bottom-justified. + */ + public static final int BOTTOM = 8; + + /** + * Tracks orientation. + */ + protected boolean isHorizontal = true; + + /** + * Tracks component sizing of width. + */ + protected boolean isWidthUniform = false; + /** + * Tracks component sizing of height. + */ + protected boolean isHeightUniform = false; + + /** + * Constructs a new Flow Layout with a centered alignment and a + * default 5-unit horizontal and vertical gap. + */ + public BetterFlowLayout() { + this(CENTER, 5, 5); + } + + /** + * Constructs a new Flow Layout with the specified alignment and a + * default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * <code>BetterFlowLayout.LEFT</code>, <code>BetterFlowLayout.RIGHT</code>, + * or <code>BetterFlowLayout.CENTER</code>. + * @param align the alignment value + */ + public BetterFlowLayout(int align) { + this(align, 5, 5); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + * <p> + * The value of the alignment argument must be one of + * <code>BetterFlowLayout.LEFT</code>, <code>BetterFlowLayout.RIGHT</code>, + * or <code>BetterFlowLayout.CENTER</code>. + * @param align the alignment value. + * @param hgap the horizontal gap between components. + * @param vgap the vertical gap between components. + */ + public BetterFlowLayout(int align, int hgap, int vgap) { + setHgap(hgap); + setVgap(vgap); + setAlignment(align); + } + + /** + * Sets whether all components should have the same height. + * @param isUniform the new value. + * @see #isHeightUniform + */ + public void setHeightUniform(boolean isUniform) { + isHeightUniform = isUniform; + } + + /** + * Sets whether all components should have the same width. + * @param isUniform the new value. + * @see #isWidthUniform + */ + public void setWidthUniform(boolean isUniform) { + isWidthUniform = isUniform; + } + + /** + * Determines whether all components will have the same height. + * The uniform height will be the maximum of the preferred heights + * of all the components in the container. + * This value defaults to false. + * @return whether components will have the same height. + */ + public boolean isHeightUniform() { + return isHeightUniform; + } + + /** + * Determines whether all components will have the same width. + * The uniform height will be the maximum of the preferred widths + * of all the components in the container. + * This value defaults to false. + * @return whether components will have the same width. + */ + public boolean isWidthUniform() { + return isWidthUniform; + } + + /** + * Sets the alignment for this layout. + * Possible values for horizontal orientation are <code>LEFT</code>, + * <code>RIGHT</code>, and <code>CENTER</code>. + * Possible values for vertical orientation are <code>TOP</code>, + * <code>BOTTOM</code>, and <code>CENTER_VERTICAL</code>. + * @param align the alignment value. + * @see java.awt.FlowLayout#getAlignment + */ + public void setAlignment(int align) { + if ( ( align == TOP ) || ( align == BOTTOM ) || ( align == CENTER_VERTICAL ) ) + { + isHorizontal = false; + } + else + { + isHorizontal = true; + } + + super.setAlignment( align ); + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSize(Container target) { + if ( isHorizontal ) { + return preferredLayoutSizeHorizontal( target ); + } else { + return preferredLayoutSizeVertical( target ); + } + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSizeHorizontal(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + int nmembers = target.getComponentCount(); + int maxWidth = 0; + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + dim.height = Math.max(dim.height, d.height); + maxWidth = Math.max(maxWidth, d.width); + if (i > 0) { + dim.width += getHgap(); + } + dim.width += d.width; + } + } + if ( isWidthUniform ) + dim.width = ( maxWidth + getHgap() ) * nmembers - getHgap(); + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + getHgap()*2; + dim.height += insets.top + insets.bottom + getVgap()*2; + return dim; + } + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSizeVertical(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + int nmembers = target.getComponentCount(); + int maxHeight = 0; + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + dim.width = Math.max(dim.width, d.width); + maxHeight = Math.max(maxHeight, d.height); + if (i > 0) { + dim.height += getVgap(); + } + dim.height += d.height; + } + } + if ( isHeightUniform ) + dim.height = ( maxHeight + getVgap() ) * nmembers - getVgap(); + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + getHgap()*2; + dim.height += insets.top + insets.bottom + getVgap()*2; + return dim; + } + } + + /** + * Returns the minimum dimensions needed to layout the components + * contained in the specified target container. + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container. + * @see #preferredLayoutSize + * @see java.awt.Container + * @see java.awt.Container#doLayout + */ + public Dimension minimumLayoutSize(Container target) { + // preferred size is also the minimum size + if ( isHorizontal ) { + return preferredLayoutSizeHorizontal( target ); + } else { + return preferredLayoutSizeVertical( target ); + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this <code>BetterFlowLayout</code> object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + public void layoutContainer(Container target) { + if ( isHorizontal ) { + layoutContainerHorizontal( target ); + } else { + layoutContainerVertical( target ); + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this <code>BetterFlowLayout</code> object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + protected void layoutContainerHorizontal(Container target) { + synchronized (target.getTreeLock()) { + Insets insets = target.getInsets(); + int maxwidth = target.getSize().width - (insets.left + insets.right + getHgap()*2); + int nmembers = target.getComponentCount(); + int x = 0, y = insets.top + getVgap(); + int rowh = 0, start = 0; + + boolean ltr = true; // target.getComponentOrientation().isLeftToRight(); + Dimension uniform = getUniformDimension( target ); + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if ( isWidthUniform ) + d.width = uniform.width; + if ( isHeightUniform ) + d.height = uniform.height; + m.setSize(d.width, d.height); + + if ((x == 0) || ((x + d.width) <= maxwidth)) { + if (x > 0) { + x += getHgap(); + } + x += d.width; + rowh = Math.max(rowh, d.height); + } else { + moveComponentsHorizontal(target, insets.left + getHgap(), y, maxwidth - x, rowh, start, i, ltr); + x = d.width; + y += getVgap() + rowh; + rowh = d.height; + start = i; + } + } + } + moveComponentsHorizontal(target, insets.left + getHgap(), y, maxwidth - x, rowh, start, nmembers, ltr); + } + } + + /** + * Centers the elements in the specified row, if there is any slack. + * @param target the component which needs to be moved + * @param x the x coordinate + * @param y the y coordinate + * @param width the width dimensions + * @param height the height dimensions + * @param rowStart the beginning of the row + * @param rowEnd the the ending of the row + */ + private void moveComponentsHorizontal(Container target, int x, int y, int width, int height, + int rowStart, int rowEnd, boolean ltr) { + synchronized (target.getTreeLock()) { + switch (getAlignment()) { + case LEFT: + x += ltr ? 0 : width; + break; + case CENTER: + x += width / 2; + break; + case RIGHT: + x += ltr ? width : 0; + break; +//1.2 case LEADING: +//1.2 break; +//1.2 case TRAILING: +//1.2 x += width; +//1.2 break; + } + for (int i = rowStart ; i < rowEnd ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + if (ltr) { + m.setLocation(x, y + (height - m.getBounds().height) / 2); + } else { + m.setLocation(target.getBounds().width - x - m.getBounds().width, y + (height - m.getBounds().height) / 2); + } + x += m.getBounds().width + getHgap(); + } + } + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this <code>BetterFlowLayout</code> object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + protected void layoutContainerVertical(Container target) { + synchronized (target.getTreeLock()) { + + Insets insets = target.getInsets(); + int maxheight = target.getBounds().height - (insets.top + insets.bottom + getVgap()*2); + int nmembers = target.getComponentCount(); + int y = 0, x = insets.left + getHgap(); + int colw = 0, start = 0; + + Dimension uniform = getUniformDimension( target ); + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if ( isWidthUniform ) + d.width = uniform.width; + if ( isHeightUniform ) + d.height = uniform.height; + m.setSize(d.width, d.height); + + if ((y == 0) || ((y + d.height) <= maxheight)) { + if (y > 0) { + y += getVgap(); + } + y += d.height; + colw = Math.max(colw, d.width); + } else { + moveComponentsVertical(target, x, insets.top + getVgap(), colw, maxheight - y, start, i ); + y = d.height; + x += getHgap() + colw; + colw = d.width; + start = i; + } + } + } + moveComponentsVertical(target, x, insets.top + getVgap(), colw, maxheight - y, start, nmembers ); + } + } + + /** + * Centers the elements in the specified row, if there is any slack. + * @param target the component which needs to be moved + * @param x the x coordinate + * @param y the y coordinate + * @param width the width dimensions + * @param height the height dimensions + * @param colStart the beginning of the column + * @param colEnd the the ending of the column + */ + private void moveComponentsVertical(Container target, int x, int y, int width, int height, + int colStart, int colEnd) { + synchronized (target.getTreeLock()) { + switch (getAlignment()) { + case TOP: + y += 0; + break; + case CENTER_VERTICAL: + y += ( height / 2 ); // - preferredLayoutSize( target ).height ) / 2 ); + break; + case BOTTOM: + y += height; + break; + } + for (int i = colStart ; i < colEnd ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + m.setLocation(x + (width - m.getBounds().width) / 2, y ); +// m.setLocation(x, y ); +// m.setSize( width, m.getBounds().height ); //! + y += m.getBounds().height + getVgap(); + } + } + } + } + + /** + * Returns a dimension representing the maximum preferred + * height and width of all the components in the container. + * @param target the container to scan. + * @return a dimension containing the maximum values. + */ + protected Dimension getUniformDimension(Container target) { + Component m = null; + Dimension preferred = null; + int maxWidth = 0, maxHeight = 0; + int nmembers = target.getComponentCount(); + for ( int i = 0; i < nmembers; i++ ) { + m = target.getComponent( i ); + if ( m.isVisible() ) { + preferred = m.getPreferredSize(); + maxWidth = Math.max( maxWidth, preferred.width ); + maxHeight = Math.max( maxHeight, preferred.height ); + } + } + return new Dimension( maxWidth, maxHeight ); + } + + /** + * Returns a string representation of this <code>BetterFlowLayout</code> + * object and its values. + * @return a string representation of this layout. + */ + public String toString() { + String str = ""; + switch (getAlignment()) { + case TOP: str = ",align=top"; break; + case CENTER_VERTICAL: str = ",align=vertical"; break; + case BOTTOM: str = ",align=bottom"; break; + default: return super.toString(); + } + return getClass().getName() + "[hgap=" + getHgap() + ",vgap=" + getVgap() + str + "]"; + } + + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java new file mode 100644 index 0000000..6e23ca1 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java @@ -0,0 +1,274 @@ +/* +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.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Rectangle; + +import javax.swing.JPanel; +import javax.swing.JRootPane; + +/** +* A custom layout for a JRootPane that handles the the layout of a +* JRootPane's layeredPane, glassPane, and menuBar, and in addition +* handles four decorative components arranged in a border layout. +* Add the decorative components to the JRootPane using the directional +* constants; CENTER is reserved for the content pane and menu bar. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class BetterRootLayout extends BorderLayout +{ + /** + * Returns the amount of space the layout would like to have. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's preferred size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension preferredLayoutSize(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.preferredLayoutSize( parent ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Returns the minimum amount of space the layout needs. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's minimum size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension minimumLayoutSize(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.minimumLayoutSize( parent ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Returns the maximum amount of space the layout can use. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's maximum size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension maximumLayoutSize(Container target) + { + JRootPane rootPane = (JRootPane) target; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.maximumLayoutSize( target ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Instructs the layout manager to perform the layout for the specified + * container. + * + * @param the Container for which this layout manager is being used + * @throws ClassCastException if parent is not a JRootPane + */ + public void layoutContainer(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + Rectangle b = parent.getBounds(); + Insets i = rootPane.getInsets(); + int w = b.width - i.right - i.left; + int h = b.height - i.top - i.bottom; + + // layout panes + + if(rootPane.getLayeredPane() != null) { + rootPane.getLayeredPane().setBounds(i.left, i.top, w, h); + } + if(rootPane.getGlassPane() != null) { + rootPane.getGlassPane().setBounds(i.left, i.top, w, h); + } + + // handle proxy panel + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + this.addLayoutComponent( proxyPanel, CENTER ); + + super.layoutContainer( parent ); + + // use proxy sizes to set sizes of layeredPane's children + + Rectangle proxyRect = proxyPanel.getBounds(); + if(rootPane.getJMenuBar() != null) { + Rectangle menuRect = proxyPanel.getBounds(); + menuRect.height = rootPane.getJMenuBar().getPreferredSize().height; + rootPane.getJMenuBar().setBounds( menuRect ); + proxyRect.y += menuRect.height; + proxyRect.height -= menuRect.height; + } + if(rootPane.getContentPane() != null) { + rootPane.getContentPane().setBounds( proxyRect ); + } + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + } + + /** + * Passes NORTH, SOUTH, EAST, WEST and CENTER to super implementation, + * and ignores all others. + */ + public void addLayoutComponent(Component comp, Object constraints) + { + if ( NORTH.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( SOUTH.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( EAST.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( WEST.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( CENTER.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + + // otherwise, ignore + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java new file mode 100644 index 0000000..deb0eb6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java @@ -0,0 +1,123 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.event.MouseEvent; + +import javax.swing.event.MouseInputListener; +import javax.swing.plaf.basic.BasicTableUI; + +/** +* BetterTableUI allows a JTable to be disabled by +* listening for MouseEvents and then forwarding them +* to the usual MouseInputHandler only if the table +* is enabled. <BR><BR> +* +* This class also works around a bug where an editable +* table's selection is changed when clicking in an edit +* cell while the control key is down. This typically +* happened while users were copying/pasting data from +* cell to cell. <BR><BR> +* +* To use, call <code>JTable.setUI()</code> on any +* JTable with a BetterTableUI as the parameter. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class BetterTableUI extends BasicTableUI implements MouseInputListener +{ +/** +* The listener to get all mouse events when the table is enabled. +*/ + protected MouseInputListener delegateHandler; + +/** +* Overridden to set self as mouse listener and create delegate. +*/ + protected MouseInputListener createMouseInputListener() + { + // normal handler is a protected inner class of parent + delegateHandler = new MouseInputHandler(); + + return this; + } + + // interface MouseInputListener + + public void mouseClicked(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseClicked(event); + } + } + + public void mouseDragged(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseDragged(event); + } + } + + public void mouseEntered(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseEntered(event); + } + } + + public void mouseExited(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseExited(event); + } + } + + public void mouseMoved(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseMoved(event); + } + } + + public void mousePressed(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + // workaround bug - control key removes an existing selection + if ( table.isEditing() && event.isControlDown() ) return; + + delegateHandler.mousePressed(event); + } + } + + public void mouseReleased(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseReleased(event); + } + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java new file mode 100644 index 0000000..769e866 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java @@ -0,0 +1,610 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.AWTEventMulticaster; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Enumeration; +import java.util.Vector; + +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.UIManager; + +/** +* ButtonPanel handles display and event broadcasting of standard buttons like +* OK/Cancel/Save/etc. The constructor takes a list or array of strings, each +* representing a button to appear on the panel from left to right. +* Any button click will send an action event to all listeners with the action +* command containing the corresponding string. Note action events are simply +* forwarded from the buttons themselves, so the source of the event will be +* the button, not the button panel. The button panel is the source of the +* STATE_CHANGED events that notify about changes to the panel itself.<BR><BR> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ButtonPanel extends JPanel implements ActionListener, MouseMotionListener +{ + // TODO: Button text should be read from resources. +/** +* Specifies a "OK" button. +* This is also the action command sent by the OK button. +*/ + public static final String OK = "OK"; +/** +* Specifies a "Save" button. +* This is also the action command sent by the Save button. +*/ + public static final String SAVE = "Save"; +/** +* Specifies a "Refresh" button. +* This is also the action command sent by the Refresh button. +*/ + public static final String REFRESH = "Refresh"; +/** +* Specifies a "Clear All" button. +* This is also the action command sent by the Clear All button. +*/ + public static final String CLEAR_ALL = "Clear All"; +/** +* Specifies a "Refresh" button. +* This is also the action command sent by the Cancel button. +*/ + public static final String CANCEL = "Cancel"; +/** +* Specifies a "Yes" button. +* This is also the action command sent by the Yes button. +*/ + public static final String YES = "Yes"; +/** +* Specifies a "No" button. +* This is also the action command sent by the No button. +*/ + public static final String NO = "No"; +/** +* Specifies an "Add" button. +* This is also the action command sent by the Add button. +*/ + public static final String ADD = "Add"; +/** +* Specifies a "Remove" button. +* This is also the action command sent by the Remove button. +*/ + public static final String REMOVE = "Remove"; +/** +* This is the action command to all listeners when the button state is changed. +*/ + public static final String STATE_CHANGED = "STATE_CHANGED"; + +/** +* This is the container to which buttons are added. +*/ + protected Container buttonContainer = null; // useful for subclasses +/** +* This is the list of all buttons on the panel. +*/ + protected Vector buttonList = null; +/** +* The insets for this panel, so they can be modified. +*/ + protected Insets insets = new Insets( 5, 5, 5, 5 ); + +/** +* This is the layout manager - which must be a FlowLayout or subclass. +*/ + protected FlowLayout buttonPanelLayout = null; + + // for action multicasting + protected ActionListener actionListener = null; + + +/** +* Constructs a ButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public ButtonPanel() + { + buttonList = new Vector(); + initLayout(); + + // default labels for bean layout + setLabels( new String[] { "One", "Two", "Three" } ); + } + +/** +* This method is responsible for the initial layout of the panel. +* Subclasses can implement different layouts, but this method +* is responsible for initializing buttonContainer and buttonPanelLayout +* and setting the container to use the layout. +*/ + protected void initLayout() + { + this.setInsets( super.getInsets() ); + buttonContainer = this; + buttonPanelLayout = new BetterFlowLayout( BetterFlowLayout.RIGHT ); + buttonContainer.setLayout(buttonPanelLayout); + ((BetterFlowLayout)buttonPanelLayout).setWidthUniform( true ); + + // setBackground( Color.blue ); // useful for debugging + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public ButtonPanel( String[] buttonList ) + { + this(); + setLabels( buttonList ); + } + +/** +* Constructs a ButtonPane using specified actions. For each action, a button +* is created, that when pressed the corresponding action is activated. The +* "name" of the action is used as the title of the button. +* @param actionList An array of actions to be used to create buttons with. +*/ + public ButtonPanel( Action[] actionList ) + { + this(); + setLabels( actionList ); + } + +/** +* Creates the buttons to appear on the panel. Any existing buttons +* are replaced. The labels are used as names and action commands +* in addition to labels. +* @param labels An array of strings to be used in labeling the buttons. +* If null, all buttons will be removed. +*/ + public void setLabels( String[] labels ) + { + if ( labels == null ) + { + labels = new String[] {}; + } + + buttonContainer.removeAll(); + this.buttonList = new Vector( labels.length ); + + String item = null; + Component button; + for ( int i = 0; i < labels.length; i++ ) + { + item = labels[i]; + if ( item != null ) + { + button = createComponentWithLabel( item.toString() ); + this.buttonList.addElement( item ); + addComponentToPanel( button ); + button.setEnabled( this.isEnabled() ); +/* + if ( i == 0 ) + { + JRootPane root = SwingUtilities.getRootPane( button ); + if ( root != null ) + root.setDefaultButton( button ); + } +*/ + } + else + { + throw new IllegalArgumentException( "ButtonPanel.setButtons: nulls are not allowed." ); + } + } + + this.revalidate(); + this.repaint(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED ) ); + } + +/** +* +*/ + public void setLabels( Action[] actions ) + { + if ( actions == null ) + { + actions = new Action[] {}; + } + + buttonContainer.removeAll(); + this.buttonList = new Vector( actions.length ); + + Action action = null; + Component button; + for ( int i = 0; i < actions.length; i++ ) + { + action = actions[i]; + if ( action != null ) + { + String name = ( String )action.getValue( Action.NAME ); + button = createComponentWithLabel( name ); + this.buttonList.addElement( name ); + addComponentToPanel( button ); + button.setEnabled( this.isEnabled() ? action.isEnabled() : false ); + + // Add the action to the "button" if it knows about action listeners. + try + { + Method addActionListenerMethod = + button.getClass().getMethod( "addActionListener", new Class[] { ActionListener.class } ); + addActionListenerMethod.invoke( button, new Object[] { action } ); + } + catch ( NoSuchMethodException e ) { /* Do Nothing */ } + catch ( IllegalAccessException e ) { e.printStackTrace(); /* TODO: Do Something? */ } + catch ( InvocationTargetException e ) { e.printStackTrace(); /* TODO: Do Something? */ } + + // Create a new listener for property change events and have + // the action broadcast to that listener. + PropertyChangeListener pcListener = new ActionChangeListener( button ); + action.addPropertyChangeListener( pcListener ); + } + else + { + throw new IllegalArgumentException( "ButtonPanel.setButtons: nulls are not allowed." ); + } + } + + this.revalidate(); + this.repaint(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED ) ); + } + + +/** +* Gets the labels of the buttons that appear on the panel, ordered from left to right. +* @return A new list containing strings used in labeling the buttons. +*/ + public String[] getLabels() + { + String[] labels = new String[ buttonList.size() ]; + int i = 0; + for ( Enumeration it = buttonList.elements(); it.hasMoreElements(); ) + { + labels[i++] = it.nextElement().toString(); + } + return labels; + } + +/** +* Gets the first component having the specified name. +* @return A component with the specified name, or null if none match. +*/ + public Component getButton( String aLabel ) + { + if ( aLabel == null ) return null; + + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( aLabel.equals( c.getName() ) ) + { + return c; + } + } + return null; + } + +/** +* Creates a new component with the specified label. +* The label is also used for the component's name +* and action command, if any. +* (This implementation returns a JButton.) +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; // TODO: get string from resource + JButton newButton = new JButton(); // might allow other types in future + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + return newButton; + } + +/** +* Adds a component to the right-most side of the layout. +* @param aComponent The component to be added to the layout. +*/ + protected void addComponentToPanel( Component aComponent ) + { + buttonContainer.add( aComponent ); + } + + +/** +* Changes the alignment of the buttons in the panel. Defaults to right-justified. +* @param alignment A valid alignment code, per BetterFlowLayout implementation. +* @see BetterFlowLayout +*/ + public void setAlignment( int alignment ) + { + buttonPanelLayout.setAlignment(alignment); + buttonContainer.doLayout(); + } +/** +* Gets the alignment of the buttons in the panel. +* @return An alignment code, per FlowLayout implementation. +* @see FlowLayout +*/ + public int getAlignment() + { + return buttonPanelLayout.getAlignment(); + } + +/** +* Changes the horizontal spacing between components in the panel. +* @param newHgap the new spacing, in pixels. May not be negative. +*/ + public void setHgap( int newHgap ) + { + if ( newHgap < 0 ) return; // may not be negative + buttonPanelLayout.setHgap( newHgap ); + } + +/** +* Gets the current horizontal spacing between components. +* @return the current horizontal spacing, in pixels. +*/ + public int getHgap() + { + return buttonPanelLayout.getHgap(); + } + +/** +* Changes the vertical spacing between components in the panel. +* @param newVgap the new spacing, in pixels. May not be negative. +*/ + public void setVgap( int newVgap ) + { + if ( newVgap < 0 ) return; // may not be negative + buttonPanelLayout.setVgap( newVgap ); + } + +/** +* Gets the current vertical spacing between components. +* @return the current vertical spacing, in pixels. +*/ + public int getVgap() + { + return buttonPanelLayout.getVgap(); + } + +/** +* Changes the insets for this panel. +* @param newInsets the new insets. +*/ + public void setInsets( Insets newInsets ) + { + insets = newInsets; + } + +/** +* Overridden to return the user-specified insets for this panel. +* @return the current insets for this panel. +*/ + public Insets getInsets() + { + return insets; + } + +/** +* Overridden to call setEnabled on all components on panel. +* @param isEnabled whether to enable the panel and all components on it. +*/ + public void setEnabled( boolean isEnabled ) + { + super.setEnabled( isEnabled ); + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + buttonContainer.getComponent( i ).setEnabled( isEnabled ); + } + } + + // Action Multicast methods + +/** +* Adds an action listener to the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + + // interface ActionListener + +/** +* Called by buttons on panel and by other components that +* might be set to broadcast events to this listener. +* @param e An action event to be received. +*/ + public void actionPerformed(ActionEvent e) + { + broadcastEvent(e); + } + +/** +* A property change listener that listens specifically for property changes +* from action objects. This is the class that ties in the action to the +* button. This class is added to an action as a property change listener. +* The corresponding component is referenced by this class toe easily handle +* updates to the component caused by changes to the action. +*/ + public class ActionChangeListener implements PropertyChangeListener + { + /** The UI component that is affected by the action's changes. */ + Component theComponent; + + /** + * Constructs an ActionChangeListener with the given component being + * the recipient of the action's changes. + * @param The component to bind with the action. + */ + public ActionChangeListener( Component aComponent ) + { + super(); + theComponent = aComponent; + } + + /** + * Called whenever a property changes on the action object. + * @pram e The property change event generated by the action. + */ + public void propertyChange( PropertyChangeEvent e ) + { + String propertyName = e.getPropertyName(); + if ( propertyName.equals( Action.NAME ) ) + { + String name = ( String )e.getNewValue(); + if ( theComponent instanceof AbstractButton ) + { + AbstractButton button = ( AbstractButton )theComponent; + String oldName = button.getName(); + button.setText( name ); + button.setName( name ); + button.setActionCommand( name ); + + // Replace the old name of the component with the new name + // in the ButtonPanel's list of components. + buttonList.setElementAt( name, buttonList.indexOf( oldName ) ); + } + + // TODO: If component is not a button (or doesn't define the getText() + // then what should be done. + } + else if ( propertyName.equals( "enabled" ) ) + { + Boolean enabled = ( Boolean )e.getNewValue(); + theComponent.setEnabled( ButtonPanel.this.isEnabled() ? enabled.booleanValue() : false ); + } + + // TODO: Icon? + } + } + + + // for testing + + public static void main( String[] argv ) + { + try + { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + JFrame dialog = new JFrame(); + BorderLayout bl = new BorderLayout( 20, 20 ); + + ButtonPanel panel = new ButtonPanel(); +// ButtonPanel panel = new ButtonPanel( new String[] { "OkayOkay", "CancelCancel" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, BorderLayout.CENTER ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + + panel.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + panel.getButton( "One" ).setEnabled( false ); + + dialog.pack(); + dialog.setVisible( true ); + + try + { + BeanInfo info = Introspector.getBeanInfo( ButtonPanel.class ); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for ( int i = 0; i < props.length; i++ ) + { + System.out.println( props[i].getName() ); + } + } + catch (Exception exc) + { + System.out.println( exc ); + } + + + + + } + + public void mouseDragged(MouseEvent e) + { + } + + public void mouseMoved(MouseEvent e) + { + } + + + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java new file mode 100644 index 0000000..5e847ae --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java @@ -0,0 +1,272 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Component; +import java.util.List; +import java.util.Vector; + +import javax.swing.AbstractButton; +import javax.swing.JCheckBox; +import javax.swing.border.EmptyBorder; + +/** +* CheckButtonPanel is a simple extension of ButtonPanel. +* Differences are that it uses JCheckBoxes and the +* default alignment is vertical. The panel defaults to having +* no buttons selected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class CheckButtonPanel extends ButtonPanel +{ +/** +* Constructs a CheckButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public CheckButtonPanel() + { + super(); + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public CheckButtonPanel( String[] buttonList ) + { + super( buttonList ); + } + +/** +* Overridden to set vertical-center alignment and zero vgap. +*/ + protected void initLayout() + { + super.initLayout(); + buttonPanelLayout.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + buttonPanelLayout.setVgap( 0 ); + } + +/** +* Overridden to return a JRadioButton. +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; + JCheckBox newButton = new JCheckBox(); + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + + // reduce insets per java l&f guidelines (was 4 on each side) + newButton.setBorder( new EmptyBorder( 1, 4, 1, 4 ) ); + + return newButton; + } + +/** +* Sets the value of the button whose name matches the given text value. +* @param aName A String matching the name of one of the buttons. +* If null, empty, or not matching, nothing happens. +* @param aValue A value to set the button. +*/ + public void setValue(String aName, boolean aValue) + { + if ( aName != null ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( c.getName().equals( aName ) ) + { + ((AbstractButton)c).setSelected( aValue ); + c.repaint(); + return; + } + } + } + } + // null, empty, or not matching - exit. + System.out.println( "CheckButtonPanel.setValue: not found: " + aName ); + } + +/** +* Sets the state of the specified buttons to the specified value. +* @param aLabelArray An Array of Strings listing the buttons to be set. +* @param aValue The value to which the specified buttons will be set. +*/ + public void setValues(String[] aLabelArray, boolean aValue) + { + if ( aLabelArray != null ) + { + for ( int i = 0; i < aLabelArray.length; i++ ) + { + setValue( aLabelArray[i], aValue ); + } + } + } + +/** +* Convenience method to set all checkboxes on the panel. +* @param aValue The value to which all checkboxes on the panel will be set. +*/ + public void setAllValues(boolean aValue) + { + setValues( getLabels(), aValue ); + } + +/** +* Convenience method to check all boxes on the panel. +*/ + public void checkAll() + { + setAllValues( true ); + } + +/** +* Convenience method to clear all boxes on the panel. +*/ + public void clearAll() + { + setAllValues( false ); + } + +/** +* A convenience method to set only those buttons on the entire +* panel that should be checked. Buttons not in the list are unchecked. +* @param aLabelArray An Array of Strings listing the buttons to be set. +*/ + public void setCheckedValues(String[] aLabelArray) + { + setAllValues( false ); + setValues( aLabelArray, true ); + } + +/** +* Gets the labels of all checkboxes that are checked. +* @return A List of Strings containing the labels of the boxes that are checked. +*/ + public List getCheckedValueList() + { + Vector v = new Vector(); + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( ((AbstractButton)c).isSelected() ) + { + v.addElement(c.getName()); + } + } + } + return v; + } + +/** +* Gets the labels of all checkboxes that are checked. +* @return A String Array containing the labels of the boxes that are checked. +*/ + public String[] getCheckedValues() + { + List v = getCheckedValueList(); + String[] result = new String[ v.size() ]; + for ( int i = 0; i < v.size(); i++ ) + { + result[i] = (String) v.get(i); + } + return result; + } + +/** +* Gets the value of the specified button. +* @param aName A String matching the name of one of the buttons. +* @return True if the button is checked, False if it is not checked. +* NOTE: If the button is not found in the list, False is returned. +*/ + public boolean getValue( String aName ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( ( c instanceof AbstractButton ) && ( ((AbstractButton)c).isSelected() ) ) + { + if ( ((AbstractButton)c).getText().equals( aName ) ) + { + return ((AbstractButton)c).isSelected(); + } + } + } + return false; + } + + // for testing + + public static void main( String[] argv ) + { + try + { + javax.swing.UIManager.setLookAndFeel( javax.swing.UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + javax.swing.JFrame dialog = new javax.swing.JFrame(); + java.awt.BorderLayout bl = new java.awt.BorderLayout( 20, 20 ); + + CheckButtonPanel panel = new CheckButtonPanel( new String[] { "One", "Two", "Three" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, java.awt.BorderLayout.CENTER ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + + panel.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + panel.getButton( "One" ).setEnabled( false ); + panel.setValues( new String[] { "One" }, true ); + panel.setValue( "Three", true ); +// panel.setCheckedValues( new String[] { "Two" } ); + String[] values = panel.getCheckedValues(); + for ( int i = 0; i < values.length; i++ ) + { + System.out.println( values[i] ); + } + + dialog.pack(); + dialog.setVisible( true ); + + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java new file mode 100644 index 0000000..a0a14ac --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java @@ -0,0 +1,84 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultCellEditor; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JTable; + +/** +* A TableCellEditor that edits colors - it launches a color dialog when clicked. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +class ColorCellEditor extends DefaultCellEditor { + Color currentColor = null; + + public ColorCellEditor(JButton b) + { + super(new JCheckBox()); // unfortunately, the constructor + // expects a check box, combo box, + // or text field. + editorComponent = b; + setClickCountToStart(1); // this is usually 1 or 2. + + // must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + fireEditingStopped(); + } + } + ); + } + + protected void fireEditingStopped() + { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() + { + return currentColor; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) + { + ((JButton)editorComponent).setText(value.toString()); + currentColor = (Color)value; + return editorComponent; + } +} + + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java new file mode 100644 index 0000000..0552183 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java @@ -0,0 +1,81 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that renders colors. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ColorCellRenderer extends JLabel implements TableCellRenderer { + Border unselectedBorder = null; + Border selectedBorder = null; + boolean isBordered = true; + + public ColorCellRenderer(boolean isBordered) + { + super(); + this.isBordered = isBordered; + setOpaque(true); // must do this for background to show up. + } + + public Component getTableCellRendererComponent( + JTable table, Object color, + boolean isSelected, boolean hasFocus, + int row, int column) + { + setBackground((Color)color); + if (isBordered) + { + if (isSelected) + { + if (selectedBorder == null) + { + selectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getSelectionBackground()); + } + setBorder(selectedBorder); + } + else + { + if (unselectedBorder == null) + { + unselectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getBackground()); + } + setBorder(unselectedBorder); + } + } + return this; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java new file mode 100644 index 0000000..2bf8dd6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java @@ -0,0 +1,57 @@ +/* +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.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.JComboBox; +import javax.swing.JTable; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that paints a JComboBox. Useful if +* you want to visibly display the JComboBox before the +* user clicks on the cell. +* +* @author bsafa@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ComboBoxCellRenderer extends JComboBox implements TableCellRenderer { + + public ComboBoxCellRenderer() + { + super(); + setOpaque(true); + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + setBackground(Color.white); + setSelectedItem(value); + return this; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java new file mode 100644 index 0000000..18ed035 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java @@ -0,0 +1,630 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Vector; + +import javax.swing.JOptionPane; +import javax.swing.JTextField; + + +/** +* DateTextField is a "smart" text field that restricts the user's input. The +* input is restructed to a string representing a date format. +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class DateTextField extends JTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Use the current date for this text field. +*/ + public static final int CURRENT_DATE = 0; + +/** +* Use blanks for this text field. +*/ + public static final int BLANKS = 1; + +/** +* Use underscores for this text field. +*/ + public static final int UNDERSCORES = 2; + +/** +* Use just a 4-digit year for this text field. +*/ + public static final int YEAR = 3; + + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int PASTE = 22; // Ctl-V + private static final int CUT = 24; // Ctl-X + + +/******************************* +* DATA MEMEBERS +*******************************/ + private int defaultType = CURRENT_DATE; + + private boolean warningMessageActive = false; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default Constructor. +*/ + public DateTextField() + { + this(1, 1, 1999, 0); + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + +/** +* Constructor. +* @param month Number of the month, January being 1. +* @param data The day of the month. +* @param year The year. +*/ + public DateTextField(int month, int date, int year) + { + this(month, date, year, 0); + } + +/** +* Constructor. +* @param columns Width of the text field (in characters). +*/ + public DateTextField(int columns) + { + this(1, 1, 1998, columns); + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + +/** +* Constructor. +* @param month Number of the month, January being 1. +* @param data The day of the month. +* @param year The year. +* @param columns Width of the text field (in characters). +*/ + public DateTextField(int month, int date, int year, int columns) + { + super("", columns); + + super.setText(createDateString(month, date, year)); + + this.addFocusListener(new FocusAdapter() + { + public void focusLost(FocusEvent e) + { + if (!(e.isTemporary())) + { + validateDateString(e); + } + } + }); + } + +/** +* Sets the date type to display when the user has not entered any date yet. +* Default is the current date. +* @see #CURRENT_DATE +* @see #BLANKS +* @see #UNDERSCORES +* @param newDefaultType The type of date to display when there is no date data. +*/ + public void setDefaultType(int newDefaultType) + { + if (newDefaultType == BLANKS) + { + defaultType = BLANKS; + super.setText(" / / "); + } + else if (newDefaultType == UNDERSCORES) + { + defaultType = UNDERSCORES; + super.setText("__/__/____"); + } + else if (newDefaultType == YEAR) + { + defaultType = YEAR; + super.setText("0000"); + } + else + { + defaultType = CURRENT_DATE; + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + } + +/** +* Returns the type of date to display when there is no user input. +* @see #CURRENT_DATE +* @see #BLANKS +* @see #UNDERSCORES +* @return The type of date to display when there is no date to display. +*/ + public int getDefaultType() + { + return defaultType; + } + +/** +* Sets the text field to the string representation of the specified date. +* @param aDate The date to set the text field to. +*/ + public void setDate(Date aDate) + { + Calendar aCalendar = Calendar.getInstance(); + + aCalendar.setTime(aDate); + + super.setText(createDateString(aCalendar.get(Calendar.MONTH) + 1, + aCalendar.get(Calendar.DATE), + aCalendar.get(Calendar.YEAR))); + } + +/** +* Sets the text field directly from a Date object. +* @param aDate The date to set the text field to. +*/ + public void setText( Date aDate ) + { + setDate( aDate ); + } + +/** +* Sets the text field to the date specified in the string. This is overridden +* from the parent class to insure a valid date is inputted. The format of the +* date expected is the type of date format this text field is currently set to. +* @param aString A string representing a date in this text field current format. +*/ + public void setText( String aString ) + { + Date testDate = null; + + if ( aString != null ) + { + ParsePosition position = new ParsePosition( 0 ); + + if ( defaultType == YEAR ) + { + SimpleDateFormat yearFormatter = new SimpleDateFormat( "yyyy" ); + testDate = yearFormatter.parse( aString, position ); + } + else + { + SimpleDateFormat fullDateFormatter = new SimpleDateFormat( "MM/dd/yyyy" ); + testDate = fullDateFormatter.parse( aString, position ); + } + } + + // The string is not a valid date, use default value for date then. + if ( testDate == null ) + { + Calendar aCalendar = Calendar.getInstance(); + + testDate = aCalendar.getTime(); + } + + setDate( testDate ); + } + +/** +* Returns the date as represented by the date string in the text field. +* @return The date in the text field. +*/ + public Date getDate() throws NumberFormatException + { + Calendar aCalendar = Calendar.getInstance(); + int year = 1980; + int month = 0; + int date = 1; + int[] tempArray = {1,3,5,7,8,10,12}; + Vector monthsWith31Days = new Vector(7); + + for (int i = 0; i < tempArray.length; ++i) + { + monthsWith31Days.addElement(new Integer(tempArray[i])); + } + + aCalendar.set(year, month, date, 12, 0, 0); + + try + { + String dateString = getText(); + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + dateString)); + + if (defaultType == YEAR) + { + year = Integer.parseInt(dateString); + + aCalendar.set(year, 0, 1, 12, 0, 0); + + return aCalendar.getTime(); + } + + month = Integer.parseInt(dateString.substring(0, 2).trim()); + date = Integer.parseInt(dateString.substring(3, 5).trim()); + year = Integer.parseInt(dateString.substring(6).trim()); + + if ((month < 1) || (month > 12)) + { + throw nfException; + } + + if ((date < 1) || (date > 31)) + { + throw nfException; + } + + if ((date == 31) && (!(monthsWith31Days.contains(new Integer(month))))) + { + throw nfException; + } + + if ((date == 30) && (month == 2)) + { + throw nfException; + } + + if ((date == 29) && (month == 2)) + { + if ((year % 100) == 0) + { + if ((year % 400) != 0) + { + throw nfException; + } + } + else + { + if ((year % 4) != 0) + { + throw nfException; + } + } + } + } + catch (IndexOutOfBoundsException ioobe) + { + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + getText())); + throw nfException; + } + catch (NumberFormatException nfe) + { + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + getText())); + throw nfException; + } + + aCalendar.set(year, (month - 1), date, 12, 0, 0); + + return aCalendar.getTime(); + } + + public void processKeyEvent(KeyEvent e) + { + String currentString = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int currentCaretPosition = 0; + int selectionStart = 0; + int selectionEnd = 0; + int modifierPosition = 0; + int modifierDirection = 1; + char modifierCharacter; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean cut = false; + boolean keyPressed = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + cut = (newChar == CUT); + + keyPressed = (e.paramString().startsWith("KEY_PRESSED")); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste) || (cut))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + if ((isPrintableCharacter(newChar)) || (backspace) || (delete)) + { + // Both the key "pressed" and key "released" events get passed + // in here for the delete and backspace key. Only processes + // these keys if the event is key "pressed". + if (((backspace) || (delete)) && (!(keyPressed))) + { + // Don't do anything, pass through to consumption. + } + else + { + // Analyze the current contents of the field + currentString = getText(); + currentLength = currentString.length(); + + char[] tempText = new char[currentLength]; + + currentCaretPosition = getCaretPosition(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + // if a range is selected, then get rid of it and place the caret + // at the begginning of the range and continue processing. + if (selectionStart != selectionEnd) + { + selectionEnd = selectionStart; + setSelectionEnd(selectionEnd); + + currentCaretPosition = selectionStart; + setCaretPosition(currentCaretPosition); + } + + if (currentCaretPosition <= currentLength) + { + // a number of delete or backspace was pressed, delete and + // backspace deletes a number and places a "space" there + + // if caret at start of string and the backspace pressed OR + // caret at end of string and delete or number pressed THEN + // don't do anything, otherwise process key stroke + if (((currentCaretPosition == 0) && (backspace)) || + ((currentCaretPosition == currentLength) && (!(backspace)))) + { + // Don't do any processing. + } + else + { + modifierPosition = currentCaretPosition; + if (backspace) + { + modifierDirection = -1; + modifierPosition += modifierDirection; + } + + // Overwrite the current position with the new character + // inputted or overwrite using a space or underscore if + // the backspace or delete key was pressed. + if (defaultType != YEAR) + { + modifierCharacter = + ((delete)||(backspace)) ? + ((defaultType == UNDERSCORES) ? '_' : ' ') : + newChar; + } + else + { + // We are dealing with a 4-digit year. Overwrite + // with new character or "0" if delete or backspace + // was pressed. + modifierCharacter = ((delete)||(backspace)) ? ('0') : newChar; + } + + if (currentString.charAt(modifierPosition) == '/') + { + modifierPosition += modifierDirection; + } + + for (int i = 0; i < currentLength; ++i) + { + if (i == modifierPosition) + { + tempText[i] = modifierCharacter; + } + else + { + tempText[i] = currentString.charAt(i); + } + } + + testString = new String(tempText); + if (isValidString(testString)) + { + super.setText(testString); + if (backspace) + { + setCaretPosition(modifierPosition); + } + else + { + setCaretPosition(modifierPosition + 1); + } + } + } + } + } + + e.consume(); + } + else if ((cut) || (paste)) + { + e.consume(); + } + // else its a non-printable character, let it pass through + } + else + { + e.consume(); + } + } + + super.processKeyEvent(e); + } + + private boolean isValidCharacter(char aChar) + { + if (((aChar >= '!') && (aChar <= '/')) || ((aChar >= ':') && (aChar <= '~'))) + { + return false; + } + return true; + } + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + + private boolean isValidDate(int month, int date, int year) + { + if ((month < 1) || (month > 12)) + { + return false; + } + + if ((date < 1) || (date > 31)) + { + return false; + } + + if ((year < 0) || (year > 9999)) + { + return false; + } + + return true; + } + + private boolean isValidString(String aString) + { + return true; + } + + private String createDateString(int month, int date, int year) + { + String dateString = ""; + + if (isValidDate(month, date, year)) + { + if (defaultType != YEAR) + { + if (month < 10) + { + dateString = "0"; + } + + dateString += String.valueOf(month); + dateString += "/"; + + if (date < 10) + { + dateString += "0"; + } + + dateString += String.valueOf(date); + dateString += "/"; + } + + if (year < 1000) + { + dateString += "0"; + if (year < 100) + { + dateString += "0"; + if (year < 10) + { + dateString += "0"; + } + } + } + + dateString += String.valueOf(year); + } + else + { + if (defaultType == YEAR) + { + dateString = "1999"; + } + else + { + dateString = "01/01/1999"; + } + } + + return dateString; + } + + private void validateDateString(FocusEvent e) + { + if (!(warningMessageActive)) + { + try + { + getDate(); + } + catch (NumberFormatException nfe) + { + System.out.println("Invalid Date String!!!"); + warningMessageActive = true; + JOptionPane.showMessageDialog(this, "Invald Date: " + getText(), "Warning", JOptionPane.WARNING_MESSAGE); + warningMessageActive = false; + if (defaultType == YEAR) + { + super.setText("1999"); + } + else + { + super.setText("01/01/1999"); + } + } + } + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java new file mode 100644 index 0000000..b3e2a76 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java @@ -0,0 +1,284 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.text.Format; + +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +/** +* A cell renderer for dealing with formatted content. +* Subclasses can specify formats or colors or styles for specific values +* or locations in the table by overridding getFormatForContext(), +* getForegroundForContext() and/or getBackgroundForContext(). +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class FormattedCellRenderer extends DefaultTableCellRenderer +{ + protected Format currentFormat, defaultFormat; + protected Color defaultForeground, defaultBackground; + protected Font defaultFont; + +/** +* Default constructor with no specified format. +*/ + public FormattedCellRenderer() + { + this( (Format) null ); + } + +/** +* Constructor specifying a format for renderered content. +*/ + public FormattedCellRenderer( Format aFormat ) + { + currentFormat = null; + defaultFormat = aFormat; + defaultForeground = super.getForeground(); + defaultBackground = super.getForeground(); + } + +/** +* Returns the format currently in use to format cell content. +* @return The Format that is currently being used. +*/ + public Format getFormat() + { + return defaultFormat; + } + +/** +* Sets the format to be used to format cell content. +*/ + public void setFormat( Format aFormat ) + { + defaultFormat = aFormat; + } + +/** +* Overrides to retain the default foreground color, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setForeground(Color c) { + super.setForeground(c); + defaultForeground = c; + } + +/** +* Overrides to retain the default background color, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setBackground(Color c) { + super.setBackground(c); + defaultBackground = c; + } + +/** +* Overrides to retain the default font, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setFont(Font f) { + super.setFont(f); + defaultFont = f; + } + +/** +* Overridden to format the value with the appropriate Format. If the +* value cannot be formatted with the Format, the superclass method is called. +* @param value An Object to be formatted. +*/ + protected void setValue(Object value) + { + if ( currentFormat != null ) + { + try + { + +// if ( ( value instanceof Number ) && ( value.toString().indexOf( "E" ) != -1 ) ) +// { +// System.out.println( "FormattedCellRenderer.setValue: format = '" + currentFormat.getClass() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: value = '" + value + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: double value = '" + ((Number)value).doubleValue() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: float value = '" + ((Number)value).floatValue() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: converted = '" + currentFormat.format( value ) + "'" ); +// } + + // WORKAROUND: This works around what may be a rounding bug in DecimalFormat. (PR 256/297) + currentFormat.format( ZERO ); + + // DEBUG: code to test for weird one/zero problem (PR 256/297) + String result = currentFormat.format( value ); +/* above workaround seems to be working + if ( result.equals( "1" ) ) + { + System.out.println( "FormattedCellRenderer.setValue: Could be the ONE/ZERO problem!" ); + System.out.println( "FormattedCellRenderer.setValue: format = '" + currentFormat.getClass() + "'" ); + System.out.println( "FormattedCellRenderer.setValue: original value = '" + value + "'" ); + System.out.println( "FormattedCellRenderer.setValue: result = '" + result + "'" ); + } +*/ + setText( result ); + + +// setText( currentFormat.format( value ) ); + return; + } + catch ( IllegalArgumentException exc ) + { + // fall back on superclass implementation + } + } + super.setValue( value ); + } + + // FIXME: remove this when possible + private static Double ZERO = new Double( 0.0 ); + +/** +* Overridden to call context delegate methods. +*/ + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + Format format; + + // allow for context-sensitve formatting + format = getFormatForContext( table, value, isSelected, hasFocus, row, column ); + if ( format != null ) + { + currentFormat = format; + } + else + { + currentFormat = defaultFormat; + } + + Color color; + + // allow for context-sensitve foreground color + color = getForegroundForContext( table, value, isSelected, hasFocus, row, column ); + if ( color != null ) + { + super.setForeground( color ); + } + else + { + super.setForeground( defaultForeground ); + } + + // allow for context-sensitve background color + color = getBackgroundForContext( table, value, isSelected, hasFocus, row, column ); + if ( color != null ) + { + super.setBackground( color ); + } + else + { + super.setBackground( defaultBackground ); + } + + // have to call this here because super defaults to table's font + Component result = + super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); + // NOTE: DefaultTableCellRenderer returns itself. + + // allow for context-sensitve font + Font font = getFontForContext( table, value, isSelected, hasFocus, row, column ); + if ( font != null ) + { + result.setFont( font ); + } + else + { + result.setFont( defaultFont ); + } + + return result; + + } + +/** +* Override this method to provide a specific format for the +* specific cell to be rendered by this component. Any format +* returned by this method will take precedence of the format +* specified by setFormat(). <br><br> +* This default implementation returns null. +* @return A Format for this cell, or null to rely on the the +* format specified by setFormat(). +*/ + public Format getFormatForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a foreground color for the renderer. +* Because the table specifies colors for selected cells, +* these colors will only be used when renderering unselected cells. <br><br> +* This default implementation returns null. +* @return A Color for the foreground of the cell, or null to rely on +* the table's default color scheme. +*/ + public Color getForegroundForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a background color for the renderer. +* Because the table specifies colors for selected cells, +* these colors will only be used when renderering unselected cells. <br><br> +* This default implementation returns null. +* @return A Color for the background of the cell, or null to rely on +* the table's default color scheme. +*/ + public Color getBackgroundForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a font for the renderer.<br><br> +* This default implementation returns null. +* @return A Font for the cell, or null to rely on the table's default font. +*/ + public Font getFontForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java new file mode 100644 index 0000000..8320d08 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java @@ -0,0 +1,845 @@ +/* +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.Enumeration; +import java.util.EventObject; +import java.util.Vector; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.tree.TreeCellEditor; +import javax.swing.tree.TreeCellRenderer; + +/** +* A cell renderer that displays icons in addition to text, +* and additionally is an editor in case you want to click +* the icon to trigger some kind of action. +* You probably should override both getStringForContext and +* getIconForContext to achieve your desired results. +* To receive mouse clicks, set the same instance of the +* renderer as the editor for the same component.<br><br> +* +* One notable addition is that this class is an action event +* broadcaster. ActionEvents are broadcast when the mouse is +* clicked on the button with an action event containing a +* user-configurable string that defaults to CLICKED. <br><br> +* +* The renderer itself can be used as a JComponent if +* you need something like a JLabel that allows you to click +* on the icon. You will want to call setIcon and setText +* to configure the component since the renderer method would +* not be called. (If you add an instance of the renderer +* to a container, you cannnot use the same instance as an +* editor in a table, tree, or list.) +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class IconCellRenderer extends JPanel + implements TableCellRenderer, TableCellEditor, + TreeCellRenderer, TreeCellEditor, ListCellRenderer, + Runnable, ActionListener, MouseListener +{ + public static final String CLICKED = "CLICKED"; + + /** + * The panel that is re-used to render everything. + * This is returned by getRendererForContext. + */ + protected JPanel rendererPanel; + protected JLabel rendererLabel; + protected JButton rendererButton; + + /** + * The panel that is used to receive mouse clicks. + * It must be a different component from rendererPanel. + * This is returned by getEditorForContext. + */ + protected JPanel editorPanel; + protected JLabel editorLabel; + protected JButton editorButton; + + private Object lastKnownValue; + private JComponent lastKnownComponent; + + // do as DefaultTableCellRenderer does + private Border noFocusBorder; + private Border treeFocusBorder; + private Color unselectedForeground; + private Color unselectedBackground; + + private Vector actionListeners; + private String actionCommand; + private Vector cellEditorListeners; + + private boolean editable; + private boolean clickable; + + /** + * Default constructor. + */ + public IconCellRenderer() + { + editable = true; + clickable = true; + + noFocusBorder = new EmptyBorder(1, 1, 1, 1); + treeFocusBorder = new LineBorder( + UIManager.getColor("Tree.selectionBorderColor") ); + setActionCommand( CLICKED ); + + rendererPanel = new JPanel(); + rendererPanel.setLayout( new GridBagLayout() ); + + editorPanel = this; + editorPanel.setLayout( new GridBagLayout() ); + + // set up constraints + GridBagConstraints imageConstraints = new GridBagConstraints(); + imageConstraints.gridx = 0; + GridBagConstraints labelConstraints = new GridBagConstraints(); + labelConstraints.fill = GridBagConstraints.HORIZONTAL; + labelConstraints.gridx = 1; + labelConstraints.weightx = 1.0; + labelConstraints.ipadx = 1; + labelConstraints.insets = new Insets( 0, 1, 0, 0 ); // sweat the pixel + + // make the editor panel go away when not in use + // and pass through all mouse events to container + + //this is not very useful since editorLabel and editorButton + //get all of the events + editorPanel.addMouseListener( this ); + + rendererLabel = new JLabel(); + rendererLabel.setOpaque( false ); + rendererPanel.add( rendererLabel, labelConstraints ); + + editorLabel = new JLabel(); + editorLabel.setText( "" ); // default state + editorLabel.setOpaque( false ); + editorPanel.add( editorLabel, labelConstraints ); + + unselectedForeground = rendererLabel.getForeground(); + unselectedBackground = rendererLabel.getBackground(); + + rendererButton = new JButton(); + rendererButton.setBorder( null ); + rendererButton.setBorderPainted( false ); + rendererButton.setContentAreaFilled( false ); + rendererButton.setFocusPainted( false ); + rendererButton.setMargin( new Insets( 0, 0, 0, 0 ) ); + rendererPanel.add( rendererButton, imageConstraints ); + + editorButton = new JButton(); + editorButton.setEnabled( clickable ); // default state + editorButton.setIcon( null ); // default state + editorButton.setBorder( null ); + editorButton.setBorderPainted( false ); + editorButton.setContentAreaFilled( false ); + editorButton.setFocusPainted( false ); + editorButton.setMargin( new Insets( 0, 0, 0, 0 ) ); + editorPanel.add( editorButton, imageConstraints ); + + editorButton.addActionListener( this ); + + //add these in order to dispatch the MouseEvents + //to the lastKnownComponent, and proper management of + //DnD operations + editorLabel.addMouseListener( this ); + editorButton.addMouseListener( this ); + } + +/** +* Returns the text string currently displayed in the editor component. +*/ + public String getText() + { + return editorLabel.getText(); + } + +/** +* Sets the text string displayed in the editor component. +* Default is an empty string. +*/ + public void setText( String aString ) + { + editorLabel.setText( aString ); + } + +/** +* Returns the icon currently displayed in the editor component. +*/ + public Icon getIcon() + { + return editorButton.getIcon(); + } + +/** +* Sets the icon currently displayed in the editor component. +* Default is null. +*/ + public void setIcon( Icon anIcon ) + { + editorButton.setIcon( anIcon ); + if ( !isClickable() ) + { + editorButton.setDisabledIcon( anIcon ); + } + } + +/** +* Returns whether the editor component's label text is editable. +*/ + public boolean isEditable() + { + return editable; + } + +/** +* Sets whether the editor component's label text is editable. +* Default is true. Editable text is not yet implemented. +*/ + public void setEditable( boolean isEditable ) + { + editable = isEditable; + } + +/** +* Returns whether the editor component's icon is clickable. +*/ + public boolean isClickable() + { + return clickable; + } + +/** +* Sets whether the editor component's icon is clickable. +* Default is true. +*/ + public void setClickable( boolean isClickable ) + { + clickable = isClickable; + editorButton.setEnabled( clickable ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getListCellRendererComponent(JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) + { + lastKnownComponent = list; + return getRendererForContext( + list, value, index, 0, isSelected, cellHasFocus, false, true ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + lastKnownComponent = table; + return getRendererForContext( + table, value, row, column, isSelected, hasFocus, false, true ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getTreeCellRendererComponent(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) + { + lastKnownComponent = tree; + return getRendererForContext( + tree, value, row, 0, selected, hasFocus, expanded, leaf ); + } + +/** +* Returns getEditorForContext with the same parameters with hasFocus true. +*/ + public Component getTableCellEditorComponent(JTable table, + Object value, boolean isSelected, int row, int column) + { + lastKnownValue = value; + lastKnownComponent = table; + return getEditorForContext( + table, value, row, column, isSelected, true, false, true ); + } + +/** +* Returns the component from getEditorForContext with hasFocus true. +*/ + public Component getTreeCellEditorComponent(JTree tree, + Object value, + boolean isSelected, + boolean expanded, + boolean leaf, + int row) + { + + + lastKnownValue = value; + lastKnownComponent = tree; + + return getEditorForContext( + tree, value, row, 0, isSelected, true, expanded, leaf ); + } + +/** +* This default implementation returns a JPanel that is configured by +* calling configureComponentForContext. +* @return An component that is used to render content. +*/ + public Component getRendererForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + + + configureComponentForContext( rendererPanel, rendererButton, rendererLabel, + container, value, row, column, + isSelected, hasFocus, isExpanded, isLeaf ); + return rendererPanel; + } + +/** +* This method returns a separate component that should be visually +* identical to the renderer component. We can't simply reuse the +* renderer component because the renderer is still used to paint +* the table while the editor component is displayed. Clicks are +* received on this component. +* This default implementation returns a JPanel that is configured by +* calling configureComponentForContext. +* @return A component used to receive clicks on the cell. +*/ + public Component getEditorForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + configureComponentForContext( editorPanel, editorButton, editorLabel, + container, value, row, column, + true, hasFocus, isExpanded, isLeaf ); // editor should always be selected + + return editorPanel; + } + +/** +* Called to configure components +*/ + protected void configureComponentForContext( + JPanel component, JButton iconButton, JLabel label, + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + if (hasFocus) + { + if ( container instanceof JTable ) + { + component.setBorder( + UIManager.getBorder("Table.focusCellHighlightBorder") ); + } + else + { + component.setBorder( noFocusBorder ); + } + + if ( container instanceof JTree ) // was: (false) + { + label.setBorder( treeFocusBorder ); + } + else + { + label.setBorder( noFocusBorder ); + } + } + else + { + label.setBorder(noFocusBorder); + component.setBorder(noFocusBorder); + } + + if (isSelected) + { + if ( container instanceof JTree ) + { + label.setOpaque( true ); + label.setForeground(UIManager.getColor("Tree.selectionForeground")); + label.setBackground(UIManager.getColor("Tree.selectionBackground")); + component.setBackground(container.getBackground()); + } + else if ( container instanceof JTable ) + { + label.setOpaque( false ); + label.setForeground( ((JTable)container).getSelectionForeground() ); + component.setBackground(((JTable)container).getSelectionBackground()); + } + else + { + label.setOpaque( false ); + label.setForeground(UIManager.getColor("Table.selectionForeground")); + component.setBackground(UIManager.getColor("Table.selectionBackground")); + } + } + else + { + label.setOpaque( false ); + label.setForeground(container.getForeground()); + component.setBackground(container.getBackground()); + } + + label.setFont(container.getFont()); + + Icon icon = getIconForContext( + container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf ); + iconButton.setIcon( icon ); + if ( !isClickable() ) + { + iconButton.setDisabledIcon( icon ); + } + + String text = getStringForContext( + container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf ); + + if ( ( text == null ) || ( "".equals( text ) ) ) + { + if ( ! label.getText().equals( "" ) ) + label.setText( "" ); + } + else + { + if ( ! label.getText().equals( text ) ) + label.setText( text ); + } + } + +/** +* Override this method to provide an icon for the renderer. +* This default implementation returns null. +* @return An icon to be displayed in the cell, or null to omit the +* icon from the cell. +*/ + public Icon getIconForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + return null; + } + +/** +* Override this method to provide a string for the renderer. +* This default implementation returns toString on the value parameter, +* or null if the value is null. +* @return A string to be displayed in the cell. +*/ + public String getStringForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + if ( value == null ) return null; + return value.toString(); + } + + /** + * Adds the specified listener to the list of listeners + * to be notified when the button receives a click. + */ + public void addActionListener( ActionListener aListener ) + { + if ( actionListeners == null ) + { + actionListeners = new Vector( 2 ); + } + actionListeners.add( aListener ); + } + + /** + * Removes the specified listener from the list of listeners + * to be notified when the button receives a click. + */ + public void removeActionListener( ActionListener aListener ) + { + actionListeners.remove( aListener ); + } + + /** + * Broadcasts the specified action event to all listeners. + */ + protected void fireActionEvent( ActionEvent anActionEvent ) + { + if ( actionListeners == null ) return; + // vector's enumeration is not fail-fast + Enumeration e = actionListeners.elements(); + while ( e.hasMoreElements() ) + { + ((ActionListener)e.nextElement()).actionPerformed( anActionEvent ); + } + } + + /** + * Returns the action command broadcast when this icon + * receives a click. Defaults to CLICKED. + */ + public String getActionCommand() + { + return actionCommand; + } + + /** + * Sets the action command broadcast when this table + * receives a double click. + */ + public void setActionCommand( String anActionCommand ) + { + actionCommand = anActionCommand; + } + +// interface CellEditor + + /** + * Returns lastKnownValue, although this should not be called. + */ + public Object getCellEditorValue() + { + return lastKnownValue; + } + + /** + * Returns true. + */ + public boolean isCellEditable(EventObject anEvent) + { + return true; + } + + /** + * Returns true. + */ + public boolean shouldSelectCell(EventObject anEvent) + { + return true; + } + + /** + * Fires an editing stopped event and returns true. + */ + public boolean stopCellEditing() + { + ChangeEvent event = new ChangeEvent( this ); + if ( cellEditorListeners != null ) + { + // vector's enumeration is not fail-fast + Enumeration e = cellEditorListeners.elements(); + while ( e.hasMoreElements() ) + { + // broadcast editing cancelled since no value is edited + ((CellEditorListener)e.nextElement()).editingCanceled( event ); + } + } + lastKnownComponent = null; + return true; + } + + /** + * Fires an editing cancelled event and returns true. + */ + public void cancelCellEditing() + { + //HACK: cancelCellEditing() causes for the dragGesture + //to be NOT recognized AT ALL since on the next MOUSE_PRESSED + //the cell editor first needs to startEditing() [if in the tree + //the CellEditorListener is a BasicTreeUI class] + //(before the drag gesture event can be recognized). + //Also the lastKnownComponent should not be set to null, + //none of the mouse events won't dispathced to the lastKnownComponent + //in that case. + + //Not calling it at all does seem to fix it, but what are the + //consequences??? + //Trying to workaround this might solve it, but it introduces having + //an extra listener (a MouseMotionListnener), which might be wasteful + //(i.e. only if a Mouse_dragged event has been initiated, but DragGesture + //hasn't been recognized, postpone calling this till finish the DnD event) + //But what if do DnD and not exited ??? The mouseExited() is not called + //anyway until the DnD event is done. + + ChangeEvent event = new ChangeEvent( this ); + if ( cellEditorListeners == null ) return; + // vector's enumeration is not fail-fast + Enumeration e = cellEditorListeners.elements(); + + while ( e.hasMoreElements() ) + { + ((CellEditorListener)e.nextElement()).editingCanceled( event ); + } + + //DO not nullify this + lastKnownComponent = null; + } + + /** + * Adds the specified listener to the list of listeners + * to be notified when the table receives a double click. + */ + public void addCellEditorListener( CellEditorListener aListener ) + { + if ( cellEditorListeners == null ) + { + cellEditorListeners = new Vector( 2 ); + } + cellEditorListeners.add( aListener ); + } + + /** + * Removes the specified listener from the list of listeners + * to be notified when the table receives a double click. + */ + public void removeCellEditorListener( CellEditorListener aListener ) + { + cellEditorListeners.remove( aListener ); + } + +// interface ActionListener + + /** + * Puts ourself on the end of the event queue for + * firing our action event to all listeners. + */ + public void actionPerformed( ActionEvent evt ) + { + //commented out in order NOT to set lastKnownComponent to null, since + //if this object is inside a table or tree, relying on getCellEditorValue() + //to return the currently edited object + //cancelCellEditing(); + + SwingUtilities.invokeLater( this ); + } + +// interface Runnable + + /** + * Fires the action event to all listeners. + * This is triggered by a click on the icon. + */ + public void run() + { + fireActionEvent( new ActionEvent( this, 0, getActionCommand() ) ); + } + +// interface MouseListener + + /** + * Passes through editor mouse clicks to last known component. + * (left click only) + */ + public void mouseClicked(MouseEvent e) + { + if(lastKnownComponent != null){ + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + + } + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + + /** + * Passes through editor right-mouse (popup trigger) mouse events to last known component. + * Needed for possible displaying of popup menus on right click + */ + public void mousePressed(MouseEvent e) + { + if ( e.isPopupTrigger() ) + { + if(lastKnownComponent != null) + { + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + } + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + } + + /** + * Does nothing. + */ + public void mouseReleased(MouseEvent e) + { + if ( e.isPopupTrigger() ) + { + if(lastKnownComponent != null){ + + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + } + + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + } + + /** + * Does nothing. + */ + public void mouseEntered(MouseEvent e) + { + } + + /** + * Cancels cell editing. + */ + public void mouseExited(MouseEvent e) + { + Object source = e.getSource(); + if(source != null && source instanceof JComponent){ + //need to convert the Point from the source's coordinate system to editorPanel's coordinate system. + //(note that simple editorPanel.contains(e.getPoint()) fails if source is editorButton) + + Point convertedPoint = SwingUtilities.convertPoint((JComponent) source, e.getPoint(), editorPanel); + + //check if exited from editorButton, but still inside the editorPanel (works for editorLabel as well) + if(!editorPanel.contains(convertedPoint)){ + + //This was getting called before, but it interfers with the DnD operation + cancelCellEditing(); + } + } + } + + /* This might be redundant + public void cleanUp(){ + + //since cancelCellEditing() was never called call it now + cancelCellEditing(); + stopCellEditing(); + + editorButton.removeActionListener( this ); + editorPanel.removeMouseListener( this ); + editorLabel.removeMouseListener( this ); + editorButton.removeMouseListener( this ); + lastKnownComponent = null; + lastKnownValue = null; + } + */ +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java new file mode 100644 index 0000000..cdaa218 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java @@ -0,0 +1,104 @@ +/* +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.components; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.ImageObserver; + +import javax.swing.JPanel; + +/** +* A JPanel that renders an image, tiling as necessary to +* fill the panel. +* The preferred size of the panel is the size of the image +* and will change until the image is fully loaded, so using +* a media tracker is recommended. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class ImagePanel extends JPanel implements ImageObserver +{ + protected Image image; + protected int imageWidth, imageHeight; + + public ImagePanel() + { + this( null ); + } + + public ImagePanel( Image anImage ) + { + image = anImage; + if ( anImage != null ) + { + prepareImage( image, this ); + // these may return -1 + imageWidth = image.getWidth( this ); + imageHeight = image.getHeight( this ); + } + else + { + imageWidth = 0; + imageHeight = 0; + } + } + + protected void paintComponent(Graphics g) + { + if ( ( image != null ) && ( imageWidth > 0 ) && ( imageHeight > 0 ) ) + { + int width = getWidth(); + int height = getHeight(); + + for ( int x = 0; x < width; x += imageWidth ) + { + for ( int y = 0; y < height; y += imageHeight ) + { + g.drawImage( image, x, y, + imageWidth, imageHeight, + getBackground(), this ); + } + } + } + } + + public boolean imageUpdate(Image img, + int infoflags, + int x, + int y, + int width, + int height) + { + imageWidth = width; + imageHeight = height; + setPreferredSize( new Dimension( width, height ) ); + revalidate(); + repaint(); + + if ( ( infoflags & ImageObserver.ALLBITS ) == ImageObserver.ALLBITS ) + { + return false; + } + return true; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java new file mode 100644 index 0000000..55c1e36 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java @@ -0,0 +1,1693 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.AWTEventMulticaster; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.Box; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +/** +* InfoPanel uses labels and textfields (or any other component - see below) +* to display a list of keys and values in a well-aligned and consistent manner, +* conforming to alignment and pixel spacing in the java look and feel +* <a href="http://java.sun.com/products/jlf/dg/higg.htm#55417">design guidelines</a>. +* <BR><BR> +* +* Each key is displayed in a label to the left of the component that contains +* the corresponding value. Each row is displayed starting at the top of the +* component's available area. Each row's height is the maximum preferred +* height of its components and the field itself gets as much of the width as +* it can, dependent on the length of the longest label. <BR><BR> +* +* The values in the fields can be editable, and the +* current value can be retrieved using the key - for this reason, unique keys +* are recommended. <BR><BR> +* +* As a convenience, push buttons may be placed across the +* bottom of the panel in a manner similar to ButtonPanel. <BR><BR> +* +* The panel forwards any ActionEvents generated by the components and +* buttons on it to all registered listeners. <BR><BR> +* +* Optionally, any component can be used instead of a textfield. +* However, <code>get/setValueForKey()</code> and <code>get/setEditable()</code> +* may not work for those components. Use <code>getComponentForKey()</code> to +* access them instead. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class InfoPanel extends JPanel implements ActionListener +{ +/** +* Special label for an empty pair - a label and component +* that take up space but are hidden from view. This might +* be useful for achieving certain layouts. +*/ + public static final String HIDDEN = "(hidden)"; + + /** Cache for the introspectComponent method */ + private static Map _method_cache = + Collections.synchronizedMap( new HashMap(30) ); + + protected Container listContainer = null; + protected int hgap; // set in constructor + protected int vgap; // set in constructor + protected int margin; // set in constructor + protected int columns; // set in constructor + protected List fields = null; + protected List labels = null; + protected List fieldSpacers = null; + protected ButtonPanel buttonPanel = null; + protected boolean isEditable = true; + protected String prefix; + protected String postfix; + protected int labelAnchor; + protected int labelAlign; +// protected Component marginStrut = null; + + // for action multicasting + protected ActionListener actionListener = null; + +/** +* Constructs an empty InfoPanel. +*/ + public InfoPanel() + { + hgap = 12; // per java l&f guidelines + vgap = 6; // java l&f says 11 + columns = 1; // default columns + margin = 0; // default margin: none + prefix = ""; // default prefix: none + postfix = ":"; // per java l&f guidelines + fields = new ArrayList(); + labels = new ArrayList(); + labelAnchor = GridBagConstraints.NORTHWEST; + // per java l&f guidelines (CENTER is nicer) + labelAlign = SwingConstants.LEFT; + // per java l&f guidelines + + doInitialLayout(); + } + +/** +* Constructs an InfoPanel with the specified labels +* each paired with a blank textfield. +* @param labelArray An Array containing the labels in the +* order in which they should appear from top to bottom. +* A null value produces an empty panel. +*/ + public InfoPanel( String[] labelArray ) + { + this(); + setLabels( labelArray ); + } + +/** +* Creates a set of labels and empty textfields after first +* clearing all existing components on the panel. +* @param labelArray An Array containing the labels in the order +* in which they should appear from top to bottom. A null +* value will clear the panel. +*/ + public void setLabels( String[] labelArray ) + { + removeAll(); + if ( labelArray == null ) return; // null clears panel + for ( int i = 0; i < labelArray.length; i++ ) + { + addPair( labelArray[i], new JTextField() ); + } + } + +/** +* Retrieves the labls for the components on the panel +* in the order in which they are displayed from top WIDTH bottom. +* These are the keys used to reference values or to reference +* the components directly. +* @return An Array of Strings containing the labels. +*/ + public String[] getLabels() + { + int length = fields.size(); + String[] labelArray = new String[ length ]; + for ( int i = 0; i < length; i++ ) + { + labelArray[i] = ((Component)fields.get(i)).getName(); + } + return labelArray; + } + +/** +* Retrieves the constant used to anchor the labels in place. +* The default value is GridBagConstraints.NORTHWEST. +*/ + public int getLabelAnchor() + { + return labelAnchor; + } + +/** +* Sets the constant used to anchor the labels in place +* and reflows the layout. +* @param anAnchorConstant An anchor constant from +* GridBagConstraints. +*/ + public void setLabelAnchor( int anAnchorConstant ) + { + labelAnchor = anAnchorConstant; + updateLabels(); + } + +/** +* Retrieves the constant used to align the labels in place. +* The default value is GridBagConstraints.CENTER. +*/ + public int getLabelAlignment() + { + return labelAlign; + } + +/** +* Sets the constant used to align the labels in place +* and reflows the layout. +* @param anAlignmentConstant LEFT, CENTER, or RIGHT constants +* from SwingUtilities. +*/ + public void setLabelAlignment( int anAlignmentConstant ) + { + labelAlign = anAlignmentConstant; + updateLabels(); + } + +/** +* Factory method for creating panel spacers. +* This implementation returns a JPanel with +* opaque set to false. Override to customize. +*/ + public JPanel createPanel() + { + JPanel result = new JPanel(); + result.setOpaque( false ); + return result; + } + +/** +* This method is responsible for the initial layout of the panel. +* All labels and textfields will later added to listContainer. +* This method is responsible for initializing listContainer. +*/ + protected void doInitialLayout() + { + listContainer = createPanel(); + listContainer.setLayout( new BetterGridBagLayout() ); + this.setLayout( new BorderLayout() ); + this.add( listContainer, BorderLayout.NORTH ); + + //listContainer.setBackground( Color.blue ); // useful for testing + //this.setBackground( Color.red ); + } + +/** +* Changes the horizontal spacing between the label and the components in the panel. +* Note: Assumes listContainer uses a GridBagLayout. +* @param newHgap the new spacing, in pixels. May not be negative. +*/ + public void setHgap( int newHgap ) + { + if ( newHgap < 0 ) return; // may not be negative + this.hgap = newHgap; + updateGaps(); + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current horizontal spacing between components. +* @return the current horizontal spacing, in pixels. +*/ + public int getHgap() + { + return this.hgap; + } + +/** +* Changes the vertical spacing between components in the panel. +* Note: Assumes listContainer uses a GridBagLayout. +* @param newVgap the new spacing, in pixels. May not be negative. +*/ + public void setVgap( int newVgap ) + { + if ( newVgap < 0 ) return; // may not be negative + this.vgap = newVgap; + updateGaps(); + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current vertical spacing between components. +* @return the current vertical spacing, in pixels. +*/ + public int getVgap() + { + return this.vgap; + } + +/** +* Sets the minimum width for the labels column. +* This left margin will grow if one of the labels +* is wider than this value. +* Note: assumes GridBagLayout. +* @param newMargin the new minimum margin in pixels. May not be negative. +*/ + public void setMargin( int newMargin ) + { + if ( newMargin < 0 ) return; // may not be negative + this.margin = newMargin; + + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints constraints = null; + Component c = null; + int count = listContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = listContainer.getComponent( i ); + constraints = gridBag.getConstraints( c ); + if ( constraints.gridy == 0 && constraints.gridx % 2 == 0 ) + { // if this is a label spacer + // replace it with an appropriately sized box + listContainer.remove( c ); + listContainer.add( Box.createHorizontalStrut( this.margin ), constraints ); + } + } + } + + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current minimum margin for the labels column. +* @return the current minimum margin in pixels. +*/ + public int getMargin() + { + return this.margin; + } + +/** +* Sets the number of columns for the panel. +* Label/Component pairs will start from the top left +* and fill in to the right before wrapping to the +* next row. The default number of columns is one. +* Note: assumes GridBagLayout. +* @param newColumns the new number of columns. May not be less than one. +*/ + public void setColumns( int newColumns ) + { + if ( newColumns < 1 ) return; // may not be less than one. + int oldColumns = this.columns; + this.columns = newColumns; + + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + int count = listContainer.getComponentCount(); + Component[] components = listContainer.getComponents(); + GridBagConstraints[] constraints = new GridBagConstraints[ components.length ]; + for ( int i = 0; i < components.length; i++ ) + { + constraints[i] = gridBag.getConstraints( components[i] ); + } + listContainer.removeAll(); + for ( int i = 0; i < components.length; i++ ) + { + if ( constraints[i].gridy != 0 ) + { // ignore first row which is reserved for spacers. + + // translate component to new position + // (columns*2 accounts for two grid columns for one "actual" column) + int index = ( constraints[i].gridy - 1 ) * oldColumns*2 + constraints[i].gridx; + constraints[i].gridy = ( index / (newColumns*2) ) + 1; + constraints[i].gridx = index % (newColumns*2) ; + listContainer.add( components[i], constraints[i] ); + } + } + createSpacers(); // replace the spacers + updateGaps(); + } + + this.revalidate(); + this.repaint(); + + } + +/** +* Sets the vertical weight used for determining how to distribute additional +* vertical space in the component. +* @param aComponent Key that exists in the layout. +* @return weighty The weight of the component, or -1.0 if not found. +*/ + public double getVerticalWeightForKey( String key ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return -1.0; + if ( ! ( listContainer.getLayout() instanceof GridBagLayout ) ) return -1.0; + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints gbc = layout.getConstraints( c ); + return gbc.weighty; + } + +/** +* Sets the vertical weight used for determining how to distribute additional +* vertical space in the component. By default, all weights are zero, so each +* component gets its preferred height. If any weights are specified, then +* additional space is allocated to those components proportionately. +* @param aComponent Key that exists in the layout. +* @param weighty The new weight. +*/ + public void setVerticalWeightForKey( String key, double weighty ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return; + if ( ! ( listContainer.getLayout() instanceof GridBagLayout ) ) return; + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints gbc = layout.getConstraints( c ); + gbc.weighty = weighty; + layout.setConstraints( c, gbc ); + // handle adding on-the-fly + updateGaps(); + this.revalidate(); + this.repaint(); + } + +/** +* Gets the current number of columns. +* @return the current number of columns. +*/ + public int getColumns() + { + return this.columns; + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a blank JPanel will be used. +*/ + public void addPair( String key, Component component ) + { + addRow( key, new Component[] { component } ); + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a blank JPanel will appear. +*/ + public void addRow( String key, Component component ) + { + addRow( key, new Component[] { component } ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param components An array of components that will be placed next to the label. +* Any nulls in the list will be replaced with blank JPanels. +*/ + public void addRow( + String key, Component[] components ) + { + addCompositeComponent( key, makeCompositeComponent( key, components ) ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the components - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param west A component that will appear to the left of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +* @param center A component that will appear between the other components, +* taking up available space. +* A null will be replaced with a blank JPanel. +* @param east A component that will appear to the right of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +*/ + public void addRow( + String key, Component west, Component center, Component east ) + { + addCompositeComponent( key, + makeCompositeComponent( key, + west, center, east ) ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the components - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param west A component that will appear to the left of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +* @param north A component that will appear above all the other components, +* as tall as its preferred height and as wide as the info panel itself. +* @param center A component that will appear between the other components, +* taking up available space. A null will be replaced with a blank JPanel. +* @param south A component that will appear below all the other components, +* as tall as its preferred height and as wide as the info panel itself. +* @param east A component that will appear to the right of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +*/ + public void addRow( + String key, Component west, Component north, + Component center, Component south, Component east ) + { + addCompositeComponent( key, + makeCompositeComponent( key, + west, north, center, south, east ) ); + } + +/** +* Produces a container that contains the specified components, +* using GridLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component[] components ) + { + JPanel panel = createPanel(); + if ( components.length != 0 ) + { + panel.setLayout( new GridLayout( 1, components.length, hgap, vgap ) ); + + Component c; + for ( int i = 0; i < components.length; i++ ) + { + c = components[i]; + if ( c != null ) + { + introspectComponent( c, key ); + panel.add( c ); + } + } + } + return panel; + } + +/** +* Produces a container that contains the specified components, +* using BorderLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component west, Component center, Component east ) + { + JPanel panel = createPanel(); + panel.setLayout( new BorderLayout( hgap, vgap ) ); + + if ( west != null ) + { + introspectComponent( west, key ); + panel.add( west, BorderLayout.WEST ); + } + + if ( center != null ) + { + introspectComponent( center, key ); + panel.add( center, BorderLayout.CENTER ); + } + + if ( east != null ) + { + introspectComponent( east, key ); + panel.add( east, BorderLayout.EAST ); + } + + return panel; + } + +/** +* Produces a container that contains the specified components, +* using BorderLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component west, Component north, + Component center, Component south, Component east ) + { + JPanel panel = createPanel(); + panel.setLayout( new BorderLayout( hgap, vgap ) ); + + if ( west != null ) + { + introspectComponent( west, key ); + panel.add( west, BorderLayout.WEST ); + } + + if ( north != null ) + { + introspectComponent( north, key ); + panel.add( north, BorderLayout.WEST ); + } + + if ( center != null ) + { + introspectComponent( center, key ); + panel.add( center, BorderLayout.CENTER ); + } + + if ( south != null ) + { + introspectComponent( south, key ); + panel.add( south, BorderLayout.CENTER ); + } + + if ( east != null ) + { + introspectComponent( east, key ); + panel.add( east, BorderLayout.EAST ); + } + + return panel; + } + +/** +* Override to return a specific component to be used +* as a label. This implementation calls createLabel(). +*/ + protected Component createLabelForKey( String aKey ) + { + return createLabel(); + } + +/** +* Provided for backwards compatibility, and called by +* the default implementation of createLabelForKey. +* This implementation returns a JLabel. +*/ + protected JLabel createLabel() + { + return new JLabel(); + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a stock JTextField will be used. +*/ + protected void addCompositeComponent( String key, Component component ) + { + if ( key == null ) + { + key = ""; + } + Component label = createLabelForKey( key ); + Component field = component; + if ( field == null ) + { + field = new JTextField( 15 ); // default to 15 columns + } + field.setName( key ); // for association and reference + label.setName( key ); // ditto + if ( label instanceof JLabel ) + { + ((JLabel)label).setHorizontalAlignment( labelAlign ); + ((JLabel)label).setLabelFor( field ); // for accessibility + } + if ( "".equals( key ) ) + { + setText( label, "" ); + } + else + { + setText( label, prefix + key + postfix ); + } + field.setEnabled( this.isEditable ); // was: setEditable + + GridBagConstraints gbc = new GridBagConstraints(); + + if ( listContainer.getComponentCount() == 0 ) + { // we've just initialized or called removeAll + createSpacers(); + } + + gbc.gridx = ( fields.size() % this.columns ) * 2; + gbc.gridy = ( fields.size() / this.columns ) + 1; // spacer is at index zero + gbc.weightx = 0.0; + gbc.weighty = 0.0; + gbc.anchor = this.labelAnchor; + gbc.fill = GridBagConstraints.HORIZONTAL; + listContainer.add( label, gbc ); + + gbc.fill = GridBagConstraints.BOTH; + gbc.gridx = gbc.gridx + 1; + //FIXME: components default to the labelAnchor - should be different? + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + listContainer.add( field, gbc ); + + if ( key.equals( HIDDEN ) ) + { // these components are not to be shown + setText( label, " " ); + field.setVisible( false ); + } + + fields.add( field ); // using list not map to allow for duplicate keys + labels.add( label ); // ditto + + // handle adding on-the-fly + updateGaps(); + this.revalidate(); + this.repaint(); + } + +/** +* Introspects a component to set the action command and to add the +* InfoPanel to its list of ActionListeners. +* @param aComponent The Component to be introspected. +* @param aKey The action command to be set. +*/ + protected void introspectComponent( Component aComponent, String aKey ) + { + // try to set properties of whatever component this might be + try { + Method [] methods = + (Method []) _method_cache.get( aComponent.getClass() ); + if (methods == null) { + Class componentClass = aComponent.getClass(); + BeanInfo info = + Introspector.getBeanInfo( componentClass ); + + MethodDescriptor[] descriptors = + info.getMethodDescriptors(); + Method setMethod = null; + Method addMethod = null; + for ( int i = 0; + ((setMethod == null || addMethod == null) && + i < descriptors.length); + i++ ) + { + Method m = descriptors[i].getMethod(); + String name = m.getName (); + if ( setMethod == null && + name.equals( "setActionCommand" ) ) + { + setMethod = m; + } + else if ( addMethod == null && + name.equals( "addActionListener" ) ) + { + addMethod = m; + } + } + + methods = new Method [] {setMethod, addMethod}; + _method_cache.put (componentClass, methods); + } + if (methods [0] != null) { + methods [0].invoke( aComponent, new Object[] { aKey } ); + } + if (methods [1] != null) { + methods [1].invoke( aComponent, new Object[] { this } ); + listenedToComponents.add( aComponent ); + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + System.out.println( "InfoPanel.introspectComponent: " + exc ); + } + } + +/** +* Called to populate a label component with the specified text. +* This implementation attempts to call setText(String) on the component. +* Override to customize. +*/ + protected void setText( Component c, String text ) + { + try + { + Method m = c.getClass().getMethod( "setText", new Class[] { String.class } ); + if ( m != null ) + { + m.invoke( c, new Object[] { text } ); + } + } + catch ( Exception exc ) + { + // no such method: ignore + } + } + +/** +* Creates spacer components on the reserved first grid row +* for each column of labels and fields. +* This allows us to set the margin for those label columns, +* and set the preferred width of the field columns. +* A list containing the field spacers should be assigned to +* the fieldSpacers instance variable. +*/ + private void createSpacers() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + // insert spacers for labels column + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridy = 0; + constraints.fill = GridBagConstraints.HORIZONTAL; + + fieldSpacers = new LinkedList(); + Component fieldSpacer; + for ( int i = 0; i < this.columns; i++ ) + { + constraints.gridx = i * 2; + listContainer.add( Box.createHorizontalStrut( this.margin ), constraints ); + + constraints.gridx = i * 2 + 1; + fieldSpacer = Box.createHorizontalStrut( 0 ); + fieldSpacers.add( fieldSpacer ); + listContainer.add( fieldSpacer, constraints ); + } + } + } + +/** +* Updates the insets for all components. +*/ + protected void updateGaps() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + Component c = null; + GridBagConstraints gbc = null; + double totalWeightY = 0.0; + int count = listContainer.getComponentCount(); + int i; + for ( i = 0; i < count; i++ ) + { + c = listContainer.getComponent( i ); + gbc = layout.getConstraints( c ); + totalWeightY += gbc.weighty; + if ( (gbc.gridx + 1) % ( this.columns * 2 ) == 0 ) + { // if last component in row + gbc.insets = new Insets( 0, 0, this.vgap, 0 ); + } + else + { + if ( gbc.gridx % 2 == 0 ) + { // is a label column - NOTE: uses eleven pixels before component, per l&f guide + gbc.insets = new Insets( 0, 0, this.vgap, 11 ); + } + else + { // is a component column + if ( gbc.gridy != 0 ) + { + if ( c instanceof JPanel ) ((JPanel)c).setPreferredSize( null ); + gbc.insets = new Insets( 0, 0, this.vgap, this.hgap ); + } + } + } + layout.setConstraints( c, gbc ); + } + + //hack: gridbag clumps components in center if weighty is zero + // if sum of weighty is zero, top-justify the list container + this.remove( listContainer ); + if ( totalWeightY == 0.0 ) + { + this.add( listContainer, BorderLayout.NORTH ); + } + else // put list container in center so it will grow + { + this.add( listContainer, BorderLayout.CENTER ); + } + } + } + +/** +* Updates the label alignment. +*/ + protected void updateLabels() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + Component c = null; + GridBagConstraints gbc = null; + Iterator it = labels.iterator(); + while ( it.hasNext() ) + { + c = (Component) it.next(); + if ( c instanceof JLabel ) + { + ((JLabel)c).setHorizontalAlignment( labelAlign ); + } + gbc = layout.getConstraints( c ); + gbc.anchor = this.labelAnchor; + layout.setConstraints( c, gbc ); + } + } + } + +/** +* Convenience method that uses a stock JTextField. +* @param key A string that will be displayed in a label, preferrably unique. +* @param value A string that will be displayed in a textfield. +*/ + public void addPair( String key, String value ) + { + addPair( key, value, null ); + } + +/** +* Convenience method that uses the specified JTextField or subclass +* and sets it to the specified value. +* @param key A string that will be displayed in a label, preferrably unique. +* @param value A string that will be displayed in a textfield. +* @param textField A JTextField or subclass that will be used to display the value. +* If null, a stock JTextField will be used. +*/ + public void addPair( String key, String value, JTextField textField ) + { + if ( value == null ) + { + value = ""; + } + JTextField field = textField; + if ( field == null ) + { + field = new JTextField( 15 ); // default to 15 columns + } + else + { + field = textField; + } + field.setText( value ); + + addPair( key, (Component) field ); + } + +/** +* Removes all components from the list. Buttons, if any, +* will remain unchanged - use setButtons( null ) to remove +* them. NOTE: does not call super.removeAll(). +*/ + public void removeAll() + { + Object component; + Method method; + Class[] paramClasses = new Class[] { ActionListener.class }; + Object[] paramObjects = new Object[] { this }; + + Iterator iterator = listenedToComponents.iterator(); + while ( iterator.hasNext() ) + { + component = iterator.next(); + try + { + method = component.getClass().getMethod( "removeActionListener", paramClasses ); + if ( method != null ) + { + method.invoke( component, paramObjects ); + } + } + catch ( Exception exception ) + { + // No removeActionListener() method, move along. + } + } + + listenedToComponents.clear(); + + listContainer.removeAll(); + fields.clear(); + labels.clear(); + this.revalidate(); + this.repaint(); + + //FIXME: It is very confusing that this + // implementation does not call super.removeAll(). + } + +/** +* Adds one or buttons to the bottom of the panel with the specified labels +* from left to right. Any action listeners will receive action events +* from clicks on these buttons - the supplied label will be used as the action command. +* @param buttons A string array containing the strings to be used for the button labels +* and action commands. A null value will remove the button panel. +* @see ButtonPanel +*/ + public void setButtons( String[] buttons ) + { + if ( buttonPanel == null ) + { + buttonPanel = new ButtonPanel(); + buttonPanel.setInsets( new Insets( 6, 0, 0, 0 ) ); + // button panel has a 11-pixel top inset + // and java l&f guide says 17-pixels before command buttons + buttonPanel.addActionListener( this ); + this.add( buttonPanel, BorderLayout.SOUTH ); + } + if ( buttons == null ) + { + this.remove( buttonPanel ); + buttonPanel = null; + } + else + { + buttonPanel.setLabels( buttons ); + } + + this.revalidate(); + this.repaint(); + } + protected Collection listenedToComponents = new LinkedList(); + +/** +* Retrieves the names of the buttons that are displayed, if any. +* @return A string array containing the strings used for the button labels +* and action commands, or null if no buttons have been created. +* @see ButtonPanel +*/ + public String[] getButtons() + { + if ( buttonPanel == null ) + { + return null; // none created + } + + return buttonPanel.getLabels(); + } + +/** +* Retrieves the actual button panel, if any. +* @return A button panel, or null if none has been created. +* @see ButtonPanel +*/ + public ButtonPanel getButtonPanel() + { + return buttonPanel; + } + + +/** +* Sets whether the values displayed in the panel should be editable. Defaults to true. +* @param isEditable Whether the values should be editable. +*/ + public void setEditable( boolean isEditable ) + { + this.isEditable = isEditable; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { + ( (Component) enumeration.next() ).setEnabled( isEditable ); + } + } + +/** +* Gets whether the values displayed in the panel are editable. +* @return Whether the values should be editable. +*/ + public boolean isEditable() + { + return this.isEditable; + } + +/** +* Sets the field associated with the key to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param key A string representing the key associated with the field. Nulls are converted to an empty string. +* @param value A object to be displayed in the specified field. Nulls are converted to an empty string. +*/ + public void setValueForKey( String key, Object value ) + { + setValueForKey( key, value, 0 ); + } + +/** +* Sets the field associated with the key to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param key A string representing the key associated with the field. Nulls are converted to an empty string. +* @param value A object to be displayed in the specified field. Nulls are converted to an empty string. +*/ + public void setValueForKey( String key, Object value, int index ) + { + if ( key == null ) + { + key = ""; + } + + Container field = null; + for ( int i = 0; i < fields.size(); i++ ) + { + field = (Container) fields.get(i); + if ( key.equals( field.getName() ) ) + { + setValueForIndex( index, i, value ); + return; + } + } + // else not found - ignore + } + +/** +* Sets the first field at the specified row index to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param row The row index of the component. +* @param value A object to be displayed in the specified field. +* Nulls are converted to an empty string. +*/ + public void setValueForIndex( int row, Object value ) + { + setValueForIndex( row, 0, value ); + } + +/** +* Sets the field at the specified row index and column index to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param row The row index of the component. +* @param index The column index of the component. +* @param value A object to be displayed in the specified field. +* Nulls are converted to an empty string. +*/ + public void setValueForIndex( int row, int col, Object value ) + { + Container field = (Container) fields.get( row ); + Component c = field.getComponent( col ); + setValueForComponent( c, value ); + } + + + +/** +* Sets the value in the field at the specified index. +* Note: If the component does not respond to setText() or setString() +* or setValue() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @param value A object to be displayed in the specified field. +*/ + protected void setValueForComponent( Component aComponent, Object value ) + { + // try to set a text or string property + try { + BeanInfo info = Introspector.getBeanInfo( aComponent.getClass() ); + MethodDescriptor[] methods = info.getMethodDescriptors(); + for ( int i = 0; i < methods.length; i++ ) + { + Method m = methods[i].getMethod(); + Class[] paramTypes = m.getParameterTypes(); + if ( paramTypes.length == 1 ) + { + if ( m.getName().equals( "setText" ) ) + { + if ( paramTypes[0].getName().equals( String.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + if ( m.getName().equals( "setString" ) ) + { + if ( paramTypes[0].getName().equals( String.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + if ( m.getName().equals( "setValue" ) ) + { + if ( paramTypes[0].getName().equals( Object.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + } + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + // FIXME: should log error in ErrorManager + System.out.println( "InfoPanel.setValueForComponent: " + exc ); + } + } + +/** +* Gets the value in the field at the specified index. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @return An object representing the value in the field at the specified index, +* or null if the component does not have a text property or if the index is out of bounds. +*/ + public Object getValueForIndex( int anIndex ) + { + return getValueForIndex( anIndex, 0 ); + } + +/** +* Gets the value in the field at the specified row and column. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @return An object representing the value in the field at the specified index, +* or null if the component does not have a text property or if the index is out of bounds. +*/ + public Object getValueForIndex( int row, int col ) + { + if ( ( row >= fields.size() ) || ( row < 0 ) ) + { // out of bounds + return null; + } + + Container field = (Container) fields.get( row ); + Component c = field.getComponent( col ); + return getValueForComponent( c ); + } + +/** +* Gets the value in the field associated with the key. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param key An string representing the key associated with the field. Nulls are converted to an empty string. +* @return An object representing the value in the field associated with the key, +* or null if the key does not exist or if the component does not have a text property. +*/ + public Object getValueForKey( String key ) + { + return getValueForKey( key, 0 ); + } + +/** +* Gets the value in the field associated with the key. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param key An string representing the key associated with the field. Nulls are converted to an empty string. +* @return An object representing the value in the field associated with the key, +* or null if the key does not exist or if the component does not have a text property. +*/ + public Object getValueForKey( String key, int index ) + { + if ( key == null ) + { + key = ""; + } + + Container field = null; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + field = (Container) enumeration.next(); + if ( key.equals( field.getName() ) ) + { + Component c = field.getComponent( index ); + if ( c != null ) + { + return getValueForComponent( c ); + } + } + } + // else not found + return null; + } + +/** +* Gets the value in the specified component. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param aComponent The specified component. +* @return An object representing the value in the component. +* or null if the component does not have a text property. +*/ + protected Object getValueForComponent( Component aComponent ) + { + // try to get a text or string property + try + { + BeanInfo info = Introspector.getBeanInfo( aComponent.getClass() ); + MethodDescriptor[] methods = info.getMethodDescriptors(); + for ( int i = 0; i < methods.length; i++ ) + { + Method m = methods[i].getMethod(); + Class[] paramTypes = m.getParameterTypes(); + if ( m.getName().equals( "getText" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + if ( m.getName().equals( "getString" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + if ( m.getName().equals( "getSelectedItem" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + // TODO: should also handle variants of setValue() + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + System.out.println( "InfoPanel.getValueFromComponent: " + exc ); + } + + // not found + return null; + } + +/** +* Gets the component associated with the key as a JTextField, for backwards compatibility. +* @param key A string representing the key associated with the component. Nulls are converted to an empty string. +* @return A JTextField that contains the value associated with the key, +* or null if the key does not exist or if the component is not a JTextField. +*/ + public JTextField getFieldForKey( String key ) + { + Component c = getComponentForKey( key ); + if ( c instanceof JTextField ) + { + return (JTextField) c; + } + return null; + } + +/** +* Gets the component associated with the key. If more than one component is associated +* with the key, returns the first such component. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + public Component getComponentForKey( String key ) + { + return getComponentForKey( key, 0 ); + } + +/** +* Gets the component associated with the key and index. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + public Component getComponentForKey( String key, int index ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return null; + return c.getComponent( index ); + } + +/** +* Gets the component at the specified row. If more than one component exists +* on that row, returns the first such component. +* @return A component or null if the row does not exist. +*/ + public Object getComponentForIndex( int row ) + { + return getComponentForIndex( row, 0 ); + } + +/** +* Gets the component at the specified row and column. +* @return A component or null if the index is out of bounds. +*/ + public Object getComponentForIndex( int row, int col ) + { + if ( ( row > fields.size() ) || ( row < 0 ) ) + { // out of bounds + return null; + } + + Container field = (Container) fields.get( row ); + return field.getComponent( col ); + } + +/** +* Gets the container associated with the key. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + protected Container getCompositeComponentForKey( String key ) + { + if ( key == null ) + { + key = ""; + } + + JPanel field = null; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + field = (JPanel) enumeration.next(); + if ( key.equals( field.getName() ) ) + { + return field; + } + } + + // else not found + return null; + } + +/** +* Provided for backwards compatibility: calls getLabelComponentForKey. +* @param key A string representing the key associated with the compoent. +* Nulls are converted to an empty string. +* @return Component label object associated with the key, or null if the key does not exist +* or if the label component is not an instance of JLabel. +*/ + public JLabel getLabelForKey( String key ) + { + Component result = getLabelComponentForKey( key ); + if ( result instanceof JLabel ) return (JLabel) result; + return null; + } + +/** +* Get the label component associated with the key. +* @param key A string representing the key associated with the compoent. +* Nulls are converted to an empty string. +* @return Component label object associated with the key, or null if the key does not exist. +*/ + public Component getLabelComponentForKey( String key ) + { + if ( key == null ) + { + key = ""; + } + + Component label = null; + Iterator enumeration = labels.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + label = (Component) enumeration.next(); + if ( key.equals( label.getName() ) ) + { + return label; + } + } + + // else not found + return null; + } + +/** +* Replaces the first component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param key A string representing the key to be associated with the component. +* Nulls are converted to an empty string. +* @param c A component to be placed next to the label corresponding to the key. +* Nulls are converted to a JTextField. +*/ + public void setComponentForKey( String key, Component c ) + { + setComponentForKey( key, c, 0 ); + } + +/** +* Replaces the component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param key A string representing the key to be associated with the component. +* Nulls are converted to an empty string. +* @param c A component to be placed next to the label corresponding to the key. +* Nulls are converted to a JTextField. +*/ + public void setComponentForKey( String key, Component c, int index ) + { + if ( c == null ) + { + c = new JTextField( 15 ); + } + if ( key == null ) + { + key = ""; + } + + Container container = this.getCompositeComponentForKey( key ); + Component field = container.getComponent( index ); + Object value = this.getValueForKey( key, index ); + if ( field != null ) + { + container.remove( index ); + container.add( c, index ); + c.setEnabled( this.isEditable ); + introspectComponent( c, key ); + setValueForComponent( c, value ); + } + } + +/** +* Replaces the first component in the specified row. Any value in the existing +* component will be copied to the new component. +* @param row A valid index. +* @param c A component to be placed next to the label corresponding to the key. +*/ + public void setComponentForIndex( int row, Component c ) + { + setComponentForIndex( row, 0, c ); + } + +/** +* Replaces the component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param row A valid index. +* @param c A component to be placed next to the label corresponding to the key. +*/ + public void setComponentForIndex( int row, int col, Component c ) + { + setComponentForKey( getLabels()[row], c, col ); + } + +/** +* Sets the string that appears before each label's text on the panel. +* @param aString A String to be used as the label prefix. +*/ + public void setLabelPrefix( String aString ) + { + prefix = aString; + setLabels( getLabels() ); // force refresh + } + +/** +* Gets the string that appears before each label's text on the panel. +* Defaults to "", an empty string. +* @return A String that is currently used as the label prefix. +*/ + public String getLabelPrefix() + { + return prefix; + } + +/** +* Sets the string that appears after each label's text on the panel. +* Defaults to ": ", a colon followed by a space. +* @param aString A String to be used as the label postfix. +*/ + public void setLabelPostfix( String aString ) + { + postfix = aString; + setLabels( getLabels() ); // force refresh + } + +/** +* Gets the string that appears after each label's text on the panel. +* @return A String that is currently used as the label postfix. +*/ + public String getLabelPostfix() + { + return postfix; + } + +/** +* Adds an action listener to the list that will be +* notified by events occurring in the panel. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by events occurring in the panel. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + + // interface ActionListener + +/** +* Called by buttons on panel and by other components that +* might be set to broadcast events to this listener. +* Simply forwards the action event unchanged. +* @param e An action event to be received. +*/ + public void actionPerformed(ActionEvent e) + { +// if ( e.getSource() instanceof AbstractButton ) +// { + broadcastEvent(e); +// } + } + + /** + * GridBagLayout allocates weightx only after considering + * the preferred width of the components in a column. + * We'd prefer that preferred width wasn't considered, + * so that the layout worked more like a html-table. + * GridBagLayout is poorly factored for subclassing, + * so this code is going to get a little bit ugly. + * Really, what good is a protected method that returns + * a private class? Would have liked to just override + * getLayoutInfo and be done with it. + */ + private class BetterGridBagLayout extends GridBagLayout + { + public Dimension preferredLayoutSize(Container parent) + { + preprocess(); + return super.preferredLayoutSize( parent ); + } + + public Dimension minimumLayoutSize(Container parent) + { + preprocess(); + return super.minimumLayoutSize( parent ); + } + + + public void layoutContainer(Container parent) + { + preprocess(); + super.layoutContainer( parent ); + } + + protected void preprocess() + { + if ( fieldSpacers == null ) return; + Iterator i; + + // find the field with the widest preferred size + Component c; + int maxWidth = 0; + i = fields.iterator(); + while ( i.hasNext() ) + { + c = (Component) i.next(); + maxWidth = Math.max( maxWidth, + Math.max( c.getPreferredSize().width, c.getMinimumSize().width ) ); + } + + // set each column's spacers to that preferred size + Dimension min = new Dimension( 0, 0 ); + Dimension pref = new Dimension( maxWidth, 0 ); + i = fieldSpacers.iterator(); + while ( i.hasNext() ) + { + ((Box.Filler)i.next()).changeShape( min, pref, pref ); + } + } + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java new file mode 100644 index 0000000..b73c74d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java @@ -0,0 +1,188 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.AWTEventMulticaster; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import javax.swing.Timer; + +/** +* KeyDelayTimer is a utility that listens for KeyEvents from one +* or more components. After receiving a KeyEvents the timer will +* broadcast an action event if a specified time interval passes without +* a subsequent KeyEvent.<BR><BR> +* +* This utility is useful for implementing any kind of auto-complete +* feature in a user interface. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class KeyDelayTimer implements ActionListener, KeyListener +{ + // delay timer for keypress-sensitve events + protected Timer keyTimer = null; + protected Component lastFieldTouched = null; + protected long timeLastFieldTouched = 0; + protected int interval = 400; // adjust as needed + + // for action multicasting + protected ActionListener actionListener = null; + +/** +* Default constructor. +*/ + public KeyDelayTimer() + { + keyTimer = new Timer( interval, this ); + } + +/** +* Convenience constructor. +* @param listener An action listener to be notified of delay events. +*/ + public KeyDelayTimer( ActionListener listener ) + { + this(); + addActionListener( listener ); + } + +/** +* Returns the last component that generated a KeyEvent. +* @return The component that sent the most recent KeyEvent. +*/ + public Component getComponent() + { + return lastFieldTouched; + } + +/** +* Returns the number of milliseconds before an ActionEvent is generated. +* The default is 400. +* @return The current delay interval in milliseconds. +*/ + public int getInterval() + { + return interval; + } + +/** +* Sets the number of milliseconds before an ActionEvent will be generated +* after a KeyEvent is received. +* @param millis The new delay interval in milliseconds. +*/ + public void setInterval( int millis ) + { + interval = millis; + keyTimer.setDelay( interval / 2 ); + } + + // interface KeyListener + + public void keyTyped(KeyEvent e) + { + } + public void keyPressed(KeyEvent e) + { + } + +/** +* Receives key events from one or more components. +* Records the component and the time this event was received, +* then starts the timer. +* @param e The key event in question. +*/ + public void keyReleased(KeyEvent e) + { // handles keystrokes in the textfields (except ENTER and ESCAPE) + if ( ( Character.isLetterOrDigit( e.getKeyChar() ) ) + || ( e.getKeyCode() == KeyEvent.VK_SPACE ) + || ( e.getKeyCode() == KeyEvent.VK_DELETE ) + || ( e.getKeyCode() == KeyEvent.VK_BACK_SPACE ) ) + { + this.lastFieldTouched = e.getComponent(); + this.timeLastFieldTouched = System.currentTimeMillis(); + this.keyTimer.start(); + return; + } + } + + // interface ActionListener + +/** +* Receives ActionEvents from the internal timer. +* If the interval has passed without another KeyEvent, +* an ActionEvent is broadcast, with the name of this class +* as the ActionCommand, and the internal timer is stopped. +* @param e The action event in question. +*/ + public void actionPerformed(ActionEvent e) + { + if ( e.getSource() == keyTimer ) + { + if ( System.currentTimeMillis() - this.timeLastFieldTouched > interval ) + { + this.keyTimer.stop(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, this.getClass().getName() ) ); + } + return; + } + } + + // Action Multicast methods + +/** +* Adds an action listener to the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java new file mode 100644 index 0000000..95b8a19 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java @@ -0,0 +1,350 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.io.Serializable; +import java.text.Format; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.border.LineBorder; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.table.TableCellEditor; + +/** +* A table cell editor customized for keyboard navigation, much like +* working with a spreadsheet. The default cell editor unfortunately +* does none of these things: +* <ul> +* <li> Selects text on start of editing. +* <li> Up and down keys move edit cell up and down. +* <li> Right and left keys move cell when selection caret is at end of text. +* <li> Escape cancels editing. +* <li> Enter commits edit. +* <li> Edits are properly committed on lost focus. +* <li> Tab and shift-tab work as expected. +* <li> Cell selection moves with the edit cell. +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class KeyableCellEditor implements TableCellEditor, FocusListener, + KeyListener, Serializable +{ + List listeners; + JTextField textField; + Object lastValue; + Format currentFormat; + + JTable table; + +/** +* Default constructor - a standard JTextField will be used for editing. +*/ + public KeyableCellEditor() + { + this( (JTextField) null ); + } + +/** +* Constructor specifying a type of JTextField to be used for editing. +* The JTextField will have its border replaced with a black line border. +* @param aTextField A JTextField or subclass for editing values. +*/ + public KeyableCellEditor( JTextField aTextField ) + { + listeners = new Vector(); + lastValue = null; + + // default to stock JTextField + textField = aTextField; + if ( textField == null ) + { + textField = new JTextField(); + } + + textField.setBorder(new LineBorder(Color.black)); + + // handle arrow keys while caret is showing + textField.addKeyListener( this ); + + // handle lost focus + textField.addFocusListener( this ); + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) + { + this.table = table; + table.removeKeyListener( this ); // if any + table.addKeyListener( this ); + return getEditorComponent( value ); + } + + protected Component getEditorComponent( Object value ) + { + if ( value != null ) + { + textField.setText( value.toString() ); + } + else + { + textField.setText( "" ); + } + + if ( value instanceof Number ) + { + textField.setHorizontalAlignment(JTextField.RIGHT); + } + else + { + textField.setHorizontalAlignment(JTextField.LEFT); + } + + // remember original value + lastValue = value; + + // select all text and get focus + textField.selectAll(); + textField.requestFocus(); + + return textField; + } + + public Object getCellEditorValue() + { + return lastValue; + } + + public boolean isCellEditable(EventObject anEvent) + { + // key events should replace the selection + // NOTE: For whatever reason, key events trigger result in a null parameter + if ( anEvent == null ) + { + textField.setText(""); + textField.requestFocus(); + return true; + } + + return true; + } + + public boolean shouldSelectCell(EventObject anEvent) + { // System.out.println( "KeyableCellEditor.shouldSelectCell: " + anEvent ); + + // key events should replace the selection + // NOTE: For whatever reason, key events are not generated + if ( anEvent instanceof KeyEvent ) + { + textField.setText(""); + textField.requestFocus(); + return true; + } + + // otherwise, select all text and continue + textField.selectAll(); + textField.requestFocus(); + + return true; + } + + public boolean stopCellEditing() + { + lastValue = textField.getText(); + fireEditingStopped(); + table.removeKeyListener( this ); // if any + return true; + } + + public void cancelCellEditing() + { + fireEditingCanceled(); + table.removeKeyListener( this ); // if any + } + + public void addCellEditorListener(CellEditorListener l) + { + listeners.add( l ); + } + + public void removeCellEditorListener(CellEditorListener l) + { + listeners.remove( l ); + } + + protected void fireEditingCanceled() + { + ChangeEvent event = new ChangeEvent( this ); + Iterator it = new ArrayList( listeners ).iterator(); // copy to prevent modification exception + while ( it.hasNext() ) + { + ((CellEditorListener)it.next()).editingCanceled( event ); + } + } + + protected void fireEditingStopped() + { + ChangeEvent event = new ChangeEvent( this ); + Iterator it = new ArrayList( listeners ).iterator(); // copy to prevent modification exception + while ( it.hasNext() ) + { + ((CellEditorListener)it.next()).editingStopped( event ); + } + } + + protected void onEnterKey() + { + stopCellEditing(); + } + + protected void onEscapeKey() + { + cancelCellEditing(); + } + + protected void moveEditCell( int dRow, int dCol ) + { + if ( table == null ) return; + int row = table.getSelectedRow() + dRow; + int col = table.getSelectedColumn() + dCol; + + row = Math.max( 0, row ); + row = Math.min( row, table.getRowCount() - 1 ); + col = Math.max( 0, col ); + col = Math.min( col, table.getColumnCount() - 1 ); + + stopCellEditing(); + table.setRowSelectionInterval( row, row ); + table.setColumnSelectionInterval( col, col ); + table.editCellAt( row, col ); + textField.selectAll(); + textField.requestFocus(); + } + + // interface KeyListener + + public void keyTyped(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyTyped: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + } + + public void keyPressed(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyPressed: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + + // catch LEFT and RIGHT here before JTextField consumes them + + int keyCode = e.getKeyCode(); + if ( keyCode == KeyEvent.VK_LEFT ) + { + if ( textField.getSelectionStart() == 0 ) + { + moveEditCell( 0, -1 ); + e.consume(); + return; + } + } + if ( keyCode == KeyEvent.VK_RIGHT ) + { + if ( textField.getSelectionEnd() == textField.getText().length() ) + { + moveEditCell( 0, 1 ); + e.consume(); + return; + } + } + if ( keyCode == KeyEvent.VK_UP ) + { + moveEditCell( -1, 0 ); + e.consume(); + return; + } + if ( keyCode == KeyEvent.VK_DOWN ) + { + moveEditCell( 1, 0 ); + e.consume(); + return; + } + } + + public void keyReleased(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyReleased: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + + // catch ENTER here to allow JTextField to process it as well + + int keyCode = e.getKeyCode(); + if ( keyCode == KeyEvent.VK_ENTER ) + { + onEnterKey(); + return; + } + if ( keyCode == KeyEvent.VK_ESCAPE ) + { + onEscapeKey(); + return; + } + + // tabs are apparently only received on key release + if ( keyCode == KeyEvent.VK_TAB ) + { + if ( e.isShiftDown() ) + { + moveEditCell( 0, -1 ); + } + else + { + moveEditCell( 0, 1 ); + } + e.consume(); + return; + } + + } + + // interface FocusListener + + public void focusGained(FocusEvent e) + { // System.out.println( "focusGained: " ); + } + + public void focusLost(FocusEvent e) + { // System.out.println( "focusLost: " ); + stopCellEditing(); + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java new file mode 100644 index 0000000..4a7f07e --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java @@ -0,0 +1,154 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Component; +import java.awt.Dimension; + +import javax.swing.JList; +import javax.swing.JViewport; +import javax.swing.ListCellRenderer; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** +* A list cell renderer that wraps its text to subsequent lines +* depending on the length of text string and the width of the +* parent list. +* +* This renderer depends on listening to the parent list's viewport +* and fixing the list's width to match the viewport's size. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @date $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 904 $ +*/ +public class LineWrappingRenderer extends MultiLineLabel + implements ListCellRenderer, ChangeListener +{ + protected static Border noFocusBorder; + + protected JList list; + protected JViewport viewport; + protected int preferredWidth; + +/** +* Required constructor. The renderer keeps a reference to +* the list in which it is used and its viewport. This list +* is the only list that may use this renderer. The renderer +* will use the current size of the list to determine where +* lines will initially break. +* @param containerList The list that will be using this renderer. +*/ + public LineWrappingRenderer( JList containerList ) + { + super(); + setLineWrap(true); + noFocusBorder = new EmptyBorder(1, 1, 1, 1); + + list = containerList; + preferredWidth = 400; + if ( list.getParent() instanceof JViewport ) + { + viewport = (JViewport) list.getParent(); + viewport.addChangeListener( this ); + int newWidth = viewport.getExtentSize().width; + if ( newWidth > 0 ) preferredWidth = newWidth; + } + else + { + // should function adequately in absence of a viewport + // System.err.println( "LineWrappingRenderer.init: list.getParent = " + list.getParent() ); + } + } + +/** +* Returns the preferred size of the label, with width +* constrained to the current width. +* @return the size +*/ + public Dimension getPreferredSize() + { + int width = getWidth(); + if ( width != preferredWidth ) + { + // if component has not yet been placed within the list + if ( width < list.getWidth() / 2 ) width = list.getWidth(); + preferredWidth = width; + } + return new Dimension( preferredWidth, super.getPreferredSize().height ); + } + +/** +* Returns this component with the width set to the +* width of the specified JList. +* @return this component. +*/ + public Component getListCellRendererComponent ( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus ) + { // System.out.println( "LineWrappingRenderer.getListCellRendererComponent:" ); + + if ( list != this.list ) + { + System.err.println( "LineWrappingRenderer.getListCellRendererComponent: " + + "warning: the list using the renderer is not the list specified in the constructor." ); + } + + if (isSelected) + { + setBackground(this.list.getSelectionBackground()); + setForeground(this.list.getSelectionForeground()); + } + else + { + setBackground(this.list.getBackground()); + setForeground(this.list.getForeground()); + } + + setText((value == null) ? "" : value.toString()); + + setEnabled(this.list.isEnabled()); + setFont(this.list.getFont()); + setBorder( (cellHasFocus) ? UIManager.getBorder("List.focusCellHighlightBorder") : noFocusBorder ); + + return this; + } + +/** +* Overridden to respond to viewport changes. +*/ + public void stateChanged(ChangeEvent e) + { + int newWidth = viewport.getExtentSize().width; + if ( newWidth > 0 ) preferredWidth = newWidth; + + // set fixed width on list + list.setFixedCellWidth( preferredWidth ); + setSize( preferredWidth, super.getSize().height ); + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java new file mode 100644 index 0000000..b5f8a9b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java @@ -0,0 +1,135 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.text.Highlighter; + +/** +* A custom JTextArea that looks and feels like a JLabel, but supports +* line wrapping. This works a lot more like the IFC label component. +* NOTE: doesn't support icons (yet). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @date $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 904 $ +*/ +public class MultiLineLabel extends JTextArea +{ + /** + * Saves a reference to the original highlighter + * to enable/disable text selection. + */ + protected Highlighter originalHighlighter; + +/* +* Creates a MultiLineLabel instance with an empty string for the title. +*/ + public MultiLineLabel() + { + super(); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + } + +/* +* Creates a MultiLineLabel instance with the specified text. +* @param text The specified text. +*/ + public MultiLineLabel( String text ) + { + super( text ); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + } + +/* +* Overridden to look like a label. +* @param text The specified text. +*/ + public void updateUI() + { + // got the implementation idea from usenet + + super.updateUI(); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + + // Set the text area's border, colors and font to + // that of a label + + LookAndFeel.installBorder(this, "Label.border"); + + LookAndFeel.installColorsAndFont(this, + "Label.background", + "Label.foreground", + "Label.font"); + } + +/** +* Sets whether text is selectable. +* Default is non-selectable text. +*/ + public void setSelectable( boolean selectable ) + { + if ( selectable ) + { + setHighlighter( originalHighlighter ); + } + else + { + originalHighlighter = getHighlighter(); + setHighlighter( null ); + } + } + +/** +* Gets whether text is selectable. +* Default is non-selectable text. +*/ + public boolean isSelectable() + { + return ( getHighlighter() != null ); + } + +/** +* Overridden to return false. +*/ + public boolean isFocusTraversable() + { + return false; + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java new file mode 100644 index 0000000..b3d2d03 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java @@ -0,0 +1,434 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc + +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.components; + +/** +* NumericTextField is a "smart" text field that restricts the user's input. The +* input is restructed to numeric only wich can be of two types: integer and real +* numbers. A range can also be placed on the text field. The default type is +* integer, with the being (Integer.MIN_VALUE, Integer.MAX_VALUE). +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NumericTextField extends SmartTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Restrict the text input to integers (whole numbers) only. +*/ + public final static int INTEGER = 0; + +/** +* Restrict the text input to floating-point numbers only. +*/ + public final static int FLOAT = 1; + + private Number maximumValue = null; + private Number minimumValue = null; + + private boolean sign = false; + private int newCaretPosition = 0; + private int valueType = INTEGER; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default constructor. +*/ + public NumericTextField() + { + this("", 0); + } + +/** +* Constructor. +* @param text The initial string the text field is set to. +*/ + public NumericTextField(String text) + { + this(text, 0); + } + +/** +* Constructor. +* @param columns Width of the text field (in characters). +*/ + public NumericTextField(int columns) + { + this("", columns); + } + +/** +* Constructor. +* @param text The initial string the text field is set to. +* @param columns Width of the text field (in characters). +*/ + public NumericTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Sets the upper limit of the range of numbers to accept. +* @param newMaximumValue The maximum number accepted by the text field. +*/ + public void setMaximumValue(double newMaximumValue) + { + if (newMaximumValue >= 0) + { + maximumValue = new Double( newMaximumValue ); + } + else + { + maximumValue = null; + } + } + +/** +* Returns the upper limit of the range of numbers to accept. +* @return The maximum number accepted by this text field. +*/ + public double getMaximumValue() + { + if ( valueType == INTEGER ) + { + return (maximumValue == null) ? (double) Integer.MAX_VALUE : maximumValue.doubleValue(); + } + else + { + return (maximumValue == null) ? Double.MAX_VALUE : maximumValue.doubleValue(); + } + } + +/** +* Sets the lower limit of the range of numbers to accept. +* @param newMinimumValue The minimum number accepted by the text field. +*/ + public void setMinimumValue(double newMinimumValue) + { + if (newMinimumValue <= 0) + { + minimumValue = new Double( newMinimumValue ); + } + else + { + minimumValue = null; + } + } + +/** +* Returns the lower limit of the range of numbers to accept. +* @return The minimum number accepted by this text field. +*/ + public double getMinimumValue() + { + if ( valueType == INTEGER ) + { + return (minimumValue == null) ? (double) Integer.MIN_VALUE : minimumValue.doubleValue(); + } + else + { + return (minimumValue == null) ? -1.0*Double.MAX_VALUE : minimumValue.doubleValue(); + // NOTE: Double.MIN_VALUE returns the smallest positive value - oooops. + } + } + +/** +* Sets which type of number this text field can accept. +* @see #INTEGER +* @see #FLOAT +* @param newValueType The type of number to accept. +*/ + public void setValueType(int newValueType) + { + if ((newValueType != INTEGER) && (newValueType != FLOAT)) + { + valueType = INTEGER; + } + else + { + valueType = newValueType; + } + } + +/** +* Returns which type of number this text field accepts. The default is +* integer. +* @see #INTEGER +* @see #FLOAT +* @return The type of number to accept. +*/ + public int getValueType() + { + return valueType; + } + +/** +* Returns the integer numeric value of the string in the text field. The type +* can be either integer of float. +* @return The current value in the text field. +*/ + public int getIntValue() + { + int value = 0; + + try + { + value = Integer.parseInt(getText()); + } + catch (NumberFormatException e) + { + try + { + Double dValue = Double.valueOf(getText()); + value = dValue.intValue(); + } + catch (NumberFormatException ignored) {} + } + + return value; + } + +/** +* Sets the text field to integer value specified. +* @param aValue An integer value to display in the text field. +*/ + public void setIntValue(int aValue) + { + setText(Integer.toString(aValue)); + } + +/** +* Returns the real number numeric value of the string in the text field. The type +* can be either integer of float. +* @return The current value in the text field. +*/ + public double getDoubleValue() + { + Double value = new Double(0); + + try + { + value = Double.valueOf(getText()); + } + catch (NumberFormatException ignored) {} + + return value.doubleValue(); + } + +/** +* Sets the text field to the double value specified. If the text field type is +* FLOAT then the the number is display as a real number. If the text field +* type is INTEGER then the number is converted to a whole number for displaying. +* @param aValue A double value to display in the text field. +*/ + public void setDoubleValue(double aValue) + { + Double temp = new Double(aValue); + + if (valueType == FLOAT) + { + setText(temp.toString()); + } + else + { + setText(Integer.toString(temp.intValue())); + } + } + +/******************************* +* PROTECTED METHODS +*******************************/ + + protected boolean isValidCharacter(char aChar) + { + if (((aChar >= ' ') && (aChar <= '/')) || ((aChar >= ':') && (aChar <= '~'))) + { + if (aChar == '.') + { + if ( valueType == FLOAT ) + { + return true; + } + else + { + return false; + } + } + else if (aChar == '-') + { + if ( getMinimumValue() < 0 ) + { + return true; + } + else + { + return false; + } + } + else if (aChar == '+') + { + if ( getMaximumValue() >= 0 ) + { + return true; + } + else + { + return false; + } + } + return false; + } + return true; + } + + protected boolean isValidString(String aString) + { + int iValue = 0; + double dValue = 0.0; + + String tempString = new String(scanForSignChar(aString)); + + if ( valueType == INTEGER ) + { + try + { + iValue = Integer.parseInt(tempString); + } + catch (NumberFormatException e1) + { + if ((tempString.compareTo("-") == 0) && (getMinimumValue() < 0.0)) + { + iValue = 0; + } + else + { + return false; + } + } + if ((((double)iValue) < getMinimumValue()) || (((double)iValue) > getMaximumValue())) + { + return false; + } + } + else + { + // Double.valueOf requires a zero before the decimal point + if ( tempString.startsWith( "." ) ) + { + tempString = "0" + tempString; + } + try + { + dValue = Double.valueOf(tempString).doubleValue(); + } + catch (NumberFormatException e2) + { + if ((tempString.compareTo("-") == 0) && (getMinimumValue() < 0.0)) + { + dValue = 0.0; + } + else + { + return false; + } + } + + if ((dValue < getMinimumValue()) || (dValue > getMaximumValue())) + { + return false; + } + } + + return true; + } + + protected void postProcessing() + { + if (sign) + { + setText(scanForSignChar(getText())); + setCaretPosition(newCaretPosition); + } + sign = false; + } + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private String scanForSignChar(String aString) + { + String newString = ""; + boolean positive = false; + boolean negative = false; + int oldCaretPosition = getCaretPosition(); + int charactersAdded = 0; + + newCaretPosition = 0; + + if (aString.length() <= 0) + { + return aString; + } + + for (int i = 0; i < aString.length(); ++i) + { + switch (aString.charAt(i)) + { + case '+': positive = true; + break; + case '-': negative = true; + break; + default: newString += aString.charAt(i); + charactersAdded++; + break; + } + + if ((i + 1) == oldCaretPosition) + { + newCaretPosition = charactersAdded; + } + } + + if ((!(positive)) && (negative)) + { + newString = "-" + newString; + newCaretPosition++; + } + + if (positive || negative) + { + sign = true; + } + + return newString; + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java new file mode 100644 index 0000000..9db2834 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java @@ -0,0 +1,572 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Vector; + +import javax.swing.BorderFactory; +import javax.swing.DefaultCellEditor; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; + +/** +* PropertyEditorTable is a table designed to display and edit the properties +* of an object. Because JTable assumes all cells in a column display +* the same data type, we have to subclass to determine the class +* based on the cell contents. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PropertyEditorTable extends JTable { + +// +// Constructors +// + + /** + * Constructs a default JTable which is initialized with a default + * data model, a default column model, and a default selection + * model. + * + * @see #createDefaultDataModel + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable() { + super(null, null, null); + } + + /** + * Constructs a JTable which is initialized with <i>dm</i> as the + * data model, a default column model, and a default selection + * model. + * + * @param dm The data model for the table + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm) { + super(dm, null, null); + } + + /** + * Constructs a JTable which is initialized with <i>dm</i> as the + * data model, <i>cm</i> as the column model, and a default selection + * model. + * + * @param dm The data model for the table + * @param cm The column model for the table + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm, TableColumnModel cm) { + super(dm, cm, null); + } + + /** + * Constructs a JTable which is initialized with <i>dm</i> as the + * data model, <i>cm</i> as the column model, and <i>sm</i> as the + * selection model. If any of the parameters are <b>null</b> this + * method will initialize the table with the corresponding + * default model. The <i>autoCreateColumnsFromModel</i> flag is set + * to false if <i>cm</i> is non-null, otherwise it is set to true + * and the column model is populated with suitable TableColumns + * for the columns in <i>dm</i>. + * + * @param dm The data model for the table + * @param cm The column model for the table + * @param sm The row selection model for the table + * @see #createDefaultDataModel + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) { + super( dm, cm, sm ); + } + + /** + * Constructs a JTable with <i>numRows</i> and <i>numColumns</i> of + * empty cells using the DefaultTableModel. The columns will have + * names of the form "A", "B", "C", etc. + * + * @param numRows The number of rows the table holds + * @param numColumns The number of columns the table holds + */ + public PropertyEditorTable(int numRows, int numColumns) { + super( numRows, numColumns ); + } + + /** + * Constructs a JTable to display the values in the Vector of Vectors, + * <i>rowData</i>, with column names, <i>columnNames</i>. + * The Vectors contained in <i>rowData</i> should contain the values + * for that row. In other words, the value of the cell at row 1, + * column 5 can be obtained with the following code: + * <p> + * <pre>((Vector)rowData.elementAt(1)).elementAt(5);</pre> + * <p> + * All rows must be of the same length as <i>columnNames</i>. + * <p> + * @param rowData The data for the new table + * @param columnNames Names of each column + */ + public PropertyEditorTable(final Vector rowData, final Vector columnNames) { + super( rowData, columnNames ); + } + + /** + * Constructs a JTable to display the values in the two dimensional array, + * <i>rowData</i>, with column names, <i>columnNames</i>. + * <i>rowData</i> is an Array of rows, so the value of the cell at row 1, + * column 5 can be obtained with the following code: + * <p> + * <pre> rowData[1][5]; </pre> + * <p> + * All rows must be of the same length as <i>columnNames</i>. + * <p> + * @param rowData The data for the new table + * @param columnNames Names of each column + */ + public PropertyEditorTable(final Object[][] rowData, final Object[] columnNames) { + super( rowData, columnNames ); + } + + /** + * Returns the type of the column at the specified view position. + * + * @return the type of the column at position <I>column</I> in the view + * where the first column is column 0. + * + * Modified mln: now a wrapper for getCellClass() + * + */ + public Class getColumnClass(int column) { + return getCellClass( 0, column ); + } + + /** + * Returns the type of the cell at the specified view position. + * + * @return the type of the cell at position <I>row</I>, <I>column</I> in the view + * where the first column is column 0. + * + * Modified mln: new methods + * + */ + public Class getCellClass(int row, int column) { + TableModel model = getModel(); + if ( model instanceof PropertyEditorTableModel ) + return ((PropertyEditorTableModel)model).getCellClass( row, column ); + else + return model.getColumnClass(convertColumnIndexToModel(column)); + } + + /** + * Return an appropriate renderer for the cell specified by this this row and + * column. If the TableColumn for this column has a non-null renderer, return that. + * If not, find the class of the data in this column (using getColumnClass()) + * and return the default renderer for this type of data. + * + * @param row the row of the cell to render, where 0 is the first + * @param column the column of the cell to render, where 0 is the first + * + * Modified mln: calls getCellClass if there's no column model + * + */ + public TableCellRenderer getCellRenderer(int row, int column) { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellRenderer renderer = tableColumn.getCellRenderer(); + if (renderer == null) { + renderer = getDefaultRenderer(getCellClass(row, column)); + } + return renderer; + } + + + /** + * Return an appropriate editor for the cell specified by this this row and + * column. If the TableColumn for this column has a non-null editor, return that. + * If not, find the class of the data in this column (using getColumnClass()) + * and return the default editor for this type of data. + * + * @param row the row of the cell to edit, where 0 is the first + * @param column the column of the cell to edit, where 0 is the first + * + * Modified mp: calls getCellClass if there's no column model + * + */ + public TableCellEditor getCellEditor(int row, int column) { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellEditor editor = tableColumn.getCellEditor(); + if (editor == null) { + editor = getDefaultEditor(getCellClass(row, column)); + } + return editor; + } + + protected void createDefaultRenderers() { + super.createDefaultRenderers(); + +/* // copying this code here as a sample of creating a renderer + // Dates + DefaultTableCellRenderer dateRenderer = new DefaultTableCellRenderer() { + DateFormat formatter = DateFormat.getDateInstance(); + public void setValue(Object value) { + setText((value == null) ? "" : formatter.format(value)); } + }; + dateRenderer.setHorizontalAlignment(JLabel.RIGHT); + setDefaultRenderer(Date.class, dateRenderer); +*/ + + DefaultTableCellRenderer fontRenderer = new DefaultTableCellRenderer() { + public void setValue(Object value) { + setText( getFontDescription( (Font) value ) ); + } + }; + fontRenderer.setHorizontalAlignment(JLabel.RIGHT); + setDefaultRenderer(Font.class, fontRenderer); + + setUpColorRenderer( this ); + setUpMethodRenderer( this ); + } + + protected String getFontDescription( Font f ) { + String s; + if ( f != null ) { + s = f.getName(); + if ( f.isBold() ) s += " Bold"; + if ( f.isItalic() ) s += " Italic"; + s += " " + f.getSize(); + } else { + s = ""; + } + return s; + } + + protected void createDefaultEditors() { + super.createDefaultEditors(); + +/* // copying this code here as a sample of creating an editor + // Numbers + JTextField rightAlignedTextField = new JTextField(); + rightAlignedTextField.setHorizontalAlignment(JTextField.RIGHT); + rightAlignedTextField.setBorder(new LineBorder(Color.black)); + setDefaultEditor(Number.class, new DefaultCellEditor(rightAlignedTextField)); + + // Booleans + JCheckBox centeredCheckBox = new JCheckBox(); + centeredCheckBox.setHorizontalAlignment(JCheckBox.CENTER); + setDefaultEditor(Boolean.class, new DefaultCellEditor(centeredCheckBox)); +*/ + setUpColorEditor( this ); + setUpMethodEditor( this ); + } + + + // following code lifted from: + // http://java.sun.com/docs/books/tutorial/ui/swing/example-swing/TableDialogEditDemo.java + + class ColorRenderer extends JLabel + implements TableCellRenderer { + Border unselectedBorder = null; + Border selectedBorder = null; + boolean isBordered = true; + + public ColorRenderer(boolean isBordered) { + super(); + this.isBordered = isBordered; + this.setOpaque(true); //MUST do this for background to show up. + } + + public Component getTableCellRendererComponent( + JTable table, Object color, + boolean isSelected, boolean hasFocus, + int row, int column) { + this.setBackground((Color)color); + if (isBordered) { + if (isSelected) { + if (selectedBorder == null) { + selectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getSelectionBackground()); + } + this.setBorder(selectedBorder); + } else { + if (unselectedBorder == null) { + unselectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getBackground()); + } + this.setBorder(unselectedBorder); + } + } + return this; + } + } + + private void setUpColorRenderer(JTable table) { + table.setDefaultRenderer(Color.class, + new ColorRenderer(true)); + } + + //Set up the editor for the Color cells. + private void setUpColorEditor(JTable table) { + //First, set up the button that brings up the dialog. + final JButton button = new JButton("") { + public void setText(String s) { + //Button never shows text -- only color. + } + }; + button.setBackground(Color.white); + button.setBorderPainted(false); + button.setMargin(new Insets(0,0,0,0)); + + //Now create an editor to encapsulate the button, and + //set it up as the editor for all Color cells. + final ColorEditor colorEditor = new ColorEditor(button); + table.setDefaultEditor(Color.class, colorEditor); + + //Set up the dialog that the button brings up. + final JColorChooser colorChooser = new JColorChooser(); + //XXX: PENDING: add the following when setPreviewPanel + //XXX: starts working. + //JComponent preview = new ColorRenderer(false); + //preview.setPreferredSize(new Dimension(50, 10)); + //colorChooser.setPreviewPanel(preview); + ActionListener okListener = new ActionListener() { + public void actionPerformed(ActionEvent e) { + colorEditor.currentColor = colorChooser.getColor(); + } + }; + final JDialog dialog = JColorChooser.createDialog(button, + "Pick a Color", + true, + colorChooser, + okListener, + null); //XXXDoublecheck this is OK + + //Here's the code that brings up the dialog. + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + button.setBackground(colorEditor.currentColor); + colorChooser.setColor(colorEditor.currentColor); + //Without the following line, the dialog comes up + //in the middle of the screen. + //dialog.setLocationRelativeTo(button); + dialog.show(); + } + }); + } + + /* + * The editor button that brings up the dialog. + * We extend DefaultCellEditor for convenience, + * even though it mean we have to create a dummy + * check box. Another approach would be to copy + * the implementation of TableCellEditor methods + * from the source code for DefaultCellEditor. + */ + class ColorEditor extends DefaultCellEditor { + Color currentColor = null; + + public ColorEditor(JButton b) { + super(new JCheckBox()); //Unfortunately, the constructor + //expects a check box, combo box, + //or text field. + editorComponent = b; + setClickCountToStart(1); //This is usually 1 or 2. + + //Must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + fireEditingStopped(); + } + }); + } + + protected void fireEditingStopped() { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() { + return currentColor; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) { + ((JButton)editorComponent).setText(value.toString()); + currentColor = (Color)value; + return editorComponent; + } + } + + class MethodRenderer extends JLabel + implements TableCellRenderer { + + Method theMethod = null; + JTable theTable = null; + + public MethodRenderer() { + super(); + } + + public Component getTableCellRendererComponent( + JTable table, Object method, + boolean isSelected, boolean hasFocus, + int row, int column) { + theMethod = (Method) method; + theTable = table; + setText( " " + theMethod.getReturnType().getName() ); + return this; + } + } + + private void setUpMethodRenderer(JTable table) { + table.setDefaultRenderer(Method.class, + new MethodRenderer()); + } + + /* + * We extend DefaultCellEditor for convenience, + * as with ColorEditor. + */ + class MethodEditor extends DefaultCellEditor { + Method theMethod = null; + JTable theTable = null; + + public MethodEditor(JButton b) { + super(new JCheckBox()); //Unfortunately, the constructor + //expects a check box, combo box, + //or text field. + editorComponent = b; + setClickCountToStart(1); //This is usually 1 or 2. + + //Must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + fireEditingStopped(); + } + }); + } + + protected void fireEditingStopped() { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() { + return theMethod; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) { + ((JButton)editorComponent).setText(value.toString()); + theMethod = (Method)value; + theTable = table; + return editorComponent; + } + } + + //Set up the editor for the Method cells. + private void setUpMethodEditor(PropertyEditorTable table) { + //First, set up the button that brings up the dialog. + final JButton button = new JButton("invoking method") { + public void setText(String s) { + //Button never shows text -- only color. + } + }; + button.setBackground(Color.white); + button.setBorderPainted(false); + button.setMargin(new Insets(0,0,0,0)); + + //Now create an editor to encapsulate the button, and + //set it up as the editor for all Color cells. + final MethodEditor methodEditor = new MethodEditor(button); + table.setDefaultEditor(Method.class, methodEditor); + + // handle the button-click + final PropertyEditorTable theTable = table; + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + Component parent = SwingUtilities.getRoot( theTable ); + if ( parent == null ) parent = theTable; + + Cursor oldCursor = parent.getCursor(); + parent.setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) ); + + Object result = null; + Object inspectedObject = ((PropertyEditorTableModel) + methodEditor.theTable.getModel()).inspectedObject; + try + { + methodEditor.theMethod.setAccessible( true ); + result = methodEditor.theMethod.invoke( + inspectedObject, (Object[])null ); + } + catch ( Exception exc ) + { + System.err.println( "PropertyEditorTable.MethodRenderer.actionPerformed: " + + "Error occurred: " + exc ); + } + theTable.methodInvoked( inspectedObject, methodEditor.theMethod, result ); + + parent.setCursor( oldCursor ); + } + }); + } + +/** +* Called by the method cell editor when a method is invoked. +* @param anObject The object upon which the method was invoked. +* @param aMethod The method that was invoked. +* @param aResult The result of the method invocation; may be null. +*/ + public void methodInvoked( Object anObject, Method aMethod, Object aResult ) + { + System.out.println( aMethod.getName() + ": " + aResult ); + } +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java new file mode 100644 index 0000000..f6a2a8d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java @@ -0,0 +1,418 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.Vector; + +import javax.swing.Timer; +import javax.swing.table.AbstractTableModel; + +/** +* PropertyEditorTableModel introspects an object to facilitate +* editing it in a PropertyTable. +* +* Because the model always reflects the current state of the +* inspected object, it is useful to have a table update at +* automated intervals. By default, this feature is turned off. +* If you turn it on, you'll want to remember to turn it off +* when you're done with the table model. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PropertyEditorTableModel extends AbstractTableModel implements ActionListener +{ + protected Object inspectedObject = null; + + final String[] columnNames = { "Property", "Value" }; + static final String METHOD_TAG = " "; + + Vector properties = new Vector(); + Hashtable methods = new Hashtable(0); + + public void setObject( Object o ) { + inspectedObject = o; + Class c = o.getClass(); + Method[] m = c.getMethods(); + + properties = new Vector(); + methods = new Hashtable(m.length, 1F); + String name, propertyName; + for ( int i = 0; i < m.length; i++ ) { +// if ( m[i].getName().startsWith( "get" ) ) +// System.out.println( m[i].getName() + ": " + m[i].getReturnType().getName() ); + + // get methods + if ( + ( ( m[i].getName().startsWith( "get" ) ) || ( m[i].getName().startsWith( "is" ) ) ) + && ( m[i].getParameterTypes().length == 0 ) + ) { + name = m[i].getName(); + if ( m[i].getName().startsWith( "is" ) ) { + propertyName = name.substring( 2, name.length() ); + // probably should only add "is" methods if accompanied by "set" method + } else { // "get" + propertyName = name.substring( 3, name.length() ); + } + if ( + ( m[i].getReturnType().getName().equals( String.class.getName() ) ) + || ( m[i].getReturnType().getName().equals( "boolean" ) ) + || ( m[i].getReturnType().getName().equals( "int" ) ) + || ( m[i].getReturnType().getName().equals( "float" ) ) + || ( m[i].getReturnType().getName().equals( "char" ) ) + || ( m[i].getReturnType().getName().equals( Color.class.getName() ) ) + || ( m[i].getReturnType().getName().equals( Font.class.getName() ) ) + ) { + properties.addElement( propertyName ); // put property + methods.put( name, m[i] ); // put method + } + else + { + // handle unknown types as invokable methods: + properties.addElement( propertyName + METHOD_TAG ); + methods.put( propertyName + METHOD_TAG, m[i] ); + } + } + else + // set methods + if ( ( m[i].getName().startsWith( "set" ) ) && + ( m[i].getParameterTypes().length == 1 ) ) { + name = m[i].getName(); + if ( + ( m[i].getParameterTypes()[0].getName().equals( String.class.getName() ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "boolean" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "int" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "float" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( Color.class.getName() ) ) +// || ( m[i].getParameterTypes()[0].getName().equals( Font.class.getName() ) ) + ) { +// System.out.println( "Accepted: " + name + ": " + m[i].getParameterTypes()[0].getName() ); + methods.put( name, m[i] ); // set method + } else { +// System.out.println( "Rejected: " + name + ": " + m[i].getParameterTypes()[0].getName() ); + } + } + else + // zero-parameter methods to be invoked on click + if ( m[i].getParameterTypes().length == 0 ) + { + properties.addElement( m[i].getName() + METHOD_TAG ); + methods.put( m[i].getName() + METHOD_TAG, m[i] ); + } + + } + + sort(properties); + fireTableDataChanged(); + } + + public int getColumnCount() { + return columnNames.length; + } + + public int getRowCount() { + return properties.size(); + } + + public String getColumnName(int col) { + return columnNames[col]; + } + + public Object getValueAt(int row, int col) { + if ( col == 0 ) + return properties.elementAt( row ); + else + { + Method m = null; + m = (Method) methods.get( "get" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) // try "is" + m = (Method) methods.get( "is" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) { // try entire method name + m = (Method) methods.get( (String) properties.elementAt( row ) ) ; + if ( m != null ) return m; + } + try { + return m.invoke( inspectedObject, (Object[])null ); + } catch ( Exception exc ) { + System.out.println( "InspectorFrame.tableModel.getValueAt: error occured while reflecting: " ); + System.out.println( exc ); + } + return null; + } + } + + public Class getColumnClass( int col ) { +// System.out.println( "getColumnClass" ); +/* try { + throw new Exception(); + } catch ( Exception exc ) { + exc.printStackTrace( System.out ); + } +*/ return new String().getClass(); + } + + public Class getCellClass(int row, int col) { +// System.out.println( "getCellClass" ); +/* + + Class c; + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) + c = new Object().getClass(); + else { + c = m.getParameterTypes()[0]; + + // special case for boolean + if ( c.getName().equals( "boolean" ) ) + c = new Boolean(true).getClass(); + + // special case for int + if ( c.getName().equals( "int" ) ) + c = new Integer(0).getClass(); + } + System.out.println( row + ": " + c.getName() ); + return c; +*/ + +// return new String().getClass(); + + Object o = getValueAt( row, col ); + if ( o == null ) o = "null"; + return o.getClass(); + } + + /* + * Don't need to implement this method unless your table's + * editable. + */ + public boolean isCellEditable(int row, int col) { + //Note that the data/cell address is constant, + //no matter where the cell appears onscreen. + if (col < 1) { + return false; + } else { + // handle method invocation + if ( ((String)properties.elementAt(row)).endsWith(METHOD_TAG) ) return true; + // handle read-only properties + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) + return false; + else + return true; + } + } + + /* + * Don't need to implement this method unless your table's + * data can change. + */ + public void setValueAt(Object value, int row, int col) { + // test for inspected object + if ( inspectedObject == null ) return; + // handle method invocation - no need to update values + if ( ((String)properties.elementAt(row)).endsWith( METHOD_TAG ) ) + { + fireTableDataChanged(); + return; + }; + + // handle writable properties + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + String parameterType = m.getParameterTypes()[0].getName(); + + // ugly cast code + if ( + ( parameterType.equals( "int" ) ) + || ( parameterType.equals( "java.lang.Integer" ) ) + ) + { + try { + value = new Integer((String)value); + } catch (NumberFormatException e) { + System.out.println("PropertyEditorTableModel.setValueAt: User attempted to enter non-integer" + + " value (" + value + + ") into an integer-only column."); + } + } + Object[] parameters = { value }; + try { + m.invoke( inspectedObject, parameters ); + if ( inspectedObject instanceof Component ) { + Component c = (Component)inspectedObject; + if ( c.getParent() != null ) + c.getParent().repaint(); + } + } catch ( Exception exc ) { + System.out.println( "PropertyEditorTableModel.setValueAt: error occured while reflecting: " ); + System.out.println( exc ); + } + + fireTableDataChanged(); + } + + + protected void sort(Vector v){ + quickSort(v, 0, v.size()-1); + } + + + // Liberated from the BasicDirectoryModel which was... + // Liberated from the 1.1 SortDemo + + // This is a generic version of C.A.R Hoare's Quick Sort + // algorithm. This will handle arrays that are already + // sorted, and arrays with duplicate keys.<BR> + // + // If you think of a one dimensional array as going from + // the lowest index on the left to the highest index on the right + // then the parameters to this function are lowest index or + // left and highest index or right. The first time you call + // this function it will be with the parameters 0, a.length - 1. + // + // @param a an integer array + // @param lo0 left boundary of array partition + // @param hi0 right boundary of array partition + private void quickSort(Vector v, int lo0, int hi0) { + int lo = lo0; + int hi = hi0; + String mid; + + if (hi0 > lo0) { + // Arbitrarily establishing partition element as the midpoint of + // the array. + mid = (String) v.elementAt((lo0 + hi0) / 2); + + // loop through the array until indices cross + while(lo <= hi) { + // find the first element that is greater than or equal to + // the partition element starting from the left Index. + // + // Nasty to have to cast here. Would it be quicker + // to copy the vectors into arrays and sort the arrays? + while((lo < hi0) && lt((String)v.elementAt(lo), mid)) { + ++lo; + } + + // find an element that is smaller than or equal to + // the partition element starting from the right Index. + while((hi > lo0) && lt(mid, (String)v.elementAt(hi))) { + --hi; + } + + // if the indexes have not crossed, swap + if(lo <= hi) { + swap(v, lo, hi); + ++lo; + --hi; + } + } + + + // If the right index has not reached the left side of array + // must now sort the left partition. + if(lo0 < hi) { + quickSort(v, lo0, hi); + } + + // If the left index has not reached the right side of array + // must now sort the right partition. + if(lo < hi0) { + quickSort(v, lo, hi0); + } + + } + } + + private void swap(Vector a, int i, int j) { + Object T = a.elementAt(i); + a.setElementAt(a.elementAt(j), i); + a.setElementAt(T, j); + } + + protected boolean lt(String a, String b) { + return a.compareTo(b) < 0; + } + + // automated updates + + private boolean autoUpdating = false; + private int updateInterval = 2000; // one-second delay on average + protected Timer timer = null; + + public boolean isAutoUpdating() + { + return autoUpdating; + } + + public void setAutoUpdating( boolean shouldAutoUpdate ) + { + if ( shouldAutoUpdate ) + { + if ( timer == null ) + { + timer = new Timer( updateInterval, this ); + timer.setRepeats( true ); + timer.start(); + } + } + else + { + if ( timer != null ) + { + timer.stop(); + timer = null; + } + } + + autoUpdating = shouldAutoUpdate; + } + + public int getUpdateInterval() + { + return updateInterval; + } + + public void setUpdateInterval( int anInterval ) + { + if ( timer != null ) + { + timer.setDelay( anInterval ); + } + + updateInterval = anInterval; + } + + public void actionPerformed( ActionEvent evt ) + { + if ( evt.getSource() == timer ) + { + fireTableDataChanged(); + } + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java new file mode 100644 index 0000000..2956c71 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java @@ -0,0 +1,174 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.Component; + +import javax.swing.AbstractButton; +import javax.swing.ButtonGroup; +import javax.swing.JRadioButton; +import javax.swing.border.EmptyBorder; + +/** +* RadioButtonPanel is a simple extension of ButtonPanel. +* Differences are that it uses radio buttons and the +* default alignment is vertical. The radio buttons are +* placed in a ButtonGroup and the panel defaults to having +* no buttons selected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class RadioButtonPanel extends ButtonPanel +{ +/** +* A ButtonGroup to help manage button state. +*/ + protected ButtonGroup buttonGroup; + +/** +* ButtonGroup does not make it easy to unselect all buttons. +* The preferred way to do it is actually to create a hidden button. +*/ + protected JRadioButton hiddenButton; + +/** +* Constructs a RadioButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public RadioButtonPanel() + { + super(); + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public RadioButtonPanel( String[] buttonList ) + { + super( buttonList ); + } + +/** +* Overridden to set vertical-center alignment and 2-pixel vgap. +*/ + protected void initLayout() + { + super.initLayout(); + buttonPanelLayout.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + buttonPanelLayout.setVgap( 2 ); // looks nicer than java l&f recommendation (imho) + } + +/** +* Overridden to return a JRadioButton. +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; + JRadioButton newButton = new JRadioButton(); + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + + // reduce insets per java l&f guidelines (was 4 on each side) + newButton.setBorder( new EmptyBorder( 0, 4, 0, 4 ) ); + + if ( buttonGroup == null ) + { + buttonGroup = new ButtonGroup(); + + // cheesy hack to allow a buttongroup to have no items selected. + // note that the button is not added to container or buttonList. + hiddenButton = new JRadioButton( "Hidden Button" ); + buttonGroup.add( hiddenButton ); + } + buttonGroup.add( newButton ); + + return newButton; + } + +/** +* Selects the button whose name matches the given text value. +* @param newText A String matching the name of one of the buttons. +* If null, empty, or not matching, all buttons are deselected. +*/ + public void setValue(String aName) + { + if ( aName != null ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( c.getName().equals( aName ) ) + { + ((AbstractButton)c).setSelected( true ); + return; + } + } + } + } + + // null, empty, or not matching - deselect all + hiddenButton.setSelected( true ); + } + +/** +* Gets the name of the currently selected button. +* @return A string matching the name of the currently selected button, +* or null of no button is selected. +*/ + public String getValue() + { + String result = null; + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( ( c instanceof AbstractButton ) && ( ((AbstractButton)c).isSelected() ) ) + { + return c.getName(); + } + } + return result; + } + +/** +* Tests whether the specified value is checked. +* @param aValue A value to be tested. +* @return True if the specified value is checked, otherwise false. +*/ + public boolean getValue( String aValue ) + { + if ( aValue == null ) return false; + return aValue.equals( getValue() ); + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java new file mode 100644 index 0000000..6914cf6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java @@ -0,0 +1,274 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.KeyEvent; + +import javax.swing.JPasswordField; + +/** + * SmartPasswordField is an extention of JPasswordField. It does everything + * a JPassword does, as well as limit the number of characters. The user + * of this class can specify that a password can only have a maximum of + * 10 characters for instance. + * + * @author rob@straylight.princeton.com + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public class SmartPasswordField extends JPasswordField +{ + +/******************************* +* CONSTANTS +*******************************/ + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int SPACE = 32; + private static final int DASH = 45; + private static final int UNDERSCORE = 95; + private static final int PERIOD = 46; + private static final int PASTE = 22; // Ctl-V + + private int passwordLength = Integer.MAX_VALUE; + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default constructor. +*/ + public SmartPasswordField() + { + super(); + } + +/** +* This constructor allows the user to set the maximum length of the password. +* @param aLength The maximum length of the password. +*/ + public SmartPasswordField( int aLength ) + { + this(); + setPasswordLength( aLength ); + } + +/** +* Sets the maximum lenght of the password. The value must be 0 or greater. +* If the length specified is less than 0, then no action occurs. +* @param aLength The maximum lenght of the password. +*/ + public void setPasswordLength( int aLength ) + { + if ( aLength >= 0 ) + { + passwordLength = aLength; + } + } + +/** +* Returns the current maximum length of the password. +* @return The current maximum length of the password. +*/ + public int getPasswordLength() + { + return passwordLength; + } + +/** +* This method processes a key event. This event is generated by input from the +* keyboard when this text field has the focus. This method is called for every +* key that is pressed and released on the keyboard. This includes modifier +* keys like the shift and alt keys. This class looks at the key and determines +* if the key is valid input given the restrictions of this class. <BR> <BR> +* @param e A key event generated by a keyboard action. +*/ + public void processKeyEvent(KeyEvent e) + { + String currentText = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int selectionStart = 0; + int selectionEnd = 0; + int endOfHead = 0; + int startOfTail = 0; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean insertionPoint = false; + boolean selectionAtStart = false; + boolean selectionAtEnd = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + + if ((isPrintableCharacter(newChar)) || (backspace) || (delete) || (paste)) + { + // Analyze the current contents of the field + currentText = new String( getPassword() ); + currentLength = currentText.length(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + insertionPoint = (selectionStart == selectionEnd); + selectionAtStart = (selectionStart == 0); + selectionAtEnd = (selectionEnd >= currentLength); + if (selectionEnd > currentLength) + { + setSelectionEnd(currentLength); + } + + // Generate new string + if (selectionStart > 0) // Create head of test string + { + endOfHead = selectionStart; + if (insertionPoint && backspace) + { + endOfHead -= 1; + } + testString += currentText.substring(0, endOfHead); + } + + if (!(backspace || delete || paste)) // Add the new character + { + testString += newChar; + } + + if (paste) // Add the string from the clipboard + { + Transferable data = getToolkit().getSystemClipboard().getContents(this); + if (data != null) + { + try + { + String clipString = (String)data.getTransferData(DataFlavor.stringFlavor); + testString += clipString; + } + catch (java.io.IOException ioe) + { + // Do nothing + } + catch (UnsupportedFlavorException ufe) + { + // Do nothing + } + } + } + + if (selectionEnd < currentLength) // Add the tail of the string + { + startOfTail = selectionEnd; + if (insertionPoint && delete) + { + startOfTail += 1; + } + testString += currentText.substring(startOfTail); + } + + } + + if (testString.compareTo("") != 0) // Null string is OK + { + if (!(isValidString(testString))) + { + e.consume(); + } + } + } + else + { + e.consume(); + } + } + super.processKeyEvent(e); + + postProcessing(); + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + +/** +* Returns whether the inputted character is valid or not. In this case all +* characters are valid input. +* @param aChar A character to perform the validity test with. +* @return True if the character is valid for this subclassed text field. <BR> +* False is the character is not valid. +*/ + protected boolean isValidCharacter(char aChar) + { + return true; + } + +/** +* Returns whether a string is valid for this text field. As the user types from +* the keyboard, this method is called to determine if the new string in the text +* field is valid based upon the restriction of this class. The length of the +* new string is checked against the maximum password length. +* @param aString The string to perform the validity check with. +* @return True if the length of the string is less than or equal to the maximum length. +* False if the character is not valud. +*/ + protected boolean isValidString(String aString) + { + if ( aString.length() > passwordLength ) + { + return false; + } + + return true; + } + +/** +* This class does not need any post processing. +*/ + protected void postProcessing() + { + /* Do Nothing */ + } + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java new file mode 100644 index 0000000..cee37e1 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java @@ -0,0 +1,244 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.KeyEvent; + +import javax.swing.JTextField; + +/** + * SmartTextField is an abstract class for that allows the text field to + * intelligently analyze the user's input in real-time. As the user enters + * keystrokes, the generated string is analyzed to determine if the new string + * is valid based on the criteria of the concrete classes that extend this + * class. An invalid keystroke is rejected and not displayed in the text + * field. This class can be extended to to create smart text fields that only + * accept integers or floating points number or alphabetic strings of maximum + * length. These are several examples. + * + * @author rob@straylight.princeton.com + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public abstract class SmartTextField extends JTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int SPACE = 32; + private static final int DASH = 45; + private static final int UNDERSCORE = 95; + private static final int PERIOD = 46; + private static final int PASTE = 22; // Ctl-V + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* This method processes a key event. This event is generated by input from the +* keyboard when this text field has the focus. This method is called for every +* key that is pressed and released on the keyboard. This includes modifier +* keys like the shift and alt keys. This class looks at the key and determines +* if the key is valid input given the restrictions of the concrete sub-classes. <BR> <BR> +* Example - A smart text field only allows alphabetic characters. If the key +* pressed is a "2" then this method will determine that the key is invalid and +* "consume" the key event. <BR> <BR> +* Note - Every printable character has a "TYPED" key event. Currentlt under +* Java 1.2.1 this does not happen. Bug 4186905 relating this bug has been +* fixed and is awaiting release. +* @param e A key event generated by a keyboard action. +*/ + public void processKeyEvent(KeyEvent e) + { + String currentText = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int selectionStart = 0; + int selectionEnd = 0; + int endOfHead = 0; + int startOfTail = 0; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean insertionPoint = false; + boolean selectionAtStart = false; + boolean selectionAtEnd = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + + if ((isPrintableCharacter(newChar)) || (backspace) || (delete) || (paste)) + { + // Analyze the current contents of the field + currentText = getText(); + currentLength = currentText.length(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + insertionPoint = (selectionStart == selectionEnd); + selectionAtStart = (selectionStart == 0); + selectionAtEnd = (selectionEnd >= currentLength); + if (selectionEnd > currentLength) + { + setSelectionEnd(currentLength); + } + + // Generate new string + if (selectionStart > 0) // Create head of test string + { + endOfHead = selectionStart; + if (insertionPoint && backspace) + { + endOfHead -= 1; + } + testString += currentText.substring(0, endOfHead); + } + + if (!(backspace || delete || paste)) // Add the new character + { + testString += newChar; + } + + if (paste) // Add the string from the clipboard + { + Transferable data = getToolkit().getSystemClipboard().getContents(this); + if (data != null) + { + try + { + String clipString = (String)data.getTransferData(DataFlavor.stringFlavor); + testString += clipString; + } + catch (java.io.IOException ioe) + { + // Do nothing + } + catch (UnsupportedFlavorException ufe) + { + // Do nothing + } + } + } + + if (selectionEnd < currentLength) // Add the tail of the string + { + startOfTail = selectionEnd; + if (insertionPoint && delete) + { + startOfTail += 1; + } + testString += currentText.substring(startOfTail); + } + + } + + if (testString.compareTo("") != 0) // Null string is OK + { + if (!(isValidString(testString))) + { + e.consume(); + } + } + } + else + { + e.consume(); + } + } + super.processKeyEvent(e); + + postProcessing(); + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + +/** +* Default constructor for this class. The initial text of the smart text field +* can be specified as well as the size (in characters) of the text field. +* @param text The initial string that is displayed in the text field. +* @param columns THe width of the text field in characters. +*/ + protected SmartTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Returns whether a character is valid for this text field. As the user types +* from the keyboard, this method is called to determine if the character is a +* valid character based in the restrictions of the subclass. +* @param aChar A character to perform the validity test with. +* @return True if the character is valid for this subclassed text field. <BR> +* False is the character is not valid. +*/ + abstract protected boolean isValidCharacter(char aChar); + +/** +* Returns whether a string is valid for this text field. As the user types from +* the keyboard, this method is called to determine if the new string in the text +* field is valid based upon the restriction of the subclass. This is done after +* the character has been determined to be valid since there can be restrictions +* placed on the text string as a whole, such a maximum length or date format. +* @param aString The string to perform the validity check with. +* @return True if the string is valid for this subclassed text field. <BR> +* False if the character is not valud. +*/ + abstract protected boolean isValidString(String aString); + +/** +* This method is used by the any subclass that need to complete any processing +* of the text string in the text field after all the requirement checks have +* been performed. +*/ + abstract protected void postProcessing(); + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java new file mode 100644 index 0000000..3d9a85b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java @@ -0,0 +1,276 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.lang.reflect.Method; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; + +/** +* StatusButtonPanel extends ButtonPanel to provide a space +* to display status messages in a consistent manner.<BR><BR> +* Messages are erased after a certain predefined interval, +* defaulting to 10 seconds. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class StatusButtonPanel extends ButtonPanel +{ +/** +* This is the action command to all listeners when the status text is changed. +*/ + public static final String STATUS_CHANGED = "STATUS_CHANGED"; + + // note: weirdness happens if you initialize + // this variable. Because it is set by initLayout + // and initLayout is called by the superclass constructor, + // this variable would get initialized after initLayout + // is called... + protected Component statusComponent; // = null; + + protected Timer timer = null; + protected int interval = 10000; // adjust as needed + +/** +* Constructs a StatusButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public StatusButtonPanel() + { + super(); + setupTimer(); + } + +/** +* Constructs a StatusButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public StatusButtonPanel( String[] buttonList ) + { + super( buttonList ); + setupTimer(); + } + +/** +* Initializes the timer instance variable. +*/ + protected void setupTimer() + { + timer = new Timer( interval, this ); + timer.addActionListener( this ); + timer.setRepeats( false ); + timer.start(); + } + +/** +* Returns the number of milliseconds before the status message is cleared. +* The default is 10000. +* @return The current delay interval in milliseconds. +*/ + public int getDelayInterval() + { + return interval; + } + +/** +* Sets the number of milliseconds before the status message is cleared. +* @param millis The new delay interval in milliseconds. +*/ + public void setDelayInterval( int millis ) + { + interval = millis; + timer.setDelay( interval ); + } + +/** +* Returns the visual component used to display the status. +* @return A component used for displaying status. +*/ + public Component getStatusComponent() + { + return statusComponent; + + } +/** +* Receives ActionEvents from the internal timer. +* @param e The action event in question. +*/ + public void actionPerformed(ActionEvent e) + { + if ( e.getSource() == timer ) + { + setText( "" ); + return; + } + + // otherwise continue with superclass implementation + super.actionPerformed( e ); + } + +/** +* This method is responsible for the initial layout of the panel. +* Subclasses can implement different layouts, but this method +* is responsible for initializing buttonPanelLayout to a valid +* layout manager and setting this panel to use it. This method +* must should initialize statusComponent to a component that ideally +* has get/setText methods, although this is not required. +*/ + protected void initLayout() + { + + statusComponent = new JTextField(); + JTextField textField = (JTextField) statusComponent; + textField.setColumns( 20 ); + textField.setBackground( getBackground() ); + textField.setEditable( false ); + +// statusComponent = new PickListPanel(); // for testing + + this.setLayout( new GridBagLayout() ); + + GridBagConstraints gbc = + new GridBagConstraints(); + gbc.gridx = GridBagConstraints.RELATIVE; + gbc.gridy = GridBagConstraints.RELATIVE; + gbc.gridwidth = 1; + gbc.gridheight = 1; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + gbc.anchor = GridBagConstraints.CENTER; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(0, 5, 0, 10); + gbc.ipadx = 0; + gbc.ipady = 0; + +//1.2 new GridBagConstraints(GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE, 1, 1, 1.0, 0.0, +//1.2 GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 5, 0, 10), 0, 0 ); + + this.add( statusComponent, gbc ); + + buttonContainer = new JPanel(); + buttonPanelLayout = new BetterFlowLayout(); + buttonContainer.setLayout(buttonPanelLayout); + buttonPanelLayout.setAlignment( BetterFlowLayout.RIGHT ); + ((BetterFlowLayout)buttonPanelLayout).setWidthUniform( true ); + gbc.weightx = 0.0; + gbc.insets = new Insets( 0, 0, 0, 0 ); + this.add( buttonContainer, gbc ); + } + +/** +* Sets the text to appear in the status area. +* @param newText A string to appear in the status area. Nulls are allowed. +*/ + public void setText(String newText) + { + // TODO: should use property introspection instead + + // use reflection to call the "setText" method, if any. + try + { + Class c = statusComponent.getClass(); + Method m = c.getMethod( "setText", new Class[] { new String().getClass() } ); + m.invoke( statusComponent, new Object[] { newText } ); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATUS_CHANGED ) ); + statusComponent.paint( statusComponent.getGraphics() ); + } + catch ( Exception exc ) + { + // "setText" method does not exist; do nothing. + } + + // if non-empty string, start the timer + if ( ! "".equals( newText ) ) + { + timer.restart(); + } + } + +/** +* Gets the text in the status area. +* @return The string being displayed in the status area. +*/ + public String getText() + { + // TODO: should use property introspection instead + + String value = ""; + // use reflection to call the "setText" method, if any. + try + { + Class c = statusComponent.getClass(); + Method m = c.getMethod( "getText", (Class[])null ); + value = (String) m.invoke( statusComponent, (Object[])null ); + } + catch ( Exception exc ) + { + // "getText" method does not exist; do nothing. + } + return value; + } + + // for testing + + public static void main( String[] argv ) + { + try + { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + JFrame dialog = new JFrame(); + BorderLayout bl = new BorderLayout( 20, 20 ); + +// StatusButtonPanel panel = new StatusButtonPanel(); +// System.out.println( panel.statusComponent ); + StatusButtonPanel panel = new StatusButtonPanel( new String[] { "Okay", "Cancel" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, BorderLayout.SOUTH ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + dialog.pack(); + dialog.setVisible( true ); + + panel.setBorder( new EmptyBorder( 5, 5, 5, 5 ) ); + panel.setAlignment( BetterFlowLayout.RIGHT ); +// panel.getButton( "One" ).setEnabled( false ); + panel.setText( "File saved." ); + System.out.println( panel.getText() ); + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java new file mode 100644 index 0000000..a51ed16 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java @@ -0,0 +1,100 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.components; + +import java.awt.Color; +import java.awt.image.RGBImageFilter; + +/** + * TintedImageFilter tints all gray pixels half-way towards + * the value passed into the constructor. This "tints" a + * mostly grayscale image. This has proven useful for tinting + * user interface decorative images towards one of the SystemColor + * constants to better mesh with a platform look and feel. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 893 $ + */ + public class TintedImageFilter extends RGBImageFilter + { + double redOffset, greenOffset, blueOffset; + + public TintedImageFilter( Color aColor ) + { + canFilterIndexColorModel = true; + redOffset = getOffset( aColor.getRed() ); + greenOffset = getOffset( aColor.getGreen() ); + blueOffset = getOffset( aColor.getBlue() ); + } + + /** + * Calculates the offset used to modify color + * values. This method returns half the difference + * between the specified color level and 192. + */ + protected double getOffset( int colorValue ) + { + return ( colorValue - 192 ) / 2; + } + + public int filterRGB(int x, int y, int rgb) + { + + int red = ( rgb & 0xff0000 ) >> 16; + int green = ( rgb & 0x00ff00 ) >> 8; + int blue = ( rgb & 0x0000ff ); + + // if roughly black + if ( red + green + blue < 30 ) return rgb; + + // if roughly gray + if ( ( Math.abs( red - green ) < 10 ) + && ( Math.abs( red - blue ) < 10 ) ) + { + red += redOffset; + if ( red < 0 ) red = 0; + if ( red > 255 ) red = 255; + green += greenOffset; + if ( green < 0 ) green = 0; + if ( green > 255 ) green = 255; + blue += blueOffset; + if ( blue < 0 ) blue = 0; + if ( blue > 255 ) blue = 255; + + return new Color( red, green, blue ).getRGB(); + } + + return rgb; + } + } + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/01/18 21:27:04 mpowers + * Made the tinting a little darker. + * + * Revision 1.1 2001/01/12 17:36:27 mpowers + * Contributing TintedImageFilter. + * + * + */ diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java new file mode 100644 index 0000000..f0bb6c2 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java @@ -0,0 +1,727 @@ +/* +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.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Stack; +import java.util.Vector; + +import javax.swing.ComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JToolBar; +import javax.swing.JTree; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; +import javax.swing.UIManager; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* TreeChooser is a FileChooser-like panel that +* uses a TreeModel as a data source. It basically +* provides an alternative to JTree for rendering +* and manipulating tree-like data. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeChooser extends JPanel + implements ActionListener, ListSelectionListener, + TreeSelectionListener, TreeModelListener, ListCellRenderer +{ + /** + * The TreeChooser responds to this action command + * by calling displayPrevious(). + */ + public static final String BACK = "Back"; + + /** + * The TreeChooser responds to this action command + * by calling displayHome(). + */ + public static final String HOME = "Home"; + + /** + * The TreeChooser responds to this action command + * by calling displayParent(). + */ + public static final String UP = "Up"; + + /** + * The TreeChooser responds to this action command + * by attempting to navigate to the first node in + * the current selection and display that node's children. + */ + public static final String SELECT = "Select"; + + protected JList contents; + protected JComboBox pathCombo; + protected JToolBar toolBar; + + protected TreeModel model; + protected TreeSelectionModel selectionModel; + protected TreeCellRenderer renderer; + protected TreePath displayPath; + protected Stack pathStack; + protected int pathIndent; + + private ChooserComboBoxModel comboBoxModel; + private JTree bogusJTree; // needed for tree cell renderer + private Dimension preferredSize; + + public TreeChooser() + { + preferredSize = new Dimension( 300, 200 ); + model = new DefaultTreeModel( new DefaultMutableTreeNode( "Root" ) ); + displayPath = new TreePath( model.getRoot() ); + selectionModel = new DefaultTreeSelectionModel(); + renderer = new DefaultTreeCellRenderer(); + pathStack = new Stack(); + pathIndent = 0; // 16; + comboBoxModel = new ChooserComboBoxModel( this ); + + bogusJTree = new JTree(); + bogusJTree.setModel( model ); + + init(); + displayHome(); + + stopListening(); // clear existing listeners + startListening(); + } + + public Dimension getPreferredSize() + { + return preferredSize; + } + + protected void init() + { + this.setLayout( new BorderLayout( 10, 10 ) ); + + contents = initList(); + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION ); + // synchs with DefaultTreeSelectionModel + + JScrollPane scrollPane = new JScrollPane( contents ); + scrollPane.setPreferredSize( new Dimension( 200, 150 ) ); + this.add( scrollPane, BorderLayout.CENTER ); + + Component previewPane = initPreviewPane(); + if ( previewPane != null ) + { + this.add( previewPane, BorderLayout.EAST ); + } + + JPanel navigationPanel = new JPanel(); + navigationPanel.setLayout( new BorderLayout( 10, 10 ) ); + this.add( navigationPanel, BorderLayout.NORTH ); + + pathCombo = initComboBox(); + if ( pathCombo != null ) + { + pathCombo.setModel( comboBoxModel ); + + // put combo in a grid bag to handle varying + // heights of JToolBars across platforms + JPanel panel = new JPanel(); + panel.setLayout( new GridBagLayout() ); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + panel.add( pathCombo, gbc ); + navigationPanel.add( panel, BorderLayout.CENTER ); + } + + Component toolBar = initToolBar(); + if ( toolBar != null ) + { + navigationPanel.add( toolBar, BorderLayout.EAST ); + } + + } + + /** + * Creates tool bar or return null if no tool bar is desired. + * This implementation returns a JToolBar containing buttons + * for BACK, UP, and HOME. + */ + protected Component initToolBar() + { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable( false ); + JButton button; + button = new JButton( UIManager.getIcon("FileChooser.upFolderIcon") ); + button.setActionCommand( UP ); + button.addActionListener( this ); + toolBar.add( button ); + button = new JButton( UIManager.getIcon("FileChooser.homeFolderIcon") ); + button.setActionCommand( HOME ); + button.addActionListener( this ); + toolBar.add( button ); +/* + button = new JButton( UIManager.getIcon("FileChooser.newFolderIcon") ); + button.setActionCommand( BACK ); + button.addActionListener( this ); + toolBar.add( button ); +*/ + return toolBar; + } + + /** + * Creates the component that is used to display a preview of the + * selected item(s) in the content area. This component would listen + * to the selection model to update itself when the selected items change. + * Return null to omit this component. + * This implementation returns null. + */ + protected Component initPreviewPane() + { + return null; + } + + /** + * Creates the JComboBox that is used to render the path leading to + * the displayed contents. Return null to omit this combo box. + * This implementation returns a stock JComboBox that uses this + * class as its cell renderer. + */ + protected JComboBox initComboBox() + { + JComboBox comboBox = new JComboBox(); + comboBox.setRenderer( this ); + return comboBox; + } + + /** + * Creates the JList that is used to render the path leading to + * the displayed contents. This method may not return null. + * This implementation returns a stock JList that uses this + * class as its cell renderer and fires a SELECT action event + * on double click. + */ + protected JList initList() + { + JList list = new JList(); + list.setCellRenderer( this ); + list.addMouseListener( new MouseAdapter() + { + public void mouseClicked( MouseEvent evt ) + { + if ( evt.getClickCount() > 1 ) + { + actionPerformed( new ActionEvent( this, 0, SELECT ) ); + } + } + }); + return list; + } + + /** + * Begins listening to the specified tree model + * and tree selection model. + */ + protected void startListening() + { + model.addTreeModelListener( this ); + selectionModel.addTreeSelectionListener( this ); + contents.addListSelectionListener( this ); + } + + /** + * Stops listening to the specified tree model + * and tree selection model. + */ + protected void stopListening() + { + model.removeTreeModelListener( this ); + selectionModel.removeTreeSelectionListener( this ); + contents.removeListSelectionListener( this ); + } + + /** + * Returns the TreeModel used by the TreeChooser. + */ + public TreeModel getModel() + { + return model; + } + + /** + * Sets the TreeModel used by the TreeChooser. + */ + public void setModel( TreeModel aTreeModel ) + { + stopListening(); + model = aTreeModel; + bogusJTree.setModel( aTreeModel ); + pathStack.removeAllElements(); + startListening(); + displayHome(); + } + + /** + * Returns the TreeSelectionModel used by the TreeChooser. + */ + public TreeSelectionModel getSelectionModel() + { + return selectionModel; + } + + /** + * Sets the TreeSelectionModel used by the TreeChooser. + */ + public void setSelectionModel( TreeSelectionModel aSelectionModel ) + { + selectionModel = aSelectionModel; + if ( aSelectionModel.getSelectionMode() == + TreeSelectionModel.SINGLE_TREE_SELECTION ) + { + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.SINGLE_SELECTION ); + } + else + { + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION ); + } + updateSelection(); + } + + /** + * Returns the TreeCellRenderer used by the TreeChooser. + */ + public TreeCellRenderer getRenderer() + { + return renderer; + } + + /** + * Sets the TreeCellRenderer used by the TreeChooser. + */ + public void setRenderer( TreeCellRenderer aRenderer ) + { + renderer = aRenderer; + updateContents(); + } + + /** + * Displays the "home" directory. + * This implementation displays the root node's children. + */ + public void displayHome() + { + setDisplayPath( null ); + } + + /** + * Displays the parent path of the currently displayed path. + */ + public void displayParent() + { + setDisplayPath( displayPath.getParentPath() ); + } + + /** + * Displays the last displayed path before the current one, + * emulating the behavior of a "back" button. + */ + public void displayPrevious() + { + if ( pathStack.empty() ) + { + displayHome(); + } + else + { + setDisplayPathDirect( (TreePath) pathStack.pop() ); + updateContents(); + } + } + + /** + * Pushes the previous item onto the stack, sets + * the display path, and then updates the contents. + * If aPath is null, the root node's children are displayed. + */ + public void setDisplayPath( TreePath aPath ) + { + if ( aPath == null ) + { + aPath = new TreePath( getModel().getRoot() ); + } + if ( ! displayPath.equals ( aPath ) ) + { + pathStack.push( displayPath ); + setDisplayPathDirect( aPath ); + } + updateContents(); + } + + /** + * Sets the displayPath field and does not + * update the stack nor update the contents. + */ + protected void setDisplayPathDirect( TreePath aPath ) + { + displayPath = aPath; + } + + /** + * Gets the currently displayed path. + */ + public TreePath getDisplayPath() + { + return displayPath; + } + + /** + * Called when selected path changes or when model indicates + * that the displayed path has changed. + */ + protected void updateContents() + { + stopListening(); + + // update combo box + comboBoxModel.fireContentsChanged(); + + // update list contents + Object displayedObject = displayPath.getLastPathComponent(); +/* +//FIXME: this display group doesn't seem to be getting the sort orderings from parent +if ( displayedObject instanceof net.wotonomy.ui.EODisplayGroup ) +System.out.println( ((net.wotonomy.ui.EODisplayGroup)displayedObject).displayedObjects() ); +*/ + int count = model.getChildCount( displayedObject ); + Object[] children = new Object[ count ]; + for ( int i = 0; i < count; i++ ) + { + children[i] = model.getChild( displayedObject, i ); + } + contents.setListData( children ); + + startListening(); + + // synchronize the selection + updateSelection(); + } + + /** + * Updates the selection in the list to reflect the + * selection in the tree selection model. + */ + public void updateSelection() + { + int index; + Object last = displayPath.getLastPathComponent(); + TreePath[] selectionPaths = selectionModel.getSelectionPaths(); + if ( selectionPaths != null ) + { + List selectedIndices = new LinkedList(); + for ( int i = 0; i < selectionPaths.length; i++ ) + { + if ( displayPath.equals( selectionPaths[i].getParentPath() ) ) + { + index = getModel().getIndexOfChild( + last, selectionPaths[i].getLastPathComponent() ); + if ( index != -1 ) + { + selectedIndices.add( new Integer( index ) ); + } + else // should never happen + { + throw new WotonomyException( + "Could not find child of displayed node." ); + } + } + } + int[] selected = new int[ selectedIndices.size() ]; + for ( int i = 0; i < selected.length; i++ ) + { + selected[i] = ((Integer)selectedIndices.get(i)).intValue(); + } + stopListening(); + contents.setSelectedIndices( selected ); + startListening(); + } + } + + // interface TreeModelListener + + public void treeNodesChanged( TreeModelEvent evt ) + { +/* + if ( displayPath.getLastPathComponent().toString().equals( + evt.getTreePath().getLastPathComponent().toString() ) ) + { +System.out.println( "TreeChooser.treeNodesChanged: " + count++ ); +*/ + updateContents(); +/* + } + else + { + System.out.println( evt.getTreePath() + " != " + displayPath ); + } +*/ + } + + public void treeNodesInserted( TreeModelEvent evt ) + { +// updateContents(); + } + + public void treeNodesRemoved( TreeModelEvent evt ) + { +// updateContents(); + } + + public void treeStructureChanged( TreeModelEvent evt ) + { + if ( ( evt.getTreePath().equals( displayPath ) ) + || ( evt.getTreePath().isDescendant( displayPath ) ) ) + { +// setDisplayPath( evt.getTreePath() ); + } + + displayHome(); + } + + // interface TreeSelectionListener + + /** + * Called when the tree selection model's value changes. + * This is presumably an external change, so this calls + * updateSelection. + */ + public void valueChanged( TreeSelectionEvent evt ) + { + updateSelection(); + } + + // interface ListSelectionListener + + /** + * Called when user changes the selection in the list. + * This implementation updates the tree selection model + * with the corresponding selection. + */ + public void valueChanged( ListSelectionEvent evt ) + { + if ( ! evt.getValueIsAdjusting() ) + { + Object last = displayPath.getLastPathComponent(); + int[] selection = contents.getSelectedIndices(); + TreePath[] selectionPaths = new TreePath[ selection.length ]; + for ( int i = 0; i < selection.length; i++ ) + { + selectionPaths[i] = displayPath.pathByAddingChild( + getModel().getChild( last, selection[i] ) ); + } + selectionModel.setSelectionPaths( selectionPaths ); + } + + } + + // interface ListCellRenderer + + /** + * This method returns the component returned by the tree cell renderer. + */ + public Component getListCellRendererComponent( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus ) + { + boolean isLeaf = ( model.isLeaf( value ) ); + + bogusJTree.setForeground( list.getForeground() ); + bogusJTree.setBackground( list.getBackground() ); + + JComponent result = (JComponent) renderer.getTreeCellRendererComponent( + bogusJTree, value, isSelected, (list != contents), + isLeaf, index, cellHasFocus ); +/* + if ( ( list != contents ) && ( index > -1 ) ) + { + result.setBorder( + BorderFactory.createEmptyBorder( 0, index*pathIndent, 0, 0 ) ); + } + else + { + result.setBorder( + BorderFactory.createEmptyBorder() ); + } +*/ + return result; + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + String command = evt.getActionCommand(); + + if ( HOME.equals( command ) ) + { + displayHome(); + } + else + if ( UP.equals( command ) ) + { + displayParent(); + } + else + if ( BACK.equals( command ) ) + { + displayPrevious(); + } + else + if ( SELECT.equals( command ) ) + { + Cursor oldCursor = getCursor(); + setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) ); + + int index = contents.getSelectedIndex(); + // if selection + if ( index != -1 ) + { + Object parent = displayPath.getLastPathComponent(); + Object child = getModel().getChild( parent, index ); + // if selected item is not a leaf + if ( getModel().getChildCount( child ) > 0 ) + { + // navigate to selected item + setDisplayPath( displayPath.pathByAddingChild( child ) ); + } + } + + setCursor( oldCursor ); + } + + } + + private class ChooserComboBoxModel implements ComboBoxModel + { + TreeChooser treeChooser; + Vector listeners; + + ChooserComboBoxModel( TreeChooser aTreeChooser ) + { + treeChooser = aTreeChooser; + listeners = new Vector(); + } + + public int getSize() + { + return treeChooser.displayPath.getPathCount(); + } + + public Object getElementAt(int index) + { + return treeChooser.displayPath.getPathComponent( index ); + } + + public Object getSelectedItem() + { + return treeChooser.displayPath.getLastPathComponent(); + } + + public void setSelectedItem(Object anItem) + { + if ( ! ( + treeChooser.displayPath.getLastPathComponent().equals( anItem ) ) ) + { + Object[] items = treeChooser.displayPath.getPath(); + TreePath path = new TreePath( getModel().getRoot() ); + for ( int i = 1; i < items.length; i++ ) + { + if ( path.getLastPathComponent() == anItem ) + { + treeChooser.setDisplayPath( path ); + return; + } + path = path.pathByAddingChild( items[i] ); + } + } + } + + public void addListDataListener(ListDataListener l) + { + listeners.add( l ); + } + + public void removeListDataListener(ListDataListener l) + { + listeners.remove( l ); + } + + public void fireContentsChanged() + { + Enumeration e = listeners.elements(); + while ( e.hasMoreElements() ) + { + ((ListDataListener)e.nextElement()).contentsChanged( + new ListDataEvent( + this, ListDataEvent.CONTENTS_CHANGED, 0, getSize() ) ); + } + } + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java new file mode 100644 index 0000000..fbf3791 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java @@ -0,0 +1,224 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 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.components; + +import java.awt.Component; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.JViewport; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that paints a portion of a JTree. +* Extends JViewport to take advantage of buffering and +* fast blitting (avoids repeated clipping and repainting). +* Defaults opaque to false: to see selection background +* painted, call setOpaque( true ). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeTableCellRenderer extends JViewport implements TableCellRenderer, MouseListener { + + JTree tree; + Component emptyComponent; + JTable delegateTable; + int lastKnownColumn; + + /** + * Constructor takes a JTree and modifies it by setting + * rootVisible to false, showsRootHandles to true, + * opaque to false, and border to null. + */ + public TreeTableCellRenderer( JTree aTree ) + { + setView( aTree ); + setBorder( null ); + tree = aTree; + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setBorder( null ); + tree.setOpaque( false ); + + Object renderer = tree.getCellRenderer(); + if ( renderer instanceof JComponent ) + { + ((JComponent)renderer).setOpaque( false ); + } + Object editor = tree.getCellEditor(); + if ( editor instanceof JComponent ) + { + ((JComponent)editor).setOpaque( false ); + } + + this.setOpaque( false ); + emptyComponent = new JLabel(); + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + if ( isSelected ) + { + setForeground( table.getSelectionForeground() ); + setBackground( table.getSelectionBackground() ); + } + else + { + setForeground( table.getForeground() ); + setBackground( table.getBackground() ); + } + + lastKnownColumn = column; + if ( delegateTable != table ) + { + if ( delegateTable != null ) + { + delegateTable.removeMouseListener( this ); + } + table.addMouseListener( this ); + delegateTable = table; + } + + Rectangle rect = tree.getRowBounds( row ); + if ( rect != null ) + { + setViewPosition( new Point( 0 /*rect.x*/, rect.y ) ); + + //FIXME: this causes problems for some LAFs (like Metal): + // in particular, the table height seems to get stuck. + //if ( table.getRowHeight( row ) != rect.height ) + //{ + // table.setRowHeight( row, rect.height ); + //} + return this; + } + else + { + return emptyComponent; + } + } + + public void mouseClicked(MouseEvent e) + { + delegateToTree( e ); + } + + public void mousePressed(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseReleased(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseEntered(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseExited(MouseEvent e) + { + delegateToTree( e ); + } + + protected void delegateToTree(MouseEvent e) + { + int col = delegateTable.getColumnModel().getColumnIndexAtX( e.getX() ); + if ( col == lastKnownColumn ) + { + Rectangle nodeRect = tree.getRowBounds( 0 ); + Rectangle cellRect = delegateTable.getCellRect( -1, col, false ); + if ( nodeRect != null ) + { + e.translatePoint( -cellRect.x, nodeRect.y ); + tree.dispatchEvent( // e ); + new MouseEvent( tree, e.getID(), e.getWhen(), e.getModifiers(), + e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger() ) ); + } + } + } + + public void repaint() + { + //if ( delegateTable != null ) delegateTable.repaint(); + + // not calling super.repaint() does not seem to cause + // any problems so we're not doing it. + } + +} + +/* + * $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.11 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.10 2002/04/12 20:07:35 mpowers + * Fixed cool/annoying view position. + * + * Revision 1.9 2002/04/09 18:12:21 mpowers + * Fixes for 1.4. + * + * Revision 1.8 2002/03/22 22:39:24 mpowers + * Can now move column to any position in the table. + * + * Revision 1.7 2002/03/11 03:13:22 mpowers + * Adjusting for viewport position; no longer responding to repaint(). + * + * Revision 1.6 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.5 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.3 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.2 2002/02/18 23:13:55 mpowers + * Only setting row height when needed. + * + * Revision 1.1 2002/02/18 03:46:08 mpowers + * Implemented TreeTableCellRenderer. + * + * + */ + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html new file mode 100644 index 0000000..618651b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html @@ -0,0 +1,26 @@ +<body> +<p> +Contains various and useful Swing components. +These can be used in conjunction with +the ui framework, however, there are no +dependencies and all of these components can be used +independently of the rest of the framework. +</p> +<p> +Of note are the ButtonPanel classes, which +automate the placement and layout of buttons +in a manner consistent with the Java Look and Feel +guidelines. This uses the BetterFlowLayout which +is another useful class. +</p> +<p> +Also of note is the InfoPanel, which automates +the placement and layout of labeled fields on +a panel. +</p> +<p> +And the +various cell renderer and editor components can +be useful as well. +</p> +</body> diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html new file mode 100644 index 0000000..574cc7b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html @@ -0,0 +1,21 @@ +<body> +<p> +Contains associations designed for use +with the Swing framework. +</p> +<p> +In general, most text components can use the +TextAssociation, and most buttons, including +checkboxes and radio buttons, can use the +ButtonAssociation. +</p> +<p> +Tables are handled with the TableColumnAssociation +by dealing with TableColumns that are later added +to a table. +</p> +<p> +The TreeAssociation will handle any component that +uses a TreeModel and a TreeSelectionModel. +</p> +</body> diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java new file mode 100644 index 0000000..4412dbc --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java @@ -0,0 +1,126 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Hashtable; + +/** + * ClassGrabber is a class loader used by WindowGrabber. + * It simply loads classes by filename and nothing more. + * It exists mainly because the java 1.1 class loading + * framework doesn't easily allow the creation of class + * loaders nor the loading of arbitrary classes. + * + * @author michael@mpowers.net + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class ClassGrabber extends ClassLoader +{ + Hashtable classMap = new Hashtable(); + + public ClassGrabber() + { + super(); + } + + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + Class c = (Class) classMap.get( name ); + + if ( c != null ) return c; + + try + { + c = findSystemClass( name ); + } + catch ( Exception exc1 ) + { + // System.err.print( "findSystemClass: " + name + ": " ); + // System.err.println( exc1 ); + } + + if ( c != null ) return c; + + try + { + c = findLoadedClass( name ); + } + catch ( Exception exc1 ) + { + // System.err.print( "findLoadedClass: " + name + ": " ); + // System.err.println( exc1 ); + } + + if ( c != null ) return c; + + try + { + InputStream input = new BufferedInputStream( new FileInputStream( name ) ); + ByteArrayOutputStream output = new ByteArrayOutputStream( 200 ); + int ch; + while ( ( ch = input.read() ) != -1 ) + { + output.write( ch ); + } + byte[] data = output.toByteArray(); + c = defineClass( null, data, 0, data.length ); + } + catch ( Exception exc ) + { + System.err.print( "getResource: " + name + ": " ); + System.err.println( exc ); + c = null; + } + + if ( c != null ) + { + classMap.put( name, c ); + if ( resolve ) resolveClass( c ); + } + + return c; + } +} + +/* + * $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.2 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:51:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java new file mode 100644 index 0000000..c63157d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java @@ -0,0 +1,160 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URL; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +/** +* Visually highlights a component with the specified image for a brief period. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class ComponentHighlighter implements ActionListener +{ + // lots of state to track + JRootPane rootPane; + JComponent component; + Component oldGlassPane; + JLabel imageLabel; + Timer timer; + JPanel glassPane; + +/** +* Alternate "Fire-and-forget" constructor loads an image from a URL. +* @param aComponent A Component that will be highlighted. +* @param aURL A URL pointing to an image. +*/ + public ComponentHighlighter( JComponent aComponent, URL aURL ) + { + if ( aURL == null ) return; + init( aComponent, Toolkit.getDefaultToolkit().getImage( aURL ) ); + } + +/** +* "Fire-and-forget" constructor. +* @param aComponent A Component that will be highlighted. +* @param anImage An image, preferably an animated GIF with transparency, +* that will slide along the length of the component. +*/ + public ComponentHighlighter( JComponent aComponent, Image anImage ) + { + init( aComponent, anImage ); + } + + protected void init( JComponent aComponent, Image anImage ) + { + if ( ( aComponent == null ) || ( anImage == null ) ) return; + + component = aComponent; + rootPane = SwingUtilities.getRootPane( component ); + oldGlassPane = rootPane.getGlassPane(); + + glassPane = new JPanel(); + rootPane.setGlassPane( glassPane ); + glassPane.setVisible( true ); + glassPane.setOpaque( false ); + glassPane.setLayout( null ); + + ImageIcon icon = new ImageIcon( anImage ); + + imageLabel = new JLabel(); + imageLabel.setIconTextGap( 0 ); + imageLabel.setIcon( icon ); + imageLabel.setSize( icon.getIconWidth(), icon.getIconHeight() ); + glassPane.add( imageLabel ); + + Rectangle bounds = component.getBounds(); + if ( component.getParent() instanceof Component ) + { + bounds = SwingUtilities.convertRectangle( (Container) component.getParent(), + bounds, rootPane.getContentPane() ); + } + imageLabel.setLocation( + bounds.x, bounds.y + bounds.height - imageLabel.getBounds().height ); + + glassPane.revalidate(); + glassPane.repaint(); + + component.transferFocus(); // halts a caret, if necessary + + timer = new Timer( 80, this ); + timer.setRepeats( true ); + timer.start(); + } + + public void actionPerformed( ActionEvent evt ) + { + Rectangle bounds = imageLabel.getBounds(); + Rectangle target = component.getBounds(); + if ( component.getParent() instanceof Component ) + { + target = SwingUtilities.convertRectangle( (Container) component.getParent(), + target, rootPane.getContentPane() ); + } + + if ( bounds.x + bounds.width > target.x + target.width ) + { // clean up and end + timer.stop(); + rootPane.setGlassPane( oldGlassPane ); + component.requestFocus(); + return; + } + + // else, slide to the right and continue + imageLabel.setLocation( + bounds.x + Math.max( bounds.width / 12, 1 ), bounds.y ); + imageLabel.repaint(); + } +} + +/* + * $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.1.1.1 2000/12/21 15:51:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java new file mode 100644 index 0000000..82fd897 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java @@ -0,0 +1,520 @@ +/* + The source code in this file, GIFEncoder.java, + belongs to the public domain. +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.AWTException; +import java.awt.Image; +import java.awt.image.PixelGrabber; +import java.io.IOException; +import java.io.OutputStream; + +/** + * GIFEncoder is a class which takes an image and saves it to a stream + * using the GIF file format. A GIFEncoder is constructed with either + * an AWT Image (which must be fully loaded) or a set of RGB arrays. + * The image can be written out with a call to <CODE>write</CODE>.<br><br> + * + * Three caveats: + * <UL> + * <LI>GIFEncoder will convert the image to indexed color upon + * construction, and this is not fast. + * + * <LI>The image cannot have more than 256 colors, since GIF is an 8 + * bit format. + * + * <LI>Since the image must be completely loaded into memory, + * there may be problems with large images. + * </UL> + * + * This implementation is heavily based on code made available by + * Adam Doppelt, which was based upon gifsave.c, which was written + * and released by Sverre H. Huseby. + * + * @author amd@brown.edu + * @author sverrehu@ifi.uio.no + * @author michael@mpowers.net + */ +public class GIFEncoder +{ + short width_, height_; + int numColors_; + byte pixels_[], colors_[]; + + ScreenDescriptor sd_; + ImageDescriptor id_; + +/** + * Construct a GIFEncoder. The constructor will convert the image to + * an indexed color array. This may take some time. If more than 256 + * colors are encountered, all subsequent colors are mapped to the first + * color encountered. + * @param image The image to encode. The image must be completely loaded. + * @exception AWTException Will be thrown if the pixel grab fails. This + * can happen if Java runs out of memory. + * */ + public GIFEncoder(Image image) throws AWTException + { + width_ = (short)image.getWidth(null); + height_ = (short)image.getHeight(null); + + int values[] = new int[width_ * height_]; + PixelGrabber grabber = new PixelGrabber( + image, 0, 0, width_, height_, values, 0, width_); + + try + { + if(grabber.grabPixels() != true) + throw new AWTException("Grabber returned false: " + + grabber.status()); + } + catch (InterruptedException e) + { + } + + byte r[][] = new byte[width_][height_]; + byte g[][] = new byte[width_][height_]; + byte b[][] = new byte[width_][height_]; + int index = 0; + for (int y = 0; y < height_; ++y) + { + for (int x = 0; x < width_; ++x) + { + r[x][y] = (byte)((values[index] >> 16) & 0xFF); + g[x][y] = (byte)((values[index] >> 8) & 0xFF); + b[x][y] = (byte)((values[index]) & 0xFF); + ++index; + } + } + toIndexedColor(r, g, b); + } + +/** + * Construct a GIFEncoder. The constructor will convert the image to + * an indexed color array. This may take some time. <br><br> + * Each array stores intensity values for the image. In other words, + * r[x][y] refers to the red intensity of the pixel at column x, row y. + * @param r An array containing the red intensity values. + * @param g An array containing the green intensity values. + * @param b An array containing the blue intensity values. + * + * @exception AWTException Will be thrown if the image contains more than + * 256 colors. + * */ + public GIFEncoder(byte r[][], byte g[][], byte b[][]) throws AWTException + { + width_ = (short)(r.length); + height_ = (short)(r[0].length); + + toIndexedColor(r, g, b); + } + +/** + * Writes the image out to a stream in the GIF file format. This will + * be a single GIF87a image, non-interlaced, with no background color. + * <B>This may take some time.</B><P> + * + * @param output The stream to output to. This should probably be a + * buffered stream. + * + * @exception IOException Will be thrown if a write operation fails. + * */ + public void write(OutputStream output) throws IOException + { + BitUtils.writeString(output, "GIF87a"); + ScreenDescriptor sd = new ScreenDescriptor(width_, height_, + numColors_); + sd.write(output); + + output.write(colors_, 0, colors_.length); + + ImageDescriptor id = new ImageDescriptor(width_, height_, ','); + id.write(output); + + byte codesize = BitUtils.bitsNeeded(numColors_); + if (codesize == 1) + ++codesize; + output.write(codesize); + + LZWCompressor.LZWCompress(output, codesize, pixels_); + output.write(0); + id = new ImageDescriptor((byte)0, (byte)0, ';'); + id.write(output); + output.flush(); + } + + void toIndexedColor(byte r[][], byte g[][], + byte b[][]) throws AWTException + { + pixels_ = new byte[width_ * height_]; + colors_ = new byte[256 * 3]; + int colornum = 0; + for (int x = 0; x < width_; ++x) + { + for (int y = 0; y < height_; ++y) + { + int search; + for (search = 0; search < colornum; ++search) + if (colors_[search * 3] == r[x][y] && + colors_[search * 3 + 1] == g[x][y] && + colors_[search * 3 + 2] == b[x][y]) + break; + + if (search > 255) + search = 0; + //throw new AWTException("Too many colors."); + + pixels_[y * width_ + x] = (byte)search; + + if (search == colornum) { + colors_[search * 3] = r[x][y]; + colors_[search * 3 + 1] = g[x][y]; + colors_[search * 3 + 2] = b[x][y]; + ++colornum; + } + } + } + + numColors_ = 1 << BitUtils.bitsNeeded(colornum); + byte copy[] = new byte[numColors_ * 3]; + System.arraycopy(colors_, 0, copy, 0, numColors_ * 3); + colors_ = copy; + } + +} + +class BitFile +{ + OutputStream output_; + byte buffer_[]; + int index_, bitsLeft_; + + public BitFile(OutputStream output) + { + output_ = output; + buffer_ = new byte[256]; + index_ = 0; + bitsLeft_ = 8; + } + + public void flush() throws IOException + { + int numBytes = index_ + (bitsLeft_ == 8 ? 0 : 1); + if (numBytes > 0) + { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + } + + public void writeBits(int bits, int numbits) throws IOException { + int bitsWritten = 0; + int numBytes = 255; + do + { + if ((index_ == 254 && bitsLeft_ == 0) || index_ > 254) + { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + + if (numbits <= bitsLeft_) + { + buffer_[index_] |= (bits & ((1 << numbits) - 1)) << + (8 - bitsLeft_); + bitsWritten += numbits; + bitsLeft_ -= numbits; + numbits = 0; + } + else + { + buffer_[index_] |= (bits & ((1 << bitsLeft_) - 1)) << + (8 - bitsLeft_); + bitsWritten += bitsLeft_; + bits >>= bitsLeft_; + numbits -= bitsLeft_; + buffer_[++index_] = 0; + bitsLeft_ = 8; + } + + } + while (numbits != 0); + } +} + +class LZWStringTable +{ + private final static int RES_CODES = 2; + private final static short HASH_FREE = (short)0xFFFF; + private final static short NEXT_FIRST = (short)0xFFFF; + private final static int MAXBITS = 12; + private final static int MAXSTR = (1 << MAXBITS); + private final static short HASHSIZE = 9973; + private final static short HASHSTEP = 2039; + + byte strChr_[]; + short strNxt_[]; + short strHsh_[]; + short numStrings_; + + public LZWStringTable() + { + strChr_ = new byte[MAXSTR]; + strNxt_ = new short[MAXSTR]; + strHsh_ = new short[HASHSIZE]; + } + + public int addCharString(short index, byte b) + { + int hshidx; + + if (numStrings_ >= MAXSTR) + return 0xFFFF; + + hshidx = Hash(index, b); + while (strHsh_[hshidx] != HASH_FREE) + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + + strHsh_[hshidx] = numStrings_; + strChr_[numStrings_] = b; + strNxt_[numStrings_] = (index != HASH_FREE) ? index : NEXT_FIRST; + + return numStrings_++; + } + + public short findCharString(short index, byte b) + { + int hshidx, nxtidx; + + if (index == HASH_FREE) + return b; + + hshidx = Hash(index, b); + while ((nxtidx = strHsh_[hshidx]) != HASH_FREE) + { + if (strNxt_[nxtidx] == index && strChr_[nxtidx] == b) + return (short)nxtidx; + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + } + + return (short)0xFFFF; + } + + public void clearTable(int codesize) + { + numStrings_ = 0; + + for (int q = 0; q < HASHSIZE; q++) + { + strHsh_[q] = HASH_FREE; + } + + int w = (1 << codesize) + RES_CODES; + for (int q = 0; q < w; q++) + { + addCharString((short)0xFFFF, (byte)q); + } + } + + static public int Hash(short index, byte lastbyte) + { + return ((int)((short)(lastbyte << 8) ^ index) & 0xFFFF) % HASHSIZE; + } +} + +class LZWCompressor { + + public static void LZWCompress(OutputStream output, int codesize, + byte toCompress[]) throws IOException + { + byte c; + short index; + int clearcode, endofinfo, numbits, limit, errcode; + short prefix = (short)0xFFFF; + + BitFile bitFile = new BitFile(output); + LZWStringTable strings = new LZWStringTable(); + + clearcode = 1 << codesize; + endofinfo = clearcode + 1; + + numbits = codesize + 1; + limit = (1 << numbits) - 1; + strings.clearTable(codesize); + bitFile.writeBits(clearcode, numbits); + + for (int loop = 0; loop < toCompress.length; ++loop) + { + c = toCompress[loop]; + if ((index = strings.findCharString(prefix, c)) != -1) + { + prefix = index; + } + else + { + bitFile.writeBits(prefix, numbits); + if (strings.addCharString(prefix, c) > limit) { + if (++numbits > 12) { + bitFile.writeBits(clearcode, numbits - 1); + strings.clearTable(codesize); + numbits = codesize + 1; + } + limit = (1 << numbits) - 1; + } + + prefix = (short)((short)c & 0xFF); + } + } + + if (prefix != -1) + bitFile.writeBits(prefix, numbits); + + bitFile.writeBits(endofinfo, numbits); + bitFile.flush(); + } +} + +class ScreenDescriptor +{ + public short localScreenWidth_, localScreenHeight_; + private byte byte_; + public byte backgroundColorIndex_, pixelAspectRatio_; + + public ScreenDescriptor(short width, short height, int numColors) + { + localScreenWidth_ = width; + localScreenHeight_ = height; + setGlobalColorTableSize((byte)(BitUtils.bitsNeeded(numColors) - 1)); + setGlobalColorTableFlag((byte)1); + setSortFlag((byte)0); + setColorResolution((byte)7); + backgroundColorIndex_ = 0; + pixelAspectRatio_ = 0; + } + + public void write(OutputStream output) throws IOException + { + BitUtils.writeWord(output, localScreenWidth_); + BitUtils.writeWord(output, localScreenHeight_); + output.write(byte_); + output.write(backgroundColorIndex_); + output.write(pixelAspectRatio_); + } + + public void setGlobalColorTableSize(byte num) + { + byte_ |= (num & 7); + } + + public void setSortFlag(byte num) + { + byte_ |= (num & 1) << 3; + } + + public void setColorResolution(byte num) + { + byte_ |= (num & 7) << 4; + } + + public void setGlobalColorTableFlag(byte num) + { + byte_ |= (num & 1) << 7; + } +} + +class ImageDescriptor +{ + public byte separator_; + public short leftPosition_, topPosition_, width_, height_; + private byte byte_; + + public ImageDescriptor(short width, short height, char separator) + { + separator_ = (byte)separator; + leftPosition_ = 0; + topPosition_ = 0; + width_ = width; + height_ = height; + setLocalColorTableSize((byte)0); + setReserved((byte)0); + setSortFlag((byte)0); + setInterlaceFlag((byte)0); + setLocalColorTableFlag((byte)0); + } + + public void write(OutputStream output) throws IOException + { + output.write(separator_); + BitUtils.writeWord(output, leftPosition_); + BitUtils.writeWord(output, topPosition_); + BitUtils.writeWord(output, width_); + BitUtils.writeWord(output, height_); + output.write(byte_); + } + + public void setLocalColorTableSize(byte num) + { + byte_ |= (num & 7); + } + + public void setReserved(byte num) + { + byte_ |= (num & 3) << 3; + } + + public void setSortFlag(byte num) + { + byte_ |= (num & 1) << 5; + } + + public void setInterlaceFlag(byte num) + { + byte_ |= (num & 1) << 6; + } + + public void setLocalColorTableFlag(byte num) + { + byte_ |= (num & 1) << 7; + } +} + +class BitUtils +{ + public static byte bitsNeeded(int n) + { + byte ret = 1; + + if (n-- == 0) + return 0; + + while ((n >>= 1) != 0) + ++ret; + + return ret; + } + + public static void writeWord(OutputStream output, + short w) throws IOException + { + output.write(w & 0xFF); + output.write((w >> 8) & 0xFF); + } + + static void writeString(OutputStream output, + String string) throws IOException + { + for (int loop = 0; loop < string.length(); ++loop) + output.write((byte)(string.charAt(loop))); + } +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java new file mode 100644 index 0000000..6c8d7ee --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java @@ -0,0 +1,226 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.lang.reflect.Method; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableModel; + +import net.wotonomy.ui.swing.components.PropertyEditorTable; +import net.wotonomy.ui.swing.components.PropertyEditorTableModel; + +/** +* The ObjectInspector displays a JFrame containing +* a PropertyEditorTable that displays an object. <br><br> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + +public class ObjectInspector implements ActionListener, MouseListener +{ + protected JTable table = null; + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + /** + * Displays the specified object in a frame. + */ + public ObjectInspector( Object anObject ) + { + initLayout( anObject ); + } + + protected void initLayout( Object aTargetObject ) + { + PropertyEditorTableModel model = + new PropertyEditorTableModel(); + model.setObject( aTargetObject ); + table = new PropertyEditorTable() + { + public void methodInvoked( Object anObject, Method aMethod, Object aResult ) + { + if + ( ( aResult == null ) + || ( aResult instanceof Number ) + || ( aResult instanceof Boolean ) + || ( aResult instanceof String ) ) + { + System.out.println( aMethod.getName() + ": " + aResult ); + } + else + { + new ObjectInspector( aResult ); + } + } + }; + table.setModel( model ); + table.addMouseListener( this ); // listen for double-clicks + + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 325, 350 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( aTargetObject.getClass().getName() ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + // interface MouseListener + + /** + * Double click to call invokeFileFromString. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + + if ( ( row == -1 ) || ( col != 0 ) ) return; + + /* do something here */ + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + + // interface ActionEventListener - for listening to key commands + + public void actionPerformed(ActionEvent evt) + { + if ( COPY.equals( evt.getActionCommand() ) ) + { + copyToClipboard(); + return; + } + } + +/** +* Copies the contents of the table to the clipboard as a tab-delimited string. +*/ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getTabDelimitedString() ); + clipboard.setContents( selection, selection ); + } + + /** + * Converts the contents of the table to a tab-delimited string. + * @return A String containing the text contents of the table. + */ + public String getTabDelimitedString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = table.getModel(); + int cols = model.getColumnCount(); + int rows = model.getRowCount(); + + Object o = null; + for ( int y = 0; y < rows; y++ ) + { + for ( int x = 0; x < cols; x++ ) + { + o = model.getValueAt( y, x ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\t' ); + } + result.append( '\n' ); + } + + return result.toString(); + } + +} + +/* + * $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.3 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.1.1.1 2000/12/21 15:51:27 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java new file mode 100644 index 0000000..f5fe3e4 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java @@ -0,0 +1,89 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Point; +import java.io.Serializable; +import java.util.Comparator; + +import javax.swing.SwingUtilities; + +/** +* A Comparator that will sort components in a common container +* based first on their y-coordinate and then on their x-coordinate, +* producing a list sorted from top to bottom and left to right. +* If all components are not in the same container, the resulting +* sort is undefined. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PositionComparator implements Comparator, Serializable +{ + private Container rootContainer; + private transient Component c1, c2; + private transient Point p1, p2; + +/** +* Standard constructor to configure the comparator. +* @param aContainer The common container for all the objects to be compared. +*/ + public PositionComparator( Container aContainer ) + { + rootContainer = aContainer; + } + + // interface Comparable + + public int compare(Object o1, Object o2) + { + c1 = (Component) o1; + c2 = (Component) o2; + + p1 = SwingUtilities.convertPoint( c1.getParent(), c1.getLocation(), rootContainer ); + p2 = SwingUtilities.convertPoint( c2.getParent(), c2.getLocation(), rootContainer ); + + if ( p1.y != p2.y ) + { + return p1.y - p2.y; + } + return p1.x - p2.x; + } +} + +/* + * $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.1.1.1 2000/12/21 15:51:27 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java new file mode 100644 index 0000000..7e61411 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java @@ -0,0 +1,457 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; + +import net.wotonomy.ui.swing.components.MultiLineLabel; + +/** +* The StackTraceInspector displays a JFrame containing +* stack trace information for a Throwable. <br><br> +* +* There are also a few static methods for obtaining +* information about the current stack, which is useful +* for determining who's calling you at runtime. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class StackTraceInspector + implements TableModel, MouseListener, ActionListener +{ + protected JTable table = null; + protected List tableModelListeners = null; + protected List methodNames = new ArrayList(); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + +/** +* Displays the current stack trace at the time +* of instantiation in a table on a frame. +*/ + public StackTraceInspector() + { + initLayout( parseStackTrace( new RuntimeException() ), null ); + } + +/** +* Displays the current stack trace at the time +* of instantiation in a table on a frame +* annotated with the specified message. +*/ + public StackTraceInspector( String aMessage ) + { + initLayout( parseStackTrace( new RuntimeException() ), aMessage ); + } + +/** +* Displays the stack trace for the given throwable +* in a table on a frame. +* @param aThrowable A Throwable whose stack will be examined. +*/ + public StackTraceInspector( Throwable aThrowable ) + { + initLayout( parseStackTrace( aThrowable ), + aThrowable.getClass() + ": " + aThrowable.getMessage() ); + } + +/** +* Simply displays the list items in a dialog. +* Presumably (but not necessarily) called from +* the other constructors. (I guess if you just +* want a frame with strings in table, you can +* call this.) +* @param aStringList A List containing Strings. +*/ + public StackTraceInspector( List aStringList ) + { + initLayout( aStringList, null ); + } + + protected void initLayout( List items, String message ) + { + methodNames = new ArrayList( items ); + table = new JTable( this ); // this class is the table model + table.addMouseListener( this ); // listen for double-clicks + + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + if ( message != null ) + { + panel.add( new MultiLineLabel( message ), BorderLayout.NORTH ); + } + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 325, 350 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Stack Trace Inspector" ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + // interface TableModel + + public int getRowCount() + { + return methodNames.size(); + } + + public int getColumnCount() + { + return 1; + } + + public String getColumnName(int columnIndex) + { + switch ( columnIndex ) + { + case 0: + return "Methods"; + case 1: + return "Property"; + } + System.out.println( "StackTraceInspector.getColumnName: unknown column: " + columnIndex ); + return ""; + } + + public Class getColumnClass(int columnIndex) + { + switch ( columnIndex ) + { + case 0: + return String.class; + case 1: + return String.class; + } + System.out.println( "StackTraceInspector.getColumnClass: unknown column: " + columnIndex ); + return Object.class; + } + + public boolean isCellEditable(int rowIndex, + int columnIndex) + { + return false; + } + + public Object getValueAt(int rowIndex, + int columnIndex) + { + return methodNames.get( rowIndex ); + } + + public void setValueAt(Object aValue, + int rowIndex, + int columnIndex) + { + } + + public void addTableModelListener(TableModelListener l) + { + if ( tableModelListeners == null ) + { + tableModelListeners = new ArrayList(); + } + tableModelListeners.add( l ); + } + + public void removeTableModelListener(TableModelListener l) + { + if ( tableModelListeners != null ) + { + tableModelListeners.remove( l ); + } + } + + // interface MouseListener + + /** + * Double click to call invokeFileFromString. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + + if ( ( row == -1 ) || ( col != 0 ) ) return; + + invokeFileFromString( methodNames.get( row ).toString() ); + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + + // interface ActionEventListener - for listening to key commands + + public void actionPerformed(ActionEvent evt) + { + if ( COPY.equals( evt.getActionCommand() ) ) + { + copyToClipboard(); + return; + } + } + +/** +* Copies the contents of the table to the clipboard as a tab-delimited string. +*/ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getSelectedStackString() ); + clipboard.setContents( selection, selection ); + } + +/** +* Converts the selected contents of the table to a string. +* @return A String containing the text contents of the table. +*/ + public String getSelectedStackString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = table.getModel(); + + Object o; + int[] selectedRows = table.getSelectedRows(); + for ( int i = 0; i < selectedRows.length; i++ ) + { + o = model.getValueAt( selectedRows[i], 0 ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\n' ); + } + + return result.toString(); + } + + + // static methods + +/** +* Obtains a list of strings representing the stack trace +* associated with this throwable starting with the most recent call. +* @param aThrowable A Throwable whose stack trace is parsed. +* @return a List containing the method names as Strings. +*/ + static public List parseStackTrace( Throwable aThrowable ) + { + String trace = null; + + // create new stream + ByteArrayOutputStream os = new ByteArrayOutputStream( 256 ); + PrintStream newErr = new PrintStream( os ); + aThrowable.printStackTrace( newErr ); // prints to System.err + + // convert to string + trace = os.toString(); + + List result = new ArrayList(); + + // populate list with parsed trace, starting from top + String token; + StringTokenizer tokens = new StringTokenizer( trace, "\n" ); + tokens.nextToken(); // strip off description of throwable + while ( tokens.hasMoreTokens() ) + { + token = tokens.nextToken(); + if ( token.indexOf( StackTraceInspector.class.getName() ) == -1 ) + { // add only those methods not from this class + + // strip whitespace, "at " from front, and \r from end + token.trim(); + token = token.substring( 4, token.length() - 1 ); + + result.add( token ); + } + } + + return result; + } + +/** +* Convenience method that obtains a String representing +* the caller's caller. +* @return a String representing a method in stack trace format. +*/ + static public String getMyCaller() + { + List trace = parseStackTrace( new RuntimeException() ); + if ( trace.size() > 1 ) + { + return trace.get( 1 ).toString(); + } + + return null; + } + +/** +* Prints a stack trace up to the first method whose fully +* qualified class name begins with "java" to System.out. +*/ + static public void printShortStackTrace() + { + String s; + Iterator i = parseStackTrace( new RuntimeException() ).iterator(); + while ( i.hasNext() ) + { + System.out.println( " " + ( s = i.next().toString() ) ); + if ( s.startsWith( "java" ) ) break; + } + } + + protected void invokeFileFromString( String aString ) + { + // strip off parentheses, if any + int openParam = aString.indexOf( "(" ); + if ( openParam != -1 ) + { + aString = aString.substring( 0, openParam ); + } + + // separate class name from method name + int lastDot = aString.lastIndexOf( "." ); + if ( lastDot == -1 ) return; + String className = aString.substring( 0, lastDot ); + String methodName = aString.substring( lastDot + 1 ); + + // convert "."s to file separator characters + StringBuffer buf = new StringBuffer(); + StringTokenizer tokens = new StringTokenizer( className, "." ); + while ( true ) + { + buf.append( tokens.nextToken() ); + if ( ! tokens.hasMoreTokens() ) break; + buf.append( File.separator ); + } + String path = buf.toString(); + java.net.URL url = ClassLoader.getSystemResource( path + ".java" ); + if ( url == null ) return; // do nothing + + String name = url.getFile(); + + // try to launch the document + try + { + // NOTE: This is Windows-dependent! + String args[] = new String[] { + "cmd", "/c", "\"start \"\" \"" + name.substring(1) + "\"\"" }; + // this translates to: cmd /c "start "" "path"" + // apparently an array is more reliable for calling exec(). + // all the extra quotes are to handle paths with spaces. + // trims off the first "/" before the drive letter. + // needed a dummy title for the console or it wouldn't work. + Runtime.getRuntime().exec( args ); + } + catch ( Exception exc ) + { + System.out.println( "DocumentLinkPanel.invokeDocument: " + exc ); + } + return; + } + +} + +/* + * $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.5 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.3 2001/07/18 21:53:33 mpowers + * Added a string argument for display as a message. + * + * Revision 1.2 2001/07/17 14:01:43 mpowers + * Added short stack trace method. + * + * Revision 1.1.1.1 2000/12/21 15:51:34 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java new file mode 100644 index 0000000..36dbacf --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java @@ -0,0 +1,368 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +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.util; + +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.text.JTextComponent; + +/** +* This class will actively check the inputs of 2 numbers in seperate text +* components. The number in the text components represent an upper and lower +* bound to some range. This class checks to make sure the user inputs values +* in the lower bound text field that are less than the value of the upper +* bound and vice versa for the upper bound text field. This class will also +* check to make sure the bounds fall within a given range if specified. +* +* The checks are automatically performed when the focus is lost on either +* component. If the inputs are correct then no event occurs. If the inputs +* are not correct, then a dialog message is displayed stating the reason why +* the bounds are invalid, and the original correct value is restored into the +* text components. +* +* @author rglista +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TextInputRangeChecker implements FocusListener +{ + protected static final int NONE = 0; + protected static final int LOWER = 1; + protected static final int UPPER = 2; + + private JTextComponent lowerComponent; + private JTextComponent upperComponent; + private double maxRange; + private double lowerNumber; + private double upperNumber; + private Collection focusListeners; + + private String invalidLowerMessage; + private String invalidUpperMessage; + private String invalidEitherMessage; + private String invalidRangeMessage; + + +/** +* Constructor with some of the settable parameters. No range checking is used. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent ) + { + this( aLowerTextComponent, anUpperTextComponent, null, null, 0.0 ); + } + +/** +* Constructor with some of the settable parameters. No range checking is +* used. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param lowerTextName The name of the lower bound, eg - start year. +* @param upperTextName The name of the upper bound, eg - end year. +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + String lowerTextName, String upperTextName ) + { + this( aLowerTextComponent, anUpperTextComponent, lowerTextName, upperTextName, 0.0 ); + } + +/** +* Constructor with some of the settable parameters. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param aMaxRange The range the bounds muist fall between, if 0 then no range +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + double aMaxRange ) + { + this( aLowerTextComponent, anUpperTextComponent, null, null, aMaxRange ); + } + +/** +* Constructor with all the settable parameters. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param lowerTextName The name of the lower bound, eg - start year. +* @param upperTextName The name of the upper bound, eg - end year. +* @param aMaxRange The range the bounds muist fall between, if 0 then no range +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + String lowerTextName, String upperTextName, + double aMaxRange ) + { + lowerComponent = aLowerTextComponent; + upperComponent = anUpperTextComponent; + maxRange = aMaxRange; + + focusListeners = new ArrayList( 1 ); // For most cases, there will be only 1 listener. + + lowerComponent.addFocusListener( this ); + upperComponent.addFocusListener( this ); + + lowerNumber = getNumber( lowerComponent ); + upperNumber = getNumber( upperComponent ); + + if ( ( lowerTextName != null ) && ( upperTextName != null ) ) + { + invalidLowerMessage = "The " + lowerTextName + " must be less than or equal to the " + upperTextName + "."; + invalidUpperMessage = "The " + upperTextName + " must be greater than or equal to the " + lowerTextName + "."; + invalidEitherMessage = "The " + lowerTextName + " and/or the " + upperTextName + " are not correct."; + invalidRangeMessage = "The maximum range for the " + lowerTextName + " and " + upperTextName + " is " + maxRange + "."; + } + else + { + invalidLowerMessage = "The lower bound must be less than or equal to the upper bound."; + invalidUpperMessage = "The upper bound must be greater than or equal to the lower bound."; + invalidEitherMessage = "The upper and/or lower bounds are not correct."; + invalidRangeMessage = "The maximum range is " + maxRange + "."; + } + } + +/** +* Allows the caller to perform the validation of the bounds programatically. +* The lower bound is compared to the upper bound and range checking is performed. +* If the lower bound is greater than the upper bound, or the range between the +* bounds is greater than the max range, then validation fails. +* @return TRUE is validation is successfull, FALSE if it fails. +*/ + public boolean performCheck() + { + return validate( null ); + } + +/** +* Adds the listener to the lists of focus listener maintened by this object. +* When one of the 2 text components receives a focus event, this object will +* fire that focus event to any of its listeners. This is useful when the +* calling object wants to be notified of the components focus events, but wants +* to ensure that the validation has occured first. +* <br><br> +* NOTE: The focus is only fired if the validation was successful. This might +* have to be changed. +* @param aListener A Focus Listener to receive Focus Events. +*/ + public void addFocusListener( FocusListener aListener ) + { + focusListeners.add( aListener ); + } + +/** +* Returns the last valid value of the lower bound. If this is called while +* the user is updating the text component but before the focus is lost, the +* value returned will be the original value before the user started updating +* the bound. +* @return The last valid value of the lower bound. +*/ + public double getLastValidatedLowerNumber() + { + return lowerNumber; + } + +/** +* Returns the last valid value of the upper bound. If this is called while +* the user is updating the text component but before the focus is lost, the +* value returned will be the original value before the user started updating +* the bound. +* @return The last valid value of the upper bound. +*/ + public double getLastValidatedUpperNumber() + { + return upperNumber; + } + +/** +* Method used to be notified when one of the text components has gained its +* focus. +*/ + public void focusGained( FocusEvent e ) + { + lowerNumber = getNumber( lowerComponent ); + upperNumber = getNumber( upperComponent ); + } + +/** +* Method used to be notified when one of the text components has lost its +* focus. Automatic validation occurs here. +*/ + public void focusLost( FocusEvent e ) + { + if ( e.isTemporary() ) + { + return; + } + + if ( validate( e.getSource() ) ) + { + fireFocusEvent( e ); + } + } + +/** +* Fires a focus lost event if the validation was successfull. +*/ + protected void fireFocusEvent( FocusEvent e ) + { + for ( Iterator it = focusListeners.iterator(); it.hasNext(); ) + { + ( ( FocusListener )it.next() ).focusLost( e ); + } + } + +/** +* Validates the bounds inputed by the user. +* @param aComponent The component to use to display a dialog window, if neccessray. +* If null, then the parent window of the text componets will be used. +* @return TRUE if validation was successful, FALSE otherwise. +*/ + protected boolean validate( Object aComponent ) + { + int componentUsed = NONE; + if ( aComponent == lowerComponent ) + { + componentUsed = LOWER; + } + else if ( aComponent == upperComponent ) + { + componentUsed = UPPER; + } + + double lower = getNumber( lowerComponent ); + double upper = getNumber( upperComponent ); + + if ( lower > upper ) + { + if ( componentUsed == LOWER ) + { + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidLowerMessage, lowerComponent ); + } + else if ( componentUsed == UPPER ) + { + upperComponent.setText( Double.toString( upperNumber ) ); + displayMessage( invalidUpperMessage, upperComponent ); + } + else + { + upperComponent.setText( Double.toString( upperNumber ) ); + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidEitherMessage, lowerComponent.getTopLevelAncestor() ); + } + + return false; + } + + if ( maxRange != 0.0 ) + { + if ( ( upper - lower ) > maxRange ) + { + if ( componentUsed == LOWER ) + { + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidRangeMessage, lowerComponent ); + } + else if ( componentUsed == UPPER ) + { + upperComponent.setText( Double.toString( upperNumber ) ); + displayMessage( invalidRangeMessage, upperComponent ); + } + else + { + upperComponent.setText( Double.toString( upperNumber ) ); + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidRangeMessage, lowerComponent.getTopLevelAncestor() ); + } + + return false; + } + } + + lowerNumber = lower; + upperNumber = upper; + return true; + } + +/** +* Creates a JOptionPane to display the reason why the bounds failed validation. +*/ + protected void displayMessage( final String message, final Component parent ) + { + SwingUtilities.invokeLater( new Runnable() + { + public void run() + { + JOptionPane.showMessageDialog( parent, message, "Data Entry Error", + JOptionPane.ERROR_MESSAGE ); + } + } ); + } + +/** +* Gets the number represented in the text component. If the text does not +* represent a number, then zero is returned. +*/ + protected double getNumber( JTextComponent aComponent ) + { + try + { + return Double.valueOf( aComponent.getText() ).doubleValue(); +//1.2 return Double.parseDouble( aComponent.getText() ); + } + catch ( NumberFormatException e ) + { + System.out.println("[GUI] TextInputRangeChecker.getNumber(): The text is NOT a number: " + aComponent.getText() ); + return 0.0; + } + } +} + +/* + * $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.2 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:51:49 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java new file mode 100644 index 0000000..c360105 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java @@ -0,0 +1,203 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.awt.Component; +import java.awt.Image; +import java.awt.Window; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.OutputStream; + +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JWindow; + +/** + * WindowGrabber is a collection of static utility methods + * for taking screen shots of lightweight containers. All + * components that are not native peers will be drawn to a + * bitmap that will be run-length compressed (LZW encoding, GIF format) + * and written to the specified file or output stream. <br><br> + * + * Any Swing/JFC or IFC window is a good candidate for use with these + * methods. The component is not expected to contain more than 256 colors + * (the maximum that GIF allows). While there is a color depth limitation, + * the compression is lossless, with no blurs or bleeding color. + * + * @author michael@mpowers.net + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class WindowGrabber +{ +/** + * Captures the screen contents of the specified component, + * and write it to a file with the specified name. + * + * @param aComponent a lightweight component or container. + * @param aFileName the name of the file to write, optionally preceded by path. + * @return True if the file was successfully written, false if there was an error. + * Errors are written to the standard error stream. + */ + static public boolean grab( Component aComponent, String aFileName ) + { + OutputStream output = null; + try + { + output = new FileOutputStream( aFileName ); + } + catch ( Exception exc ) + { + System.err.println( exc ); + return false; + } + + return grab( aComponent, output ); + } + +/** + * Captures the screen contents of the specified component, + * and write it to a file with the specified name. + * + * @param aComponent a lightweight component or container. + * @param anOutputStream an output stream to which the image will be written. + * @return True if the image was successfully written, false if there was an error. + * Errors are written to the standard error stream. + */ + static public boolean grab( Component aComponent, OutputStream anOutputStream ) + { + Image img = aComponent.createImage( + aComponent.getSize().width, aComponent.getSize().height ); + aComponent.paintAll( img.getGraphics() ); + + try { + GIFEncoder encoder = new GIFEncoder( img ); + encoder.write( anOutputStream ); + anOutputStream.flush(); + } catch ( Exception exc ) { + System.err.println( exc ); + return false; + } + + return true; + } + + protected static void processFilenames( String path, String[] filenames ) + { + ClassLoader loader = new ClassGrabber(); + + File f; + for ( int i = 0; i < filenames.length; i++ ) + { + try + { + f = new File( path + filenames[i] ); + if ( f.isDirectory() ) + { + processFilenames ( + path + filenames[i] + "/", + f.list( new FilenameFilter() + { + public boolean accept(File dir, String name) + { + return name.endsWith( ".class" ); + } + }) + ); + } + else + { + System.out.println( "Loading " + filenames[i] + ": " ); + Class c = loader.loadClass( path + filenames[i] ); + + System.out.println( c ); + + if ( JWindow.class.isAssignableFrom( c ) || + JDialog.class.isAssignableFrom( c ) || + JFrame.class.isAssignableFrom( c ) ) + { + try + { + Window w = (Window) c.newInstance(); + if ( w.getBounds().width * w.getBounds().height == 0 ) + { // if size not specified or set, pack it + w.pack(); + } + String gifName = // replace .class with .gif + filenames[i].substring( + 0, filenames[i].length() - 5 ) + "gif"; + w.addNotify(); + w.repaint(); + grab( w, gifName ); + System.out.println( "wrote: " + gifName ); + } + catch ( Exception exc ) + { + System.err.println( "WindowGrab failed for " + filenames[i] + ": " ); + exc.printStackTrace(); + } + + } + else + { + System.out.println( "not a JWindow, JDialog, or JFrame; ignored." ); + } + } + } + catch ( Exception exc ) + { + System.err.println( filenames[i] + ": " + exc ); + } + } + } + +/** + * Captures images of any Swing window component classes specified + * as parameters. If created, image file will have the same name + * as the corresponding class file, but with a ".gif" extension. + * + * @param argv a list of filenames or directory names. + */ + public static void main( String[] argv ) + { + processFilenames( "", argv ); + System.exit( 0 ); + } + +} + +/* + * $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.1.1.1 2000/12/21 15:51:49 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java new file mode 100644 index 0000000..a36ba12 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java @@ -0,0 +1,521 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.Window; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** +* A collection of window-related utilities. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* +*/ +public class WindowUtilities +{ + +/** +* Place frame at center (vertically and horizontally) of screen. +*/ + public static final int CENTER = 0; + +/** +* Center dialog on frame area of parent, if any. +*/ + public static final int CENTER_PARENT = 1; + +/** +* Place lower and to the right of the last window +* placed in this manner. Will wrap to top and left +* of screen as necessary. +*/ + public static final int CASCADE = 10; + + // cascade state + private static int lastX = 0; + private static int lastY = 0; + private static int incrementX = 20; + private static int incrementY = 20; + +/** +* Place the window in the center of the screen. +* Note: don't forget to first set the size of your window. +* This is a convenience method and simply calls place() with +* the CENTER parameter. +* @param aWindow The window to be centered. +* @see #place +*/ + public static void center( Window aWindow ) + { + place( aWindow, CENTER ); + } + +/** +* Place lower and to the right of the last window +* placed in this manner. Will wrap to top and left +* of screen as necessary. +* This is a convenience method and simply calls place() with +* the CASCADE parameter. +* @param aWindow The window to be cascaded. +* @see #place +*/ + public static void cascade( Window aWindow ) + { + place( aWindow, CASCADE ); + } + +/** +* Place the window in the specified location. +* Note: don't forget to first set the size of your window. +* @param aWindow The window to be placed. +* @param location Where on screen to place the frame. +*/ + public static void place( Window aWindow, int location) + { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Dimension mySize = aWindow.getSize(); + int x = (aWindow.getLocation()).x; + int y = (aWindow.getLocation()).y; + float aspectRatio = (float)screenSize.height/(float)screenSize.width; + + // hack to make windows appear on left monitor if dual monitor + // if aspect ratio is less than 0.6, assume dual monitor + if (aspectRatio < 0.6) + { + screenSize.width = screenSize.width/2; + } + + switch (location) + { + case CENTER_PARENT: + if ( ( ! ( aWindow instanceof Dialog ) ) + || ( ((Dialog)aWindow).getParent() == null ) ) +//1.2 || ( ((Dialog)aWindow).getOwner() == null ) ) + { + place( aWindow, CENTER ); + return; + } + Point parentLocation = (((Dialog)aWindow).getParent()).getLocation(); +//1.2 (((Dialog)aWindow).getOwner()).getLocation(); + Dimension parentSize = (((Dialog)aWindow).getParent()).getSize(); + +//1.2 Dimension parentSize = (((Dialog)aWindow).getOwner()).getSize(); + + if (parentSize.width > mySize.width) + { + x = ((parentSize.width - mySize.width)/2) + parentLocation.x; + } + if (parentSize.height > mySize.height) + { + y = ((parentSize.height - mySize.height)/2) + parentLocation.y; + } + break; + case CENTER: + if (screenSize.width > mySize.width) + { + x = (screenSize.width - mySize.width)/2; + } + if (screenSize.height > mySize.height) + { + y = (screenSize.height - mySize.height)/2; + } + break; + case CASCADE: + x = lastX + incrementX; + if ( x + mySize.width > screenSize.width ) + { + x = incrementX; + } + y = lastY + incrementY; + if ( y + mySize.height > screenSize.height ) + { + y = incrementY; + } + lastX = x; + lastY = y; + break; + default: + // don't move the frame + Point p = aWindow.getLocation(); + x = p.x; + y = p.y; + break; + } + aWindow.setLocation(x,y); + } + +/** + * Returns the first parent Window of the specified component. + * + * @param c the Component whose parent will be found. + * @return the Window that contains the component, + * or null if the component does not have a valid Frame parent. + */ + public static Window getWindowForComponent(Component c) + { + for(Component p = c; p != null; p = p.getParent()) { + if (p instanceof Window) { + return (Window) p; + } + } + return null; + } + + + +/** +* Prints out a list of all components to System.out. +* @param aContainer the Container whose components will be listed. +*/ + public static void dumpComponents( Container aContainer ) + { + dumpComponents( aContainer, "" ); + } + + protected static void dumpComponents( Container aContainer, String padding ) + { + Component c = null; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + if ( c instanceof javax.swing.JComponent ) + { + System.out.println( padding + c.getClass() + ": " + + ((javax.swing.JComponent)c).getAccessibleContext().getAccessibleName() ); + } + else + { + System.out.println( padding + c.getClass() + ": " + c.getName() ); + } + if ( c instanceof Container ) + { + dumpComponents( (Container) c, padding + " " ); + } + } + } + +/** +* Gets a list of all children of a specified container, sorted by position. +* Components are sorted from top to bottom and then left to right. +* @param aContainer The container whose children are to be returned. +* @result A List containing the sorted components. +*/ + public static List getSortedChildComponents( Container aContainer ) + { + List result = new ArrayList( getAllChildComponents( aContainer ) ); + Collections.sort( result, new PositionComparator( aContainer ) ); + return result; + } + + public static void dumpSortedChildComponents( Container aContainer ) + { + Component c = null; + Iterator it = getSortedChildComponents( aContainer ).iterator(); + while ( it.hasNext() ) + { + c = (Component) it.next(); + System.out.println( c.getLocation() + " : " + c.getClass() ); + } + } + + public static void dumpNamedChildComponents( Container aContainer ) + { + Iterator it = getUniqueNameMap( getSortedChildComponents( aContainer ) ).values().iterator(); + while ( it.hasNext() ) + { + System.out.println( it.next() ); + } + } + +/** +* Generates a unique name for each object in a list and returns +* a map that maps the objects to the names. The name is based +* on the class of the object in the object list. +* @param anObjectList A List of objects. +* @return A Map that maps the objects in the list to the generated names. +*/ + public static Map getUniqueNameMap( List anObjectList ) + { + Map namesToObjects = new HashMap(anObjectList.size(), 1F); + Map objectsToNames = new HashMap(anObjectList.size(), 1F); + + Object o = null; + String name = null; + int lastIndex = 0; + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + name = o.getClass().getName(); + lastIndex = name.lastIndexOf( "." ); + if ( lastIndex != -1 ) + { + name = name.substring( lastIndex+1 ); + } + name = incrementString( name ); + while ( namesToObjects.get( name ) != null ) + { + name = incrementString( name ); + } + namesToObjects.put( name, o ); + objectsToNames.put( o, name ); + } + + return objectsToNames; + } + +/** +* Numerically increments a string. For example, "hello" becomes "hello1" +* and "hello1" becomes "hello2" while "hello999" becomes "hello1000". +* @param aString a String to be incremented. +* @return The incremented String. +*/ + public static String incrementString( String aString ) + { + int i = aString.length()-1; + while ( ( i >= 0 ) && ( Character.isDigit( aString.charAt( i ) ) ) ) + { + i--; + } + + if ( i == aString.length()-1 ) + { // no numerics at end of string, increment manually + return aString + "1"; + } + + String alpha = aString.substring( 0, i+1 ); + String numeric = aString.substring( i+1 ); + numeric = Integer.toString( Integer.parseInt( numeric ) + 1 ); + + return alpha + numeric; + + } + +/** +* Gets all children of the specified container. +* @param aContainer the Container to be searched. +* @return A Collection containing all of the child components of the container. +*/ + public static Collection getAllChildComponents( Container aContainer ) + { + Collection result = new ArrayList(); + addAllChildComponents( aContainer, result ); + return result; + } + +/** +* Adds all children of the specified container to the specified collection. +* @param aContainer the Container to be searched. +* @param aCollection the Collection to which the child components will be added. +*/ + protected static void addAllChildComponents( Container aContainer, Collection aCollection ) + { + Component c = null; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + aCollection.add( c ); + if ( c instanceof Container ) + { + addAllChildComponents( (Container) c, aCollection ); + } + } + } + +/** +* Sets each child component's tooltip to show the name generated from +* getComponentNameMap(). +* (We're doing this so the tooltip authors can know how to reference +* the components.) +* @param aContainer the Container whose components will be labeled. +*/ + public static void labelComponents( Container aContainer ) + { + Map nameToComponent = getNameToComponentMap( aContainer ); + Map nameToName = new HashMap(nameToComponent.size(), 1F); + Iterator it = nameToComponent.keySet().iterator(); + String key; + while ( it.hasNext() ) + { + key = it.next().toString(); + nameToName.put( key, key ); + } + labelComponents( aContainer, nameToName ); + } + +/** +* Sets each child component's tooltip to show a given string retrieved +* from a map using the component's generated name as a key. +* @param aContainer the Container whose components will be labeled. +* @param aNameMap a Map of generated names to string values. +*/ + public static void labelComponents( Container aContainer, Map aNameMap ) + { + if ( aNameMap == null ) return; + + String key; + Object o ; + Iterator it = aNameMap.keySet().iterator(); + Map nameToComponent = getNameToComponentMap( aContainer ); + while ( it.hasNext() ) + { + key = it.next().toString(); + o = nameToComponent.get( key ); + if ( o instanceof javax.swing.JComponent ) + { + ((javax.swing.JComponent)o).setToolTipText( aNameMap.get( key ).toString() ); + } + } + } + +/** +* Generates a deterministically unique name for each component in a +* container. The name is based on the name of the class of the component +* followed by a number. Each class of component is numbered based on it's +* position in the container, sorted from top to bottom and left to right. +* @param aContainer the Container whose components will named. +* @return a Map that maps each component to its name. +*/ + public static Map getComponentToNameMap( Container aContainer ) + { + return getUniqueNameMap( getSortedChildComponents( aContainer ) ); + } + +/** +* Maps a deterministically unique name to a component in a +* container. The name is based on the name of the class of the component +* followed by a number. Each class of component is numbered based on it's +* position in the container, sorted from top to bottom and left to right. +* @param aContainer the Container whose components will named. +* @return a Map that maps each component to its name. +*/ + public static Map getNameToComponentMap( Container aContainer ) + { + Map componentToName = getComponentToNameMap( aContainer ); + Map result = new HashMap(componentToName.size(), 1F); + Iterator it = componentToName.keySet().iterator(); + Object key; + while ( it.hasNext() ) + { + key = it.next(); + result.put( componentToName.get( key ), key ); + } + return result; + } + +/** +* Sets the tooltips of all components in a container to the +* respective names of those components. (We're using this +* so the tooltip authors can know how to reference the components.) +* @param aContainer the Container whose components will be labeled. +*/ + public static void nameComponents( Container aContainer ) + { + nameComponents( aContainer, "" ); + } + + protected static void nameComponents( Container aContainer, String path ) + { + Component c = null; + String className = null; + int index = 0; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + className = c.getClass().getName(); + className = className.substring( className.lastIndexOf( '.' ) + 1 ); + System.out.println( path + className ); + if ( c instanceof javax.swing.JComponent ) + { + // ((javax.swing.JComponent)c).setToolTipText( path + className + " (" + c.getName() + ")" ); + ((javax.swing.JComponent)c).setToolTipText( c.getName() ); + } + if ( c instanceof Container ) + { + nameComponents( (Container) c, path + className + "." ); + } + } + } + +/** +* Sets the enabled state of a container and all of its components. +* @param aContainer the Container whose components will be enabled. +* @param isEnabled True if enabled, false id disabled. +*/ + public static void enableComponents( Container aContainer, boolean isEnabled ) + { + Component c = null; + String className = null; + int index = 0; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + if ( c instanceof Container ) + { + enableComponents( (Container) c, isEnabled ); + } + else + { + c.setEnabled( isEnabled ); + } + } + aContainer.setEnabled( isEnabled ); + } + +} + +/* + * $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.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:51:55 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html new file mode 100644 index 0000000..97f1598 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html @@ -0,0 +1,6 @@ +<body> +<p> +Contains utilities that ease Swing development +but don't quite qualify as components. +</p> +</body> |
