summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src
diff options
context:
space:
mode:
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src')
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java335
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java327
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java444
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java700
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java613
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java134
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java120
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java1518
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java368
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java216
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java333
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java457
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java283
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java419
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java927
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java708
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java1212
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java1029
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java582
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java331
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java1751
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java74
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java335
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java129
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java515
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java274
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java123
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java610
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java272
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java84
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java81
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java57
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java630
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java284
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java845
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java104
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java1693
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java188
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java350
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java154
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java135
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java434
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java572
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java418
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java174
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java274
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java244
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java276
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java100
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java727
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java224
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html26
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html21
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java126
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java160
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java520
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java226
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java89
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java457
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java368
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java203
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java521
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html6
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>