summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components
diff options
context:
space:
mode:
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components')
-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
31 files changed, 10406 insertions, 0 deletions
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>