From aedc34d55462a75e329bbf342251ff6504cd117e Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 19 May 2024 17:56:33 -0400 Subject: Initial import from SVN --- .../net/wotonomy/ui/swing/TableAssociation.java | 927 +++++++++++++++++++++ 1 file changed, 927 insertions(+) create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java') 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: +* +* +* @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. + * 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. + * + * + */ + -- cgit v1.2.3