diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
| commit | aedc34d55462a75e329bbf342251ff6504cd117e (patch) | |
| tree | bcc8f1f2352582717b484df302aeea6696b8f000 /projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components | |
Initial import from SVN
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components')
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> |
