/*
Copyright (c) 2007 John C. Gunther. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, are permitted provided that the
following conditions are met:
- Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
- Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
- Neither the name of the JComponentBreadboard Consortium nor
the names of its contributors may be used to endorse or
promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITURE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Note: the license conditions above were copied from the
BSD open source license template available at
http://www.opensource.org.licenses/bsd-license.php.
*/
package net.sourceforge.jcomponentbreadboard;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.lang.reflect.*;
import java.beans.*;
/**
*
* Represents all aspects of a Java/Swing form--from layout management to how the
* form is connected to the objects it views and controls.
* For more information including detailed examples, see the
* {@link net.sourceforge.jcomponentbreadboard
* package description} and the
* JComponentBreadboard User's Guide .
*
* @author John C. Gunther
* @version 1.0.1 (requires JDK 1.5 or higher)
*
*/
@SuppressWarnings("serial")
public class JComponentBreadboard extends JPanel {
// When a single JComponent ref, possibly with a longish name, is mapped
// into a large sub-block of the breadboard array, the resulting
// verbiage makes it hard to read the breadboard array. Use of "" to
// refer to the object ref in the cell immediately above, and __ for the
// ref immediately to the left (the "ColumnarDitto"), can often increase
// the readability of tbe breadboard array, as well as making it easier
// both to enter and edit such arrays (name changes, for example,
// require 1 replacement, not 100)
private final static class ColumnarDitto {
public static final ColumnarDitto COLUMNAR_DITTO = new ColumnarDitto();
private ColumnarDitto() {}
};
/** A directive that, when placed into the cell of a breadboard array,
** instructs JComponentBreadboard to use the object reference immediately
** to the left of this cell for this cell, too. In other words, a kind
** of across columns ditto mark.
**
**
** The odd name for this keyword ("__") is meant to suggest an
** ellipsis or "fill in the blank", to save space, and to make the
** breadboard array easier to read.
**
**
** @see #setBreadboard setBreadboard
** @see #ROWWISE_DITTO ROWWISE_DITTO
**
**/
public static final ColumnarDitto __ = ColumnarDitto.COLUMNAR_DITTO;
/** A directive that, when placed into the cell of a breadboard array,
** instructs JComponentBreadboard to use the object reference immediately
** above this cell for this cell, too. In other words, a
** conventional ditto mark. This constant is equal to the String literal "", and the
** intention is that the "" String literal,
** rather than this constant, will be placed onto actual breadboard arrays both
** to save space and also for its mnemonic significance (when interpreted as a ditto mark).
**
** @see #setBreadboard setBreadboard
** @see #__ "columnar ditto"
**/
public final String ROWWISE_DITTO = "";
private final static String JCB_PROPERTY_PREFIX = "JCB_";
// This randomly generated name suffix assures no one will choose
// the same client property names, or keyword strings, as we do.
private final static String JCB_PROPERTY_SUFFIX = "_wVs0Xe1oH7Ou5juhuf9w";
private static String jcbPropertyName(String sName) {
return JCB_PROPERTY_PREFIX + sName + JCB_PROPERTY_SUFFIX;
}
// special keyword strings that can be returned from getXXXInvalidDataMessage() methods
/**
** Special keyword returned by data validation methods to indicate that the data
** String passed to the method contains valid data. See the section titled
** Data Validation Method Signatures within the {@link
** #jbConnect(JComponent,String) jbConnect} method's javadocs for more
** information. For an example that uses DATA_IS_VALID within a data validation
** method that validates a numeric entry field, see the NumericInputDialog
** application of the JComponentBreadboard User's Guide.
**
** @see #REVERT_QUIETLY REVERT_QUIETLY
** @see #REVERT_AND_BEEP REVERT_AND_BEEP
** @see #DEFAULT_INVALID_DATA_MESSAGE_TITLE DEFAULT_INVALID_DATA_MESSAGE_TITLE
** @see #jbConnect(JComponent,String) jbConnect
**
**/
public final static String DATA_IS_VALID = jcbPropertyName("DATA_IS_VALID");
/** Special keyword returned by data validation methods to indicate that
** the data String passed to the method contains invalid data, and
** that you want the field to reject this input and quietly revert back
** to the last validated entry.
**
** @see #DATA_IS_VALID DATA_IS_VALID
** @see #REVERT_AND_BEEP REVERT_AND_BEEP
** @see #DEFAULT_INVALID_DATA_MESSAGE_TITLE DEFAULT_INVALID_DATA_MESSAGE_TITLE
** @see #jbConnect(JComponent,String) jbConnect
**
**/
public final static String REVERT_QUIETLY = jcbPropertyName("REVERT_QUIETLY");
/** Special keyword returned by data validation methods to indicate that
** the data String passed to the method contains invalid data, and
** that you want the field to reject this input and, after beeping for
** feedback, revert the data back to the last validated entry.
**
** @see #DATA_IS_VALID DATA_IS_VALID
** @see #REVERT_QUIETLY REVERT_QUIETLY
** @see #DEFAULT_INVALID_DATA_MESSAGE_TITLE DEFAULT_INVALID_DATA_MESSAGE_TITLE
** @see #jbConnect(JComponent,String) jbConnect
**
**/
public final static String REVERT_AND_BEEP = jcbPropertyName("REVERT_AND BEEP");
// ScalingDirective defines the four constants used to define how each
// row and column of a JComponentBreadboard get scaled up or down in
// response to surplus or deficit space in the parent container.
private static final class ScalingDirective {
private boolean shrinks;
private boolean expands;
private ScalingDirective(boolean shrinks, boolean expands) {
this.shrinks = shrinks;
this.expands = expands;
}
public final static ScalingDirective NOSCALE = new ScalingDirective(false, false);
public final static ScalingDirective EXPANDS = new ScalingDirective(false, true);
public final static ScalingDirective SHRINKS = new ScalingDirective(true, false);
public final static ScalingDirective BISCALE = new ScalingDirective(true, true);
// Does the column (row) width (height) shrink down uniformly from its
// "tightest fitting to preferred sizes grid" width (height) in order to fit
// the component grid into a too-small parent container?
public boolean shrinks() {
return shrinks;
}
// Does the column (row) width (height) expand up uniformly from its
// "tightest fitting to preferred sizes grid" width (height) in order to use up
// any extra space available in the parent container?
public boolean expands() {
return expands;
}
};
/**
** Columns (rows) of the breadboard array whose headers contain this
** directive do not uniformly scale their preferred-size-based
** widths (heights) up when extra width (height) is available in the
** parent container, and also do not uniformly scale down when
** the parent container is too small.
**
** @see #setBreadboard setBreadboard
** @see #SHRINKS SHRINKS
** @see #EXPANDS EXPANDS
** @see #BISCALE BISCALE
**
**/
public final static ScalingDirective NOSCALE = ScalingDirective.NOSCALE;
/**
** Columns (rows) of the breadboard array whose headers contain this
** directive uniformly scale their preferred-size-based widths (heights)
** up when extra width (height) is available in the parent container,
** but do not uniformly scale down when the parent container
** is too small.
**
** @see #setBreadboard setBreadboard
** @see #NOSCALE NOSCALE
** @see #SHRINKS SHRINKS
** @see #BISCALE BISCALE
**
**/
public final static ScalingDirective EXPANDS = ScalingDirective.EXPANDS;
/**
** Columns (rows) of the breadboard array whose headers contain this
** directive do not scale their preferred-size-based widths
** (heights) up when extra width (height) is available, but do scale
** down uniformly when the parent container is too small.
**
**
** @see #setBreadboard setBreadboard
** @see #NOSCALE NOSCALE
** @see #EXPANDS EXPANDS
** @see #BISCALE BISCALE
**/
public final static ScalingDirective SHRINKS = ScalingDirective.SHRINKS;
/**
** Columns (rows) of the breadboard array whose headers contain this
** directive both uniformly scale their preferred-size-based widths
** (heights) up when extra width (height) is available in the parent
** container, and also uniformly scale down when the parent container
** is too small.
**
** @see #setBreadboard setBreadboard
** @see #NOSCALE NOSCALE
** @see #SHRINKS SHRINKS
** @see #EXPANDS EXPANDS
**
**/
public final static ScalingDirective BISCALE = ScalingDirective.BISCALE;
// default scaling directive when all scaling directive headers are elided
private final static ScalingDirective DEFAULT_SCALING_DIRECTIVE = NOSCALE;
// These hold references to the constants that define scaling directives
// (see above) for each row and column in the breadboard array.
private ScalingDirective[] rowScale = null;
private ScalingDirective[] colScale = null;
// returns the component-type based default for the fill directive
private static final int XFILL = 0;
private static final int YFILL = 1;
private static boolean[] defaultFill(JComponent jc) {
boolean[] result = new boolean[YFILL+1];
result[XFILL] = false;
result[YFILL] = false;
if (hasMethod(jc, "getOrientation", int.class, null, 0)) {
int orientation = ((Integer) getMethodValue(jc,"getOrientation", null)).intValue();
if (SwingConstants.VERTICAL == orientation)
result[YFILL] = true;
else if (SwingConstants.HORIZONTAL == orientation)
result[XFILL] = true;
}
return result;
}
private static boolean defaultXFill(JComponent jc) {
boolean result = defaultFill(jc)[XFILL];
return result;
}
private static boolean defaultYFill(JComponent jc) {
boolean result = defaultFill(jc)[YFILL];
return result;
}
// name for the "fill", etc. client property recognized by JComponentBreadboard
// (other properties, like xAlign, are stored in existing JComponent properties)
private final static String X_FILL_PROPNAME = jcbPropertyName("xFill");
private final static String Y_FILL_PROPNAME = jcbPropertyName("yFill");
private final static String X_UPSIZE_PROPNAME = jcbPropertyName("xUpsize");
private final static String Y_UPSIZE_PROPNAME = jcbPropertyName("yUpsize");
/**
** Defines whether or not the component should always expand to fill out any
** extra horizontal space that may be available in the
** breadboard grid cell(s) that it occupies.
**
**
** For most JComponents , the default if not specified is not to fill.
** The exception is for horizontally oriented JComponents (horizontally
** oriented JScrollbars , JSliders , etc.--any
** JComponent with a getOrientation method that returns
** SwingConstants.HORIZONTAL ) which fill by default.
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param jc the JComponent whose horizontal fill attribute is to be specified.
** @return the JComponent whose fill attribute was specified (jc ).
**
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
** @see #setBreadboard setBreadboard
**
**/
public static JComponent xFill(boolean isXFilled, JComponent jc) {
if (null != jc)
jc.putClientProperty(X_FILL_PROPNAME, new Boolean(isXFilled));
return jc;
}
/**
** Convenience method equivalent to xFill(true, jc)
**
** @param jc the JComponent whose horizontal fill attribute is set to true .
** @return the JComponent reference passed in as its argument.
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static JComponent xFill(JComponent jc) {
return xFill(true, jc);
}
/**
** Convenience method equivalent to applying xFill(isXFilled, jca[i])
** to every non-null element of the given 1-D array.
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param jca the JComponent array containing the elements operated upon.
** @return the array reference passed in as its second argument
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static JComponent[] xFill(boolean isXFilled, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xFill(isXFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to applying xFill(true, jca[i])
** to every non-null element of the given 1-D array.
**
** @param jca the JComponent array containing the elements operated upon.
** @return the array reference passed in as its argument
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static JComponent[] xFill(JComponent[] jca) {
return xFill(true, jca);
}
/**
** Convenience method equivalent to applying xFill(isXFilled, jca[i][j])
** to every non-null element of the given 2-D array.
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param jca the JComponent array containing the elements operated upon.
** @return the array reference passed in as its second argument.
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static JComponent[][] xFill(boolean isXFilled, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xFill(isXFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to applying xFill(true, jca[i][j])
** to every non-null element of the given 2-D array.
**
** @param jca the JComponent array containing the elements operated upon.
** @return the array reference passed in as its argument
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static JComponent[][] xFill(JComponent[][] jca) {
return xFill(true, jca);
}
/**
** Defines whether or not the component should always expand to fill out any
** extra vertical space that may be available in the
** breadboard grid cell(s) that it occupies.
**
** For most JComponents , the default if not specified is not to fill.
** The exception is for vertically oriented JComponents (vertically
** oriented JScrollbars , JSliders , etc.--any JComponent with
** a getOrientation method that returns SwingConstants.VERTICAL )
** which fill by default.
**
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jc the JComponent whose vertical fill attribute is to be specified.
** @return the JComponent whose fill attribute was specified (jc ).
**
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
** @see #setBreadboard setBreadboard
**
**/
public static JComponent yFill(boolean isYFilled, JComponent jc) {
if (null != jc)
jc.putClientProperty(Y_FILL_PROPNAME, isYFilled);
return jc;
}
/**
** Convenience method equivalent to yFill(true, jc)
**
** @param jc the JComponent whose vertical fill attribute is to be specified.
** @return the JComponent whose fill attribute was specified (jc ).
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent yFill(JComponent jc) {
return yFill(true,jc);
}
/**
** Convenience method equivalent to applying yFill(isYFilled, jca[i])
** to every non-null element of the given 1-D array.
**
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent[] yFill(boolean isYFilled, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yFill(isYFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to applying yFill(true, jca[i])
** to every non-null element of the given 1-D array.
**
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent[] yFill(JComponent[] jca) {
return yFill(true, jca);
}
/**
** Convenience method equivalent to applying yFill(isYFilled, jca[i][j])
** to every non-null element of the given 2-D array.
**
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent[][] yFill(boolean isYFilled, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yFill(isYFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to applying yFill(true, jca[i][j])
** to every non-null element of the given 2-D array.
**
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent[][] yFill(JComponent[][] jca) {
return yFill(true, jca);
}
/**
** Convenience method equivalent to
** xFill(isXFilled,yFill(isYFilled, jc))
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jc the JComponent operated upon
** @return the JComponent operated upon
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent xyFill(boolean isXFilled, boolean isYFilled, JComponent jc) {
return xFill(isXFilled, yFill(isYFilled, jc));
}
/**
** Convenience method equivalent to
** xFill(true,yFill(true, jc))
**
** @param jc the JComponent operated upon
** @return the JComponent operated upon
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static JComponent xyFill(JComponent jc) {
return xyFill(true, true, jc);
}
/**
** Convenience method equivalent to applying
** xyFill(isXFilled,isYFilled, jca[i]) to every non-null element of the
** given 1-D array.
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #xyFill(boolean, boolean, JComponent) xyFill(boolean, boolean, JComponent)
**
**/
public static JComponent[] xyFill(boolean isXFilled, boolean isYFilled, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xyFill(isXFilled, isYFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to
** xyFill(true, true, jca)
**
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #xyFill(boolean, boolean, JComponent[]) xyFill(boolean, boolean, JComponent[])
**
**/
public static JComponent[] xyFill(JComponent[] jca) {
return xyFill(true, true, jca);
}
/**
** Convenience method equivalent to applying
** xyFill(isXFilled, isYFilled, jca[i][j]) to every non-null element of the
** given 2-D array
**
** @param isXFilled true if horizontal filling is to be enabled, false if not.
** @param isYFilled true if vertical filling is to be enabled, false if not.
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #xyFill(boolean, boolean, JComponent) xyFill(boolean, boolean, JComponent)
**
**/
public static JComponent[][] xyFill(boolean isXFilled, boolean isYFilled, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xyFill(isXFilled, isYFilled, jca[i]);
return jca;
}
/**
** Convenience method equivalent to xyFill(true, true, jca) .
**
** @param jca the JComponent array whose elements are operated upon
** @return the JComponent array whose elements were operated upon
** @see #xyFill(boolean, boolean, JComponent[][]) xyFill(boolean, boolean, JComponent[][])
**
**/
public static JComponent[][] xyFill(JComponent[][] jca) {
return xyFill(true, true, jca);
}
/**
** Returns true if the an attribute to expand the JComponent's width to fill
** out the grid cells that contain it horizontally has been attached to the
** JComponent, false otherwise.
**
** Such attributes are attached via the xFill method.
**
** @param jc the JComponent whose xFill attribute is to be returned
** @return true if horizontal filling is enabled for the component, else false .
** @see #xFill(boolean, JComponent) xFill(boolean, JComponent)
**
**/
public static boolean isXFilled(JComponent jc) {
Boolean result = (Boolean) jc.getClientProperty(X_FILL_PROPNAME);
if (null == result)
result = defaultXFill(jc);
return result;
}
/**
** Returns true if an attribute to expand the JComponent's height to fill
** out the grid cells that contain it vertically has been attached to the
** JComponent, false otherwise.
**
** Such attributes are attached via the yFill method.
**
** @param jc the JComponent whose yFill attribute is to be returned
** @return true if vertical filling is enabled for the component, else false .
** @see #yFill(boolean, JComponent) yFill(boolean, JComponent)
**
**/
public static boolean isYFilled(JComponent jc) {
Boolean result = (Boolean) jc.getClientProperty(Y_FILL_PROPNAME);
if (null == result)
result = defaultYFill(jc);
return result;
}
/**
** Declares that, in situations in which the JComponent's (possibly
** xUpsize adjusted) preferred width
** is smaller than the width of the breadboard grid-cell-block that contains it,
** the JComponent should be positioned the specified fraction of the way
** between the extreme flush left and extreme flush right positions.
**
** For example, a fraction of 0 means flush left, a fraction of 1 flush right,
** and a fraction of 0.5 means horizontally centered.
**
** Note: this method changes the JComponent's xAlignment property
** via a call to jc.setXAlignment(fraction) .
**
**
** @param fraction the fraction of the way between the extreme left and extreme
** right positions within the breadboard grid-cell-block that contains the
** component at which to position the component horizontally.
** @param jc the JComponent to receive this horizontal alignment attribute
** @return the JComponent passed in as the last argument (jc ), whose
** x alignment has been modified.
**
** @see #setBreadboard setBreadboard
** @see #xUpsize(int,JComponent) xUpsize
** @see #yAlign(double,JComponent) yAlign
**
**/
public static JComponent xAlign(double fraction, JComponent jc) {
if (fraction < 0) throw new IllegalArgumentException(
"fraction = " + fraction + "; fraction arg cannot be negative.");
else if (fraction > 1) throw new IllegalArgumentException(
"fraction = " + fraction + "; fraction arg cannot be greater than 1.");
else if (null != jc) {
jc.setAlignmentX((float) fraction);
}
return jc;
}
/**
** Convenience method that issues xAlign(fraction, jca[i])
** to set the xAlign fraction on every non-null element of a 1-D array of
** JComponents .
**
** @param fraction the fraction of the way between the extreme left and extreme
** right positions within the breadboard grid-cell-blocks that contain the
** components at which to position the components horizontally.
** @param jca contains the JComponents to receive this horizontal alignment attribute
** @return the array reference passed in as its second argument
**
** @see #xAlign(double, JComponent) xAlign(double, JComponent)
**
**/
public static JComponent[] xAlign(double fraction, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xAlign(fraction, jca[i]);
return jca;
}
/**
** Convenience method that issues xAlign(fraction, jca[i][j])
** to set the xAlign fraction on every non-null element of a 2-D array of
** JComponents .
**
** @param fraction the fraction of the way between the extreme left and extreme
** right positions within the breadboard grid-cell-blocks that contain the
** components at which to position the components horizontally.
** @param jca contains the JComponents to receive this horizontal alignment attribute
** @return the array reference passed in as its second argument.
**
** @see #xAlign(double, JComponent) xAlign(double, JComponent)
**
**/
public static JComponent[][] xAlign(double fraction, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xAlign(fraction, jca[i]);
return jca;
}
/**
** Declares that, in situations in which the JComponent's (possibly
** yUpsize adjusted) preferred height
** is smaller than the height of the breadboard grid-cell-block that contains it,
** the JComponent should be positioned the specified fraction of the way
** between the extreme top and extreme bottom positions.
**
** For example, a fraction of 0 means flush against the top edge,
** a fraction of 1 flush against the bottom edge,
** and a fraction of 0.5 means vertically centered.
**
** Note: this method changes the JComponent's yAlignment property
** via a call to jc.setYAlignment(fraction) .
**
** @see #setBreadboard setBreadboard
** @see #yUpsize(int,JComponent) yUpsize
** @see #xAlign(double,JComponent) xAlign
**
** @param fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-block that contains the
** component) at which to position the component vertically.
** @param jc the JComponent to receive this vertical alignment attribute
** @return the JComponent passed in as the last argument, whose
** y alignment has been modified.
**
**
**
**/
public static JComponent yAlign(double fraction, JComponent jc) {
if (fraction < 0) throw new IllegalArgumentException(
"fraction = " + fraction + "; fraction arg cannot be negative.");
else if (fraction > 1) throw new IllegalArgumentException(
"fraction = " + fraction + "; fraction arg cannot be greater than 1.");
else if (null != jc) {
jc.setAlignmentY((float) fraction);
}
return jc;
}
/**
** Convenience method that issues yAlign(fraction, jca[i])
** to set the yAlignment of every non-null element of a 1-D array of
** JComponents .
**
** @param fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components vertically.
** @param jca contains the JComponents to receive this vertical alignment attribute
** @return the array reference passed in as its second argument
**
** @see #yAlign(double, JComponent) yAlign(double, JComponent)
**
**/
public static JComponent[] yAlign(double fraction, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yAlign(fraction, jca[i]);
return jca;
}
/**
** Convenience method that issues yAlign(fraction, jca[i][j])
** to set the yAlignment of every non-null element of a 2-D array of
** JComponents .
**
** @param fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components vertically.
** @param jca contains the JComponents to receive this vertical alignment attribute
** @return the array reference passed in as its second argument
**
** @see #yAlign(double, JComponent) yAlign(double, JComponent)
**
**/
public static JComponent[][] yAlign(double fraction, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yAlign(fraction, jca[i]);
return jca;
}
/**
** Convenience method equivalent to xAlign(xFraction, yAlign(yFraction, jc))
**
** @param xFraction the fraction of the way between the extreme left and extreme
** right positions (within the breadboard grid-cell-block that contains the
** component) at which to position the component horizontally.
** @param yFraction the fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-block that contains the
** component) at which to position the components vertically.
** @param jc the JComponent to receive these alignment attributes
** @return the JComponent reference passed in as its third argument
**
** @see #xAlign(double, JComponent) xAlign(double, JComponent)
** @see #yAlign(double, JComponent) yAlign(double, JComponent)
**
**/
public static JComponent xyAlign(double xFraction, double yFraction, JComponent jc) {
return xAlign(xFraction, yAlign(yFraction, jc));
}
/**
** Convenience method equivalent to applying xyAlign(widthIncreaseInPixels,
** heightIncreaseInPixels, jca[i])) to every non-null element in the 1-D array.
**
** @param xFraction the fraction of the way between the extreme left and extreme
** right positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components horizontally.
** @param yFraction the fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components vertically.
** @param jca contains the JComponents to receive these alignment attributes
** @return the array passed in as the third parameter
**
** @see #xyAlign(double, double, JComponent) xyAlign(double, double, JComponent)
**
**/
public static JComponent[] xyAlign(double xFraction, double yFraction, JComponent[] jca) {
return xAlign(xFraction, yAlign(yFraction, jca));
}
/**
** Convenience method equivalent to applying xyAlign(widthIncreaseInPixels,
** heightIncreaseInPixels, jca[i][j])) to every non-null element in the 2-D array.
**
** @param xFraction the fraction of the way between the extreme left and extreme
** right positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components horizontally.
** @param yFraction the fraction of the way between the extreme top and extreme
** bottom positions (within the breadboard grid-cell-blocks that contain the
** components) at which to position the components vertically.
** @param jca contains the JComponents to receive these alignment attributes
** @return the array passed in as the third parameter
**
** @see #xyAlign(double, double, JComponent) xyAlign(double, double, JComponent)
**
**/
public static JComponent[][] xyAlign(double xFraction, double yFraction, JComponent[][] jca) {
return xAlign(xFraction, yAlign(yFraction, jca));
}
/**
** The maximum allowed xUpsize width increase, or yUpsize
** height increase.
**
**
** With screen coordinates of around 10 times larger than this, Swing
** starts producing a variety of strange error messages. Moreover, it is
** unlikely increases larger than this -- far more than a whole screen of pixels
** on today's monitors--will be needed for the fine-tuning adjustments of
** xUpsize and yUpsize .
**
**
** An IllegalArgumentException is thrown if the first arg of xUpsize
** or yUpsize exceeds this limit.
**
**
** @see #xUpsize(int, JComponent) xUpsize
** @see #yUpsize(int, JComponent) yUpsize
**
**/
public final static int MAX_UPSIZE = 10000;
/**
**
** Declares that, for JComponentBreadboard layout purposes, the specified
** JComponent should be considered to have a preferred width the specified
** number of pixels larger than the JComponent's actual preferred width.
**
** Use this method to increase or decrease the width of a JComponent
** from what it would normally be (e.g. if you want a slightly wider JButton
** than Swing would normally provide).
**
**
** @param widthIncreaseInPixels the number of pixels wider that you want
** the width to be. Value must not exceed MAX_UPSIZE. Negative
** values can be used to decrease the width. However, if the sum of
** the width adjustment and original preferred width is ever less than
** 0, the adjusted preferred width will be exactly 0, not negative.
** @param jc the JComponent whose preferred width is to be adjusted
** @return the JComponent whose preferred width was adjusted (jc ).
**
**/
public static JComponent xUpsize(int widthIncreaseInPixels, JComponent jc) {
if (MAX_UPSIZE < widthIncreaseInPixels)
throw new IllegalArgumentException(
"widthIncreaseInPixels > MAX_UPSIZE (" +
widthIncreaseInPixels + " > " + MAX_UPSIZE + ")");
else if (0 == widthIncreaseInPixels)
jc.putClientProperty(X_UPSIZE_PROPNAME, null);
else if (null != jc)
jc.putClientProperty(X_UPSIZE_PROPNAME, new Integer(widthIncreaseInPixels));
return jc;
}
/**
** Convenience method equivalent to issuing a
** xUpsize(widthIncreaseInPixels, jc[i])) on every non-null element
** of a 1-D JComponent array.
**
**
** @param widthIncreaseInPixels the width increase attribute to be set on each JComponent in the array
** @param jca the 1-D array of JComponents operated upon.
** @return the 1-D array reference, jca , passed into it.
**
** @see #xUpsize(int, JComponent) xUpsize(int, JComponent)
**
**/
public static JComponent[] xUpsize(int widthIncreaseInPixels, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xUpsize(widthIncreaseInPixels, jca[i]);
return jca;
}
/**
** Convenience method equivalent to issuing a
** xUpsize(widthIncreaseInPixels, jc[i][j])) on every non-null element
** of a 2-D JComponent array.
**
**
** @param widthIncreaseInPixels the width increase attribute to be set on each JComponent in the array
** @param jca the 2-D array of JComponents operated upon.
** @return the 2-D array reference, jca , passed into it.
**
** @see #xUpsize(int, JComponent) xUpsize(int, JComponent)
**
**/
public static JComponent[][] xUpsize(int widthIncreaseInPixels, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
xUpsize(widthIncreaseInPixels, jca[i]);
return jca;
}
/**
**
** Declares that, for JComponentBreadboard layout purposes, the specified
** JComponent should be considered to have a preferred height the specified
** number of pixels larger than the JComponent's actual preferred height.
**
** Use this method to increase or decrease the height of a JComponent
** from what it would normally be (e.g. if you want a slightly taller JButton
** than Swing would normally provide)
**
**
** @param heightIncreaseInPixels the number of pixels taller that you want
** the height to be. Value must not exceed MAX_UPSIZE. Negative
** values can be used to decrease the height. However, if the sum of
** this height adjustment and the original preferred height is ever less than
** 0, the adjusted preferred height will be exactly 0, not negative.
** @param jc the JComponent whose preferred height is to be adjusted
** @return the JComponent whose preferred height was adjusted (jc ).
**
**/
public static JComponent yUpsize(int heightIncreaseInPixels, JComponent jc) {
if (MAX_UPSIZE < heightIncreaseInPixels)
throw new IllegalArgumentException(
"heightIncreaseInPixels > MAX_UPSIZE (" +
heightIncreaseInPixels + " > " + MAX_UPSIZE + ")");
else if (0 == heightIncreaseInPixels)
jc.putClientProperty(Y_UPSIZE_PROPNAME, null);
else
jc.putClientProperty(Y_UPSIZE_PROPNAME, new Integer(heightIncreaseInPixels));
return jc;
}
/**
** Convenience method equivalent to issuing a
** yUpsize(heightIncreaseInPixels, jc[i])) on every non-null element
** of a 1-D JComponent array.
**
**
** @param heightIncreaseInPixels the height increase to be set on each JComponent in the array
** @param jca the 1-D array of JComponents operated upon.
** @return the 1-D array reference, jca , passed into it.
**
** @see #yUpsize(int, JComponent) yUpsize(int, JComponent)
**
**/
public static JComponent[] yUpsize(int heightIncreaseInPixels, JComponent[] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yUpsize(heightIncreaseInPixels, jca[i]);
return jca;
}
/**
** Convenience method equivalent to issuing a
** yUpsize(heightIncreaseInPixels, jc[i][j])) on every non-null element
** of a 2-D JComponent array.
**
**
** @param heightIncreaseInPixels the height increase attribute to be set on each JComponent in the array
** @param jca the 2-D array of JComponents operated upon.
** @return the 2-D array reference, jca , passed into it.
**
** @see #xUpsize(int, JComponent) xUpsize(int, JComponent)
**
**/
public static JComponent[][] yUpsize(int heightIncreaseInPixels, JComponent[][] jca) {
for (int i = 0; jca != null && i < jca.length; i++)
yUpsize(heightIncreaseInPixels, jca[i]);
return jca;
}
/**
** Convenience method equivalent to xUpsize(widthIncreaseInPixels,
** yUpsize(heightIncreaseInPixels, jc)) .
**
** @param widthIncreaseInPixels the number of pixels wider that you want
** the width to be. Value must not exceed MAX_UPSIZE. Negative
** values can be used to decrease the width. However, if the sum of
** the width adjustment and original preferred width is ever less than
** 0, the adjusted preferred width will be exactly 0, not negative.
** @param heightIncreaseInPixels the number of pixels taller that you want
** the height to be. Value must not exceed MAX_UPSIZE. Negative
** values can be used to decrease the height. However, if the sum of
** this height adjustment and the original preferred height is ever less than
** 0, the adjusted preferred height will be exactly 0, not negative.
** @param jc the JComponent whose preferred height is to be adjusted
** @return the JComponent whose preferred height was adjusted (jc ).
**
** @see #xUpsize(int, JComponent) xUpsize(int, JComponent)
** @see #yUpsize(int, JComponent) yUpsize(int, JComponent)
**
**/
public static JComponent xyUpsize(
int widthIncreaseInPixels, int heightIncreaseInPixels, JComponent jc) {
return xUpsize(widthIncreaseInPixels, yUpsize(heightIncreaseInPixels, jc));
}
/**
** Convenience method equivalent to applying xyUpsize(widthIncreaseInPixels,
** heightIncreaseInPixels, jca[i]) to every non-null element in the 1-D array.
**
** @param widthIncreaseInPixels the width increase attribute to be set on each JComponent in the array
** @param heightIncreaseInPixels the height increase attribute to be set on each JComponent in the array
** @param jca the 1-D array of JComponents operated upon.
** @return the 1-D array reference, jca , passed into it.
**
** @return the array passed in as the third parameter
**
** @see #xyUpsize(int, int, JComponent) xyUpsize(int, int, JComponent)
**
**/
public static JComponent[] xyUpsize(
int widthIncreaseInPixels, int heightIncreaseInPixels, JComponent[] jca) {
return xUpsize(widthIncreaseInPixels, yUpsize(heightIncreaseInPixels, jca));
}
/**
** Convenience method equivalent to applying
** xyUpsize(widthIncreaseInPixels, heightIncreaseInPixels,
** jca[i][j])) to every non-null element in the 2-D array.
**
** @param widthIncreaseInPixels the width increase attribute to be set on each JComponent in the array
** @param heightIncreaseInPixels the height increase attribute to be set on each JComponent in the array
** @param jca the 2-D array of JComponents operated upon.
** @return the 2-D array reference, jca , passed into it.
**
** @see #xyUpsize(int, int, JComponent) xyUpsize(int, int, JComponent)
**
**/
public static JComponent[][] xyUpsize(
int widthIncreaseInPixels, int heightIncreaseInPixels, JComponent[][] jca) {
return xUpsize(widthIncreaseInPixels, yUpsize(heightIncreaseInPixels, jca));
}
/**
** Returns the number of pixels that JComponentBreadboard will add to
** the JComponent's preferred width to form the "adjusted preferred width"
** used by JComponentBreadboard's layout manager.
**
** @param jc the component whose xUpsize setting is to be returned
** @return the number of pixels the given component's width has been increased
** above the preferred width via the xUpsize method. If xUpsize has
** not been applied to the component, returns 0.
**
** @see #xUpsize(int, JComponent) xUpsize(int, JComponent)
**
**/
public static int getXUpsize(JComponent jc) {
int result = 0;
Integer prop = (Integer) jc.getClientProperty(X_UPSIZE_PROPNAME);
if (null != prop)
result = prop.intValue();
return result;
}
/**
** Returns the number of pixels that JComponentBreadboard will add to
** the JComponent's preferred height to form the "adjusted preferred height"
** used by JComponentBreadboard's layout manager.
**
**
** @param jc the component whose yUpsize setting is to be returned
** @return the number of pixels the given component's height has been increased
** above its preferred height via the yUpsize method. If yUpsize has
** not been applied to the component, returns 0.
**
** @see #yUpsize(int, JComponent) yUpsize(int, JComponent)
**
**/
public static int getYUpsize(JComponent jc) {
int result = 0;
Integer prop = (Integer) jc.getClientProperty(Y_UPSIZE_PROPNAME);
if (null != prop)
result = prop.intValue();
return result;
}
/**
** Convenience method that returns a zero height JPanel whose preferred width
** is as specified.
**
**
This method can be used to introduce small amounts of
** horizontal space between JComponents within the breadboard array.
**
**
**
** @param widthInPixels width of zero height JPanel to be returned, in pixels
** @return a zero height JPanel of the specified width
**
** @see #ySpace(int) ySpace
** @see #xySpace(int, int) xySpace
** @see #setBreadboard setBreadboard
**
**/
public static JPanel xSpace(int widthInPixels) {
JPanel result = new JPanel();
result.setPreferredSize(new Dimension(widthInPixels, 0));
return result;
}
/**
** Convenience method that returns a zero width JPanel whose preferred height
** is as specified.
**
**
This method can be used to introduce small amounts of
** vertical space between JComponents within the breadboard array.
**
**
**
** @param heightInPixels height of zero width JPanel to be returned, in pixels
** @return a zero width JPanel of the specified height
**
** @see #xSpace(int) xSpace
** @see #xySpace(int, int) xySpace
** @see #setBreadboard setBreadboard
**
**/
public static JPanel ySpace(int heightInPixels) {
JPanel result = new JPanel();
result.setPreferredSize(new Dimension(0, heightInPixels));
return result;
}
/**
** Convenience method that returns an empty JPanel whose preferred width and
** height are as specified.
**
**
This method can be used to introduce small amounts of
** vertical and horizontal space within the breadboard array.
**
**
**
** @param widthInPixels width JPanel to be returned, in pixels
** @param heightInPixels height of JPanel to be returned, in pixels
** @return an empty JPanel of the specified dimensions
**
** @see #xSpace(int) xSpace
** @see #ySpace(int) ySpace
** @see #setBreadboard setBreadboard
**
**/
public static JPanel xySpace(int widthInPixels, int heightInPixels) {
JPanel result = new JPanel();
result.setPreferredSize(new Dimension(widthInPixels, heightInPixels));
return result;
}
/**
** Convenience method that returns a new horizontal, flush top, flush left JSeparator
**
** This method can be used to more easily introduce horizontal separator
** lines into a JComponentBreadboard's breadboard array.
**
** @return the new JSeparator
**
** @see #ySep ySep
** @see #setBreadboard setBreadboard
**
**/
public static JSeparator xSep() {
JSeparator result = (JSeparator) xyAlign(0,0,new JSeparator(SwingConstants.HORIZONTAL));
return result;
}
/**
** Same as xSep() except returns a new vertically oriented JSeparator , and
** is used to introduce vertical lines into the breadboard array.
**
** @see #xSep xSep
**
**/
public static JSeparator ySep() {
JSeparator result = (JSeparator) xyAlign(0,0,new JSeparator(SwingConstants.VERTICAL));
return result;
}
// is the JComponent at the specified grid position the same as the
// JComponent immediately to it's left within the grid?
private boolean sameAsLeft(Object[][] breadboard, int iRow, int iCol) {
return iCol > 0 && (breadboard[iRow][iCol-1] == breadboard[iRow][iCol]);
}
// is the JComponent at the specified grid position the same as the
// JComponent immediately to it's right within the grid?
private boolean sameAsRight(Object[][] breadboard, int iRow, int iCol) {
return (iCol+1 < breadboard[iRow].length &&
breadboard[iRow][iCol+1] == breadboard[iRow][iCol]);
}
// is the JComponent at the specified grid position the same as the
// JComponent immediately above it within the grid?
private boolean sameAsAbove(Object[][] breadboard, int iRow, int iCol) {
return iRow > 0 && breadboard[iRow-1][iCol] == breadboard[iRow][iCol];
}
// is the JComponent at the specified grid position the same as the
// JComponent immediately below it within the grid?
private boolean sameAsBelow(Object[][] breadboard, int iRow, int iCol) {
return iRow+1 < breadboard.length &&
breadboard[iRow+1][iCol] == breadboard[iRow][iCol];
}
// is the specified grid position in the upper left corner of a
// rectangular region of the grid containing references to the same
// JComponent?
private boolean isUpperLeft(Object[][] breadboard, int iRow, int iCol) {
return null!= breadboard[iRow][iCol] && !sameAsLeft(breadboard, iRow, iCol) && !sameAsAbove(breadboard, iRow, iCol);
}
// same as isUpperLeft method, only for lower left corner
private boolean isLowerLeft(Object[][] breadboard, int iRow, int iCol) {
return null!= breadboard[iRow][iCol] && !sameAsLeft(breadboard, iRow, iCol) &&
!sameAsBelow(breadboard, iRow, iCol);
}
// same as isUpperLeft method, only for upper right corner
private boolean isUpperRight(Object[][] breadboard, int iRow, int iCol) {
return null!= breadboard[iRow][iCol] && !sameAsRight(breadboard, iRow, iCol) && !sameAsAbove(breadboard, iRow, iCol);
}
// same as isUpperLeft method, only for lower right corner
@SuppressWarnings("unused")
private boolean isLowerRight(Object[][] breadboard, int iRow, int iCol) {
return null!= breadboard[iRow][iCol] && !sameAsRight(breadboard, iRow, iCol) &&
!sameAsBelow(breadboard, iRow, iCol);
}
private boolean isOneElement1DArray(Object array) {
return array instanceof JComponent[] && ((JComponent[]) array).length == 1;
}
private boolean isOneElement2DArray(Object array) {
return array instanceof JComponent[][] &&
((JComponent[][]) array).length == 1 &&
isOneElement1DArray(((JComponent[][]) array)[0]);
}
// Does the object represent a 1D array of JComponents with 2 or more elements?
private boolean isJComponentArray1D(Object obj) {
return obj instanceof JComponent[] &&
((JComponent[]) obj).length > 1;
}
// Does the object represent a 2D array of JComponents with more than
// 1 row and more than 1 column ?
private boolean isJComponentArray2D(Object obj) {
return obj instanceof JComponent[][] &&
((JComponent[][]) obj).length > 1 &&
isJComponentArray1D(((JComponent[][]) obj)[0]);
}
// Does the object represent a single row of JComponents with 2 or
// more columns, as a 1-Row 2D array ?
private boolean isJComponentRow(Object obj) {
return obj instanceof JComponent[][] &&
((JComponent[][]) obj).length == 1 &&
((JComponent[][]) obj)[0] instanceof JComponent[] &&
((JComponent[][]) obj)[0].length > 1;
}
// Does the object represent a single column of JComponents with 2 or
// more rows, as a 1-Column 2D array ?
private boolean isJComponentCol(Object obj) {
return obj instanceof JComponent[][] &&
((JComponent[][]) obj).length > 1 &&
((JComponent[][]) obj)[0] instanceof JComponent[] &&
((JComponent[][]) obj)[0].length == 1;
}
// returns height of rectangular sub-block within the breadboard whose upper
// left corner is in the specified breadboard row and column.
private int jbRowCount(Object[][] breadboard, int iRow, int iCol) {
int result = 1;
while (sameAsBelow(breadboard, iRow+result-1, iCol))
result++;
return result;
}
// returns width of rectangular sub-block within the breadboard whose upper
// left corner cell is in the specified breadboard row and column.
private int jbColCount(Object[][] breadboard, int iRow, int iCol) {
int result = 1;
while (sameAsRight(breadboard, iRow, iCol+result-1))
result++;
return result;
}
// JComponentBreadboard arrays are required to contain only
// non-overlapping rectangular blocks of cells that all contain the
// same object reference (or null).
//
// This function returns the difference between the given row index
// (iRow), and the row index of the first row in the block of cells
// that contains the object reference at the given cell (iRow, iCol).
//
// For example, returns 0 for cells in the first row of the block,
// 1 for cells in the second row, etc.
//
private int jbRowOffset(Object[][] breadboard, int iRow, int iCol) {
int result = 0;
while (sameAsAbove(breadboard, iRow-result, iCol))
result++;
return result;
}
// analogous to jbRowOffset, but for column offsets.
private int jbColOffset(Object[][] breadboard, int iRow, int iCol) {
int result = 0;
while (sameAsLeft(breadboard, iRow, iCol-result))
result++;
return result;
}
// Assuming the given element is mapped into a single column or
// single row block in the breadboard array, returns the offset from
// the topmost or leftmost element of that block.
private int jbOffset(Object[][] breadboard, int iRow, int iCol) {
int rowOffset = jbRowOffset(breadboard, iRow, iCol);
int colOffset = jbColOffset(breadboard, iRow, iCol);
int result = Math.max(rowOffset, colOffset);
if (colOffset > 0 && rowOffset > 0)
// if this method is used properly, I think this should never be thrown
throw new IllegalStateException("colOffset="+colOffset +
" rowOffset="+rowOffset +
" for a 1-D array at breadboard[" +
iRow + "][" + iCol + "]. " +
"1-D arrays must be mapped to a " +
"single row or column of the breadboard so " +
"either colOffset or rowOffset must be 0.");
return result;
}
// returns the JComponent that occupies a given breadboard cell
private JComponent getComponentAtCell(Object[][] breadboard, int iRow, int iCol) {
JComponent result = null;
if (isJComponentArray1D(breadboard[iRow][iCol])) {
int offset = jbOffset(breadboard, iRow, iCol);
result = ((JComponent[]) breadboard[iRow][iCol])[offset];
}
else if (isJComponentArray2D(breadboard[iRow][iCol])) {
int colOffset = jbColOffset(breadboard, iRow, iCol);
int rowOffset = jbRowOffset(breadboard, iRow, iCol);
result = ((JComponent[][]) breadboard[iRow][iCol])[rowOffset][colOffset];
}
else if (isJComponentRow(breadboard[iRow][iCol])) {
// Special "fill down" rule for 2-D arrays representing a single row:
//
// Whenever a single row array occupies multiple rows in the breadboard, the
// single JComponent in each column of that array occupies every row
// of the associated breadboard column into which it is mapped:
int colOffset = jbColOffset(breadboard, iRow, iCol);
int rowOffset = 0;
result = ((JComponent[][]) breadboard[iRow][iCol])[rowOffset][colOffset];
}
else if (isJComponentCol(breadboard[iRow][iCol])) {
// Special "fill across" rule for 2-D arrays representing a single column:
//
// Whenever a single column array occupies multiple columns in the breadboard, the
// single JComponent in each row of that array occupies every column
// of the associated breadboard row into which it is mapped:
int colOffset = 0;
int rowOffset = jbRowOffset(breadboard, iRow, iCol);
result = ((JComponent[][]) breadboard[iRow][iCol])[rowOffset][colOffset];
}
// one element arrays are simply dereferenced and treated like ordinary
// JComponent references...simpler to allow them than to make them illegal
else if (isOneElement2DArray(breadboard[iRow][iCol]))
result = ((JComponent[][]) (breadboard[iRow][iCol]))[0][0];
else if (isOneElement1DArray(breadboard[iRow][iCol]))
result = ((JComponent[]) (breadboard[iRow][iCol]))[0];
else if (breadboard[iRow][iCol] instanceof JComponent)
result = (JComponent) (breadboard[iRow][iCol]);
else if (breadboard[iRow][iCol] != null )
// should have caught this earlier, but you never know.
throw new IllegalStateException("Array element that could not be interpreted encountered at " +
"breadboard[" + iRow + "][" + iCol + "]");
return result;
}
// Requires that the 2-D array contains at least one row, and the
// each of these rows has at least one element (simplifies later
// tests if we can assume this)
private void requireNonEmptyRows(Object[][] array, String arrayName) {
if (null == array)
throw new IllegalArgumentException("The " + arrayName + " cannot be null.");
else if (array.length < 1)
throw new IllegalArgumentException("The " + arrayName + " must have at least one row.");
else {
for (int i=0; i < array.length; i++) {
if (null == array[i])
throw new IllegalArgumentException("Row " + i + " of the " + arrayName + " is null. Rows cannot be null.");
else if (array[i].length < 1)
throw new IllegalArgumentException("Row " + i + " of the " + arrayName + " has zero elements. Rows must have at least 1 element.");
}
}
}
// returns index of first row whose number of elements differs from that of
// the first row, or 0 if no two rows have a different number of elements.
// Assumes a non-null 2-D array all of whose rows are non-null.
private int firstNonRectangularRow(Object[][] array) {
int result = 0;
for (int iRow = 1; iRow < array.length && 0 == result; iRow++) {
if (array[iRow].length != array[0].length)
result = iRow;
}
return result;
}
// Requires that the breadboard have a perfectly rectangular shape
// (we can rely on this requirement to simplify the logic elsewhere)
private void requireRectangularBreadboardArray(Object [][] breadboard) {
requireNonEmptyRows(breadboard, "breadboard array");
int iRow = firstNonRectangularRow(breadboard);
if (iRow > 0) {
throw new IllegalArgumentException("Row " + iRow + " of your breadboard array," +
" has " + breadboard[iRow].length +
" elements. This differs from row 0," +
" which has " + breadboard[0].length +
" elements. All rows of the breadboard array" +
" must have the same number of elements.");
}
}
// Requires that any 2-D JComponent arrays placed on the breadboard have
// a perfectly rectangular shape. The breadboard cell reference is
// assumed to be to the lower right corner of the breadboard cell-block
// into which the array is mapped.
private void requireRectangular2DArray(Object[][] breadboard, int iRow, int iCol) {
requireNonEmptyRows((Object[][]) breadboard[iRow][iCol], "array at breadboard["+iRow+"]["+iCol+"]");
int iBad = firstNonRectangularRow((JComponent[][]) breadboard[iRow][iCol]);
if (iBad > 0) {
int row0 = iRow - jbRowOffset(breadboard, iRow, iCol);
int col0 = iCol - jbColOffset(breadboard, iRow, iCol);
throw new IllegalArgumentException("Invalid JComponent[][] array found in " +
"rows " + row0 + " to " + iRow + " of " +
"columns " + col0 + " to " + iCol +
" of your breadboard. " +
" Row " + iBad + " of this array has " +
((JComponent[][]) (breadboard[iRow][iCol]))[iBad].length +
" elements, which " +
" differs from row 0," +
" which has " +
((JComponent[][]) (breadboard[iRow][iCol]))[0].length +
" elements. All 2-D arrays placed on the breadboard " +
" must be rectangular in shape.");
}
}
// Checks that 1-D and 2-D arrays are plugged into same-shaped
// sub-blocks in breadboard. Assumes that all breadboard sub-blocks
// associated with a single reference are rectangularly shaped, and
// that all arrays have at least element, and are also rectangular
// shaped (these are validated in earlier validation methods).
private void requireArraysFitInTheirSubblocks(Object [][] breadboard) {
for (int iRow = 0; iRow < breadboard.length; iRow++) {
for (int iCol =0; iCol < breadboard[iRow].length; iCol++) {
if (isUpperLeft(breadboard, iRow, iCol)) {
int nRows = jbRowCount(breadboard, iRow, iCol);
int nCols = jbColCount(breadboard, iRow, iCol);
if (isJComponentArray1D(breadboard[iRow][iCol])) {
int len = ((JComponent[]) (breadboard[iRow][iCol])).length;
if (nRows > 1 && nCols > 1 ||
Math.max(nRows, nCols) != len) {
throw new IllegalArgumentException(
"A 1-D array has been incorrectly placed into rows " +
iRow + " to " + (iRow + nRows - 1) + " of columns " +
iCol + " to " + (iCol + nCols - 1) + " of your breadboard. " +
"Since this 1-D array has " + len + " elements, " +
"it must be placed into either a " +
"1 row, " + len + " column block of breadboard " +
"cells or into a 1 column, " + len + " row block. ");
}
}
else if (isJComponentArray2D(breadboard[iRow][iCol])) {
int rows = ((JComponent[][]) (breadboard[iRow][iCol])).length;
int cols = ((JComponent[][]) (breadboard[iRow][iCol]))[0].length;
if (nRows != rows || nCols != cols) {
throw new IllegalArgumentException(
"The 2-D array placed in rows " +
iRow + " to " + (iRow + nRows - 1) + " of columns " +
iCol + " to " + (iCol + nCols - 1) + " of your breadboard " +
"has " + rows + " rows and " +
cols + " columns which differs from the " +
nRows + " rows and " +
nCols + " columns of the block of breadboard " +
"cells it has been placed into. " +
"2-D arrays with more than 1 row and more than 1 column " +
"must fit exactly into the breadboard cells that they occupy.");
}
}
else if (isJComponentRow(breadboard[iRow][iCol])) {
int cols = ((JComponent[][]) (breadboard[iRow][iCol]))[0].length;
if (nCols != cols) {
throw new IllegalArgumentException(
"The one row, 2-D array, placed in rows " +
iRow + " to " + (iRow + nRows - 1) + " of columns " +
iCol + " to " + (iCol + nCols - 1) + " of your breadboard, " +
"has " +
cols + " columns which differs from the " +
nCols + " columns of the block of breadboard " +
"cells it has been placed into. " +
"One row, 2-D arrays " +
"must have exactly the same number of columns as " +
"the block of cells in the breadboard that they occupy.");
}
}
else if (isJComponentCol(breadboard[iRow][iCol])) {
int rows = ((JComponent[][]) (breadboard[iRow][iCol])).length;
if (nRows != rows) {
throw new IllegalArgumentException(
"The one column, 2-D array, placed in rows " +
iRow + " to " + (iRow + nRows - 1) + " of columns " +
iCol + " to " + (iCol + nCols - 1) + " of your breadboard, " +
"has " +
rows + " rows which differs from the " +
nRows + " rows of the block of breadboard " +
"cells it has been placed into. " +
"One column, 2-D arrays " +
"must have exactly the same number of rows as " +
"the block of cells in the breadboard that they occupy.");
}
}
}
}
}
}
// Requires non-null cells of the breadboard array to contain square
// blocks of JComponent, JComponent[], or JComponent[][] references.
private void requireAcceptableJComponentReferences(Object[][] breadboard) {
for (int iRow = 0; iRow < breadboard.length; iRow++) {
for (int iCol =0; iCol < breadboard[iRow].length; iCol++) {
if (null == breadboard[iRow][iCol])
continue;
else if (!(breadboard[iRow][iCol] instanceof JComponent) &&
!(breadboard[iRow][iCol] instanceof JComponent[]) &&
!(breadboard[iRow][iCol] instanceof JComponent[][]))
throw new IllegalArgumentException("The breadboard array, after removing " +
"headers and expanding dittos, must contain only null or " +
"JComponent, JComponent[], or JComponent[][] object references. " +
"But, breadboard["+iRow+"]["+iCol+"] contains a reference of type " +
breadboard[iRow][iCol].getClass().getSimpleName());
else if (breadboard[iRow][iCol] instanceof JComponent[] &&
((JComponent[]) breadboard[iRow][iCol]).length < 1)
throw new IllegalArgumentException(
"breadboard[" + iRow + "][" + iCol + "] " +
"contains a 1-D array with 0 elements. At least 1 element is required.");
else if (breadboard[iRow][iCol] instanceof JComponent[][])
requireRectangular2DArray(breadboard, iRow, iCol);
// else reference is OK, but still need to breaboard positions it occupies
// (these positions must form a square block)
// if each upper left corner anchors a square block, the entire breadboard
// consists of such square blocks.
if (isUpperLeft(breadboard, iRow, iCol)) {
int nRows = jbRowCount(breadboard, iRow, iCol);
int nCols = jbColCount(breadboard, iRow, iCol);
// check across each row that all have same number of columns
for (int i = 1; i < nRows; i++)
if (jbColCount(breadboard, iRow+i, iCol) != nCols)
throw new IllegalArgumentException(
"Object references must form square subblocks. " +
"But, the block beginning at breadboard[" + iRow + "][" + iCol + "] " +
"has " + nCols + " columns on row 0 but " +
jbColCount(breadboard, iRow+i, iCol) + " columns on " +
"row " + (iRow+i) + ".");
// check down each column that all columns have same number of rows
for (int j = 0; j < nCols; j++)
if (jbRowCount(breadboard, iRow, iCol+j) != nRows)
throw new IllegalArgumentException(
"Object references must form square subblocks. " +
"However, the block beginning at breadboard[" + iRow + "][" + iCol + "] " +
"has " + nRows + " rows in column 0 but " +
jbRowCount(breadboard, iRow, iCol+j) + " rows in " +
"column " + (iCol+j) + ".");
}
}
}
}
// Require that each JComponent appears in exactly one rectangular
// sub-block within the breadboard. Assumes breadboard consists of
// rectangular blocks of the same object ref, and that any array refs
// fit properly into these blocks.
private void requireSingleSubblockPerJComponent(Object[][] breadboard) {
Object obj = null;
// The String in the Hashmap is used in the exception messages
HashMap distinctComponents = new HashMap();
for (int iRow = 0; iRow < breadboard.length; iRow++) {
for (int iCol =0; iCol < breadboard[iRow].length; iCol++) {
obj = null;
if (isJComponentArray1D(breadboard[iRow][iCol])) {
obj = getComponentAtCell(breadboard, iRow, iCol);
}
else if (isJComponentArray2D(breadboard[iRow][iCol])) {
obj = getComponentAtCell(breadboard, iRow, iCol);
}
else if (isJComponentRow(breadboard[iRow][iCol]) &&
0==jbRowOffset(breadboard, iRow, iCol)) {
obj = getComponentAtCell(breadboard, iRow, iCol);
}
else if (isJComponentCol(breadboard[iRow][iCol]) &&
0==jbColOffset(breadboard, iRow, iCol)) {
obj = getComponentAtCell(breadboard, iRow, iCol);
}
else if (isUpperLeft(breadboard, iRow, iCol)) {
// one element arrays are dereferenced but otherwise act like ordinary JComponent refs
if (isOneElement2DArray(breadboard[iRow][iCol]))
obj = ((JComponent[][]) (breadboard[iRow][iCol]))[0][0];
else if (isOneElement1DArray(breadboard[iRow][iCol]))
obj = ((JComponent[]) (breadboard[iRow][iCol]))[0];
else if (breadboard[iRow][iCol] instanceof JComponent[] ||
breadboard[iRow][iCol] instanceof JComponent[][])
obj = null;
else
obj = breadboard[iRow][iCol];
}
if (null != obj) {
if (!(obj instanceof JComponent)) {
throw new IllegalArgumentException(
"The object that appears at (after removing any headers and expanding arrays) " +
"breadboard[" + iRow + "][" + iCol +"] is of type \"" +
obj.getClass().getName() + "\". " +
"Either null, or an object of type JComponent is required.");
}
else if (!distinctComponents.containsKey(obj)) {
distinctComponents.put(obj, iRow + "][" + iCol);
}
else {
throw new IllegalArgumentException(
"The JComponent that appears at (after removing any headers and expanding arrays) breadboard["+iRow+ "][" + iCol + "] " +
"also occupies an earlier block at " +
"breadboard[" + distinctComponents.get(obj) + "]. " +
"Separate JComponents must appear in exactly one " +
"rectangular block of breadboard-grid cells. Similarly, each JComponent within " +
"any arrays placed on the breadboard must map into a single breadboard grid cell-block.");
}
}
// else one of the situations where repeated JComponents are valid, or null.
}
}
}
// Check that a breadboard array has the required format:
private void validateBreadboard(Object[][] breadboard) {
requireRectangularBreadboardArray(breadboard);
requireAcceptableJComponentReferences(breadboard);
requireArraysFitInTheirSubblocks(breadboard);
requireSingleSubblockPerJComponent(breadboard);
}
// add the components (ignoring those repeated within rectangular
// regions) to this JComponentBreadboard (a JPanel).
private void addBreadboardJComponents(Object[][] breadboard) {
for (int iRow = 0; iRow < breadboard.length; iRow++) {
for (int iCol = 0; iCol < breadboard[iRow].length; iCol++) {
if (isJComponentArray1D(breadboard[iRow][iCol])) {
int offset = jbOffset(breadboard, iRow, iCol);
JComponent jc = ((JComponent[]) breadboard[iRow][iCol])[offset];
if (null != jc) add(jc);
}
else if (isJComponentArray2D(breadboard[iRow][iCol]) ||
isJComponentCol(breadboard[iRow][iCol]) ||
isJComponentRow(breadboard[iRow][iCol])) {
int rowOffset = jbRowOffset(breadboard, iRow, iCol);
int colOffset = jbColOffset(breadboard, iRow, iCol);
if (isJComponentArray2D(breadboard[iRow][iCol]) ||
isJComponentCol(breadboard[iRow][iCol]) && 0 == colOffset ||
isJComponentRow(breadboard[iRow][iCol]) && 0 == rowOffset) {
JComponent jc =
((JComponent[][]) breadboard[iRow][iCol])[rowOffset][colOffset];
if (null != jc) add(jc);
}
}
else if (isUpperLeft(breadboard, iRow, iCol)) {
JComponent jc = null;
if (isOneElement2DArray(breadboard[iRow][iCol]))
jc = ((JComponent[][]) (breadboard[iRow][iCol]))[0][0];
else if (isOneElement1DArray(breadboard[iRow][iCol]))
jc = ((JComponent[]) (breadboard[iRow][iCol]))[0];
else if (breadboard[iRow][iCol] instanceof JComponent)
jc = (JComponent) (breadboard[iRow][iCol]);
if (null != jc) add(jc);
}
}
}
}
/**
** This class provides the layout manager for JComponentBreadboard.
**
**
** For more information, see the discussion of the layout algorithm
** within the javadocs for the {@link #setBreadboard setBreadboard}
** method.
**
**/
private class JComponentBreadboardLayout implements LayoutManager2 {
// a 2-D array of components in the breadboard. Note that the original
// breadboard array can contain component array references, and these
// are expanded out into the underlying individual components in this array
JComponent[][] comp;
// row heights, col width for "tightest fitting to preferred sizes, pushed closest
// to upper left corner, grid", layout that is always our starting point.
int[] preferredRowHeights;
int[] preferredColWidths;
// This constructor relies upon there being appropriately,
// previously, defined rowScale and colScale arrays in outter
// class parent, and it also assumes that the breadboard array
// conforms to the requirements imposed by the outter class'
// validateBreadboard method.
public JComponentBreadboardLayout(Object[][] breadboard) {
super();
if (breadboard.length != rowScale.length)
throw new IllegalArgumentException("breadboard.length must equal rowScale.length");
if ((breadboard.length==0 && colScale.length!=0) ||
(breadboard.length > 0 && breadboard[0].length != colScale.length))
throw new IllegalArgumentException("breadboard[0].length must equal colScale.length");
comp = new JComponent[rowScale.length][colScale.length];
for (int iRow = 0; iRow < rowScale.length; iRow++) {
for (int iCol = 0; iCol < colScale.length; iCol++) {
comp[iRow][iCol] = getComponentAtCell(breadboard, iRow, iCol);
}
}
// these are computed on-the-fly during layout, just allocate them for now
preferredRowHeights = new int[rowScale.length];
preferredColWidths = new int[colScale.length];
}
// Returns the preferred height, resized for the component's Y-upsize setting.
private int getResizedPreferredHeight(JComponent jc) {
int result = jc.getPreferredSize().height + getYUpsize(jc);
// neg upsizes are allowed, but final resized size must be at least 0.
if (result < 0) result = 0;
return result;
}
// Given a cell reference at the bottom of a single component mapped cell block,
// and assuming preferred heights of preceeding rows are known, returns
// the excess height, beyond that already available in preceeding rows,
// needed to fit the component in at its preferred height. Negative
// excesses mean that the previous rows already have more than enough space.
private int excessHeight(JComponent[][] comp, int iRow, int iCol) {
int result = getResizedPreferredHeight(comp[iRow][iCol]);
for (int i = 0; sameAsAbove(comp, iRow-i, iCol); i++)
result -= preferredRowHeights[iRow-i-1];
return result;
}
// updates the preferred row heights to reflect the current preferred heights
// of all components in the grid. Note that preferred sizes can change
// (for example, if user enters more text into a JTextField)
private void updatePreferredRowHeights() {
for (int iRow = 0; iRow < rowScale.length; iRow++) {
int maxHeight = 0;
for (int iCol = 0; iCol < colScale.length; iCol++) {
if (isLowerLeft(comp, iRow, iCol) && comp[iRow][iCol].isVisible())
maxHeight = Math.max(maxHeight, excessHeight(comp, iRow, iCol));
}
preferredRowHeights[iRow] = maxHeight;
}
}
// Returns the preferred height, resized for the component's Y-upsize setting.
private int getResizedPreferredWidth(JComponent jc) {
int result = jc.getPreferredSize().width + getXUpsize(jc);
// neg upsizes are allowed, but final resized size must be at least 0.
if (result < 0) result = 0;
return result;
}
// horizontal analogue of the excessHeight method.
private int excessWidth(JComponent[][] comp, int iRow, int iCol) {
int result = getResizedPreferredWidth(comp[iRow][iCol]);
for (int i = 0; sameAsLeft(comp, iRow, iCol-i); i++)
result -= preferredColWidths[iCol-i-1];
return result;
}
// horizontal analogue of the updatePreferredRowHeights method
private void updatePreferredColWidths() {
for (int iCol = 0; iCol < colScale.length; iCol++) {
int maxWidth = 0;
for (int iRow = 0; iRow < rowScale.length; iRow++) {
if (isUpperRight(comp, iRow, iCol) && comp[iRow][iCol].isVisible())
maxWidth = Math.max(maxWidth, excessWidth(comp, iRow, iCol));
}
preferredColWidths[iCol] = maxWidth;
}
}
// returns sum of all, or shrinks() or expands() tagged, row or column ints
private int arraySum(int[] a, ScalingDirective[] opt,
boolean countShrinks, boolean countExpands) {
int result = 0;
for (int i=0; i < a.length; i++) {
if (null == opt || // null scale directives means "count everything"
(countShrinks && opt[i].shrinks()) ||
(countExpands && opt[i].expands()))
result += a[i];
}
return result;
}
// sums integers associated with shrinkable rows or columns
private int shrinkableArraySum(int a[], ScalingDirective[] opt) {
return arraySum(a, opt, true, false);
}
// sums integers associated with expandable rows or columns
private int expandableArraySum(int a[], ScalingDirective[] opt) {
return arraySum(a, opt, false, true);
}
// sums all of the integers in an array associated with any row or column
private int arraySum(int[] a) {
return arraySum(a, null, true, true);
}
/**
** Returns preferred size of the container.
**
** Parent argument must be same as the outer class parent that
** holds the JCBreadboard layout manager.
**/
public Dimension preferredLayoutSize(Container parent) {
if (parent != JComponentBreadboard.this)
throw new IllegalArgumentException("JComponentBreadboardLayout layout " +
"managers can only manage the " +
"layout of the parent " +
"JComponentBreadboard that contains " +
"them.");
// I'm not exactly sure why this is needed, but
// GridBagLayout.java synchronizes it's getPreferredSize calls
// on components with such a line, so, fearing a dead-lock, I
// synchronize them too.
synchronized (parent.getTreeLock()) {
Dimension result = new Dimension();
updatePreferredRowHeights();
result.height = arraySum(preferredRowHeights);
updatePreferredColWidths();
result.width = arraySum(preferredColWidths);
Border border = ((JComponent) parent).getBorder();
if (border instanceof TitledBorder) {
Dimension borderSize = ((TitledBorder) border).getMinimumSize(parent);
// Any extra height or width needed for the title label is added to
// last row or column. Behavior is the same as if a zero-height
// JPanel with a width equal to the minimum required for the title
// label were mapped into an extra row of the breadboard, and a second
// zero-width JPanel with a height equal to the minimum height
// required for the title label were mapped into an extra column of
// the breadboard.
//
// Without this code, longer title labels can get clipped.
//
preferredRowHeights[rowScale.length-1] +=
Math.max(0, borderSize.height - result.height);
preferredColWidths[colScale.length-1] +=
Math.max(0, borderSize.width - result.width);
result.height = Math.max(result.height, borderSize.height);
result.width = Math.max(result.width, borderSize.width);
}
Insets insets = ((JComponent) parent).getInsets();
result.height += insets.top + insets.bottom;
result.width += insets.left + insets.right;
return result;
}
}
// Sets the component's bounds only if they have changed (idea was
// to make it faster but I was not able to measure any appreciable
// speed increase).
private void setBoundsIfTheyChanged(JComponent comp,
int x, int y, int width, int height) {
Rectangle r = comp.getBounds();
if (r.x != x || r.y != y || r.width != width || r.height != height)
comp.setBounds(x, y, width, height);
}
/**
** Lays out the components in the breadboard within the parent.
**
** Parent argument must be same as the outer class parent that
** holds the JCBreadboard layout manager.
**/
public void layoutContainer(Container parent) {
if (parent != JComponentBreadboard.this)
throw new IllegalArgumentException("JComponentBreadboardLayout layout managers can only manage the layout of the parent JComponentBreadboard that contains them.");
Insets insets = ((JComponent) parent).getInsets();
Dimension prefParentSize = preferredLayoutSize(parent);
int prefHeight = prefParentSize.height;
int prefWidth = prefParentSize.width;
Dimension parentSize = parent.getSize();
int parentHeight = parentSize.height;
int parentWidth = parentSize.width;
// height of rows, width of columns that are shrinkable; expandable
int shrinksHeight = shrinkableArraySum(preferredRowHeights, rowScale);
int shrinksWidth = shrinkableArraySum(preferredColWidths, colScale);
int expandsHeight = expandableArraySum(preferredRowHeights, rowScale);
int expandsWidth = expandableArraySum(preferredColWidths, colScale);
// estimate the y-grid row dividing line locations, taking into account any
// expansion or shrinkage of the rows required to fit in the parent
int[] y = new int[rowScale.length+1]; // 1 more row divider than # of rows
y[0] = insets.top;
// we use double rather than int to prevent accumulation of roundoff errors:
double yNext = y[0];
for (int iRow = 0; iRow < rowScale.length; iRow++) {
yNext += preferredRowHeights[iRow];
// applies various row scaling directives to make rows fit vertically in parent
if (rowScale[iRow].expands() &&
parentHeight > prefHeight && expandsHeight > 0) {
// extra vertical space, and at least one non-zero, expandable, row
yNext += preferredRowHeights[iRow] *
(parentHeight - prefHeight)/((double) expandsHeight);
}
else if (rowScale[iRow].shrinks() &&
parentHeight < prefHeight && shrinksHeight > 0) {
// deficit vertical space and at least 1 non-zero shrinkable col
// scale down to fit, or, if that's not possible, just zero it out.
yNext += preferredRowHeights[iRow] * Math.max(-1.0,
(parentHeight - prefHeight)/((double) shrinksHeight));
}
y[iRow+1] = (int) Math.rint(yNext);
}
// analogously, estimate the x-grid col divider locations, taking into account any
// expansion or shrinkage of the columns required to fit into the parent
int[] x = new int[colScale.length+1];
x[0] = insets.left;
double xNext = x[0];
for (int iCol = 0; iCol < colScale.length; iCol++) {
xNext += preferredColWidths[iCol];
// applies col scaling directives to make cols fit horizontally in parent
if (colScale[iCol].expands() &&
parentWidth > prefWidth && expandsWidth > 0) {
// extra horizontal space, and at least 1 non-zero, expandable, col
xNext += preferredColWidths[iCol] *
(parentWidth - prefWidth)/((double) expandsWidth);
}
else if (colScale[iCol].shrinks() &&
parentWidth < prefWidth && shrinksWidth > 0) {
// deficit horizontal space and at least 1 non-zero shrinkable col
// scale down to fit, or, if that's not possible, just zero it out.
xNext += preferredColWidths[iCol] * Math.max(-1.0,
(parentWidth - prefWidth)/((double) shrinksWidth));
}
x[iCol+1] = (int) Math.rint(xNext);
}
// With row and column cell divider locations in hand, we next
// use them to determine the size/location of each cell or
// block of cells that contains a component, and, using various
// component specific layout attributes such as "fill", the
// exact size and location within each such cell block of the
// component it contains:
for (int iRow = 0; iRow < rowScale.length; iRow++) {
for (int iCol = 0; iCol < colScale.length; iCol++) {
if (isUpperLeft(comp, iRow, iCol) && comp[iRow][iCol].isVisible()) {
int cellWidth = (x[iCol + jbColCount(comp, iRow, iCol)] - x[iCol]);
int cellHeight = (y[iRow + jbRowCount(comp, iRow, iCol)] - y[iRow]);
int resizedCompWidth = getResizedPreferredWidth(comp[iRow][iCol]);
int resizedCompHeight = getResizedPreferredHeight(comp[iRow][iCol]);
// note that cells can shrink, making cellWidth < resizedCompWidth
int width = isXFilled(comp[iRow][iCol]) ?
cellWidth : Math.min(cellWidth, resizedCompWidth);
int height = isYFilled(comp[iRow][iCol]) ?
cellHeight : Math.min(cellHeight, resizedCompHeight);
int deltaX = cellWidth - width;
int deltaY = cellHeight - height;
float xAlign = comp[iRow][iCol].getAlignmentX();
float yAlign = comp[iRow][iCol].getAlignmentY();
setBoundsIfTheyChanged(comp[iRow][iCol],
x[iCol]+(int) Math.rint(xAlign*deltaX),
y[iRow]+ (int) Math.rint(yAlign*deltaY),
width, height);
}
}
}
}
// more or less do-nothing methods required to fill out the LayoutManager2 interface
// begin here.
/** Unsupported method required by LayoutManager2 interface.
** Calling will raise an exception. (components can only be
** added in the constructor, and can never be removed) */
public void addLayoutComponent(Component comp, Object constraints) {
throw new IllegalStateException("Components can only be added via the constructor, and can never be removed.");
}
/** method required by LayoutManager2 interface (returns Container.LEFT_ALIGNMENT)*/
public float getLayoutAlignmentX(Container target) {
return Container.LEFT_ALIGNMENT;
}
/** method required by LayoutManager2 interface (returns Container.TOP_ALIGNMENT)*/
public float getLayoutAlignmentY(Container target) {
return Container.TOP_ALIGNMENT;
}
/** do-nothing method required by LayoutManager2 interface */
public void invalidateLayout(Container target) {
}
/**
** Method required by LayoutManager2 interface (returns
** Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE) --
** essentially, there is no maximum size
**
**/
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/** Unsupported method required by LayoutManager2 interface.
** Calling will raise an exception. (components can only be
** added in the constructor, and can never be removed) */
public void addLayoutComponent(String name, Component comp) {
throw new IllegalStateException("Components can only be added via the constructor, and can never be removed.");
}
/**
** Method required by LayoutManager2 interface (returns
** Dimension(0, 0) -- essentially, there is no minimum size
**
**/
public Dimension minimumLayoutSize(Container parent) {
return new Dimension(0,0);
}
/** Do-nothing method required by LayoutManager2 interface. */
public void removeLayoutComponent(Component comp) {
// this is called by Swing, so it cannot throw an exception,
// but it does not do anything, because the only way to add and
// remove components is via setBreadboard.
}
}
// hidden (but responsive to key strokes) JMenuBar used to
// implement ability to jbConnect to a KeyStroke ("hot keys")
private JMenuBar hotKeyMenuBar = new JMenuBar();
// Sets the "plain" (that is, after any row/column ScalingDirective headers have been
// stripped off) breadboard array as the active breadboard array.
private void setPlainBreadboard(Object[][] breadboard) {
validateBreadboard(breadboard);
setLayout(null); // expect adding/removing is faster if no layout present
removeAll();
add(hotKeyMenuBar); // hidden menu for implementing KeyStroke-connections
addBreadboardJComponents(breadboard);
JComponentBreadboardLayout layout = new JComponentBreadboardLayout(breadboard);
setLayout(layout);
repaint(); // without this line, busier forms can fail to refresh correctly
// (I don't know why--my reading of JDK says it should not be needed)
}
// allocates appropriately sized row and column scaling directive vectors, and sets
// all of their elements to the specified row and column scaling directives
private void setAllScales(Object[][] breadboard, ScalingDirective theRowScale,
ScalingDirective theColScale) {
rowScale = new ScalingDirective[breadboard.length];
colScale = new ScalingDirective[breadboard.length==0 ? 0 : breadboard[0].length];
for (int i = 0; i < rowScale.length; i++) rowScale[i] = theRowScale;
for (int i = 0; i < colScale.length; i++) colScale[i] = theColScale;
}
// copies ids associated with scaling directive factors from the breadboard array
private void setScales(Object[][] breadboard) {
rowScale = new ScalingDirective[breadboard.length-1];
colScale = new ScalingDirective[breadboard[0].length-1];
for (int i = 0; i < rowScale.length; i++) {
if (null == breadboard[i+1][0]) {
throw new IllegalArgumentException(
"breadboard["+(i+1)+"][0] is null. " +
"This cell is a row header, and thus must be one of the four possible " +
"allowed ScalingDirective values: NOSCALE, SHRINKS, EXPANDS, or BISCALE");
}
rowScale[i] = (ScalingDirective) breadboard[i+1][0];
}
for (int i = 0; i < colScale.length; i++) {
if (null == breadboard[0][i+1]) {
throw new IllegalArgumentException(
"breadboard[0]["+(i+1)+"] is null. " +
"This cell is a column header, and thus must be one of the four possible " +
"allowed ScalingDirective values: NOSCALE, SHRINKS, EXPANDS, or BISCALE");
}
colScale[i] = (ScalingDirective) breadboard[0][i+1];
}
}
// Does the breadboard lack row/column scaling prefixes (= "plain")
// or does it have them. Throws an exception in ambiguous situations.
private boolean isPlainBreadboard(Object[][] breadboard) {
requireRectangularBreadboardArray(breadboard);
boolean result = false;
int rowCount = 0; // # of rows prefixed by scaling directive
int colCount = 0; // # of columns prefixed by a scaling directive
int firstNonDirective = 0;
// ignore upper left cell, which isn't used with scaled header formats
for (int i = 1; i < breadboard.length; i++) {
if (breadboard[i][0] instanceof JComponentBreadboard.ScalingDirective)
rowCount++;
else if (firstNonDirective == 0)
firstNonDirective = i;
}
if (rowCount > 0 && firstNonDirective > 0)
throw new IllegalArgumentException(
"Column #" + firstNonDirective + " of the first row of your breadboard " +
"array is not a scaling directive, " +
"but other columns contain scaling directives. " +
"Either the first row of your breadboard array must contain only the " +
"NOSCALE, SHRINKS, EXPANDS, or BISCALE scaling directives (which would " +
"make it a header), or else it must contain none of these keywords.");
firstNonDirective = 0;
for (int i=1; breadboard.length > 0 && i < breadboard[0].length; i++) {
if (breadboard[0][i] instanceof JComponentBreadboard.ScalingDirective)
colCount++;
else if (firstNonDirective == 0)
firstNonDirective = i;
}
if (colCount > 0 && firstNonDirective > 0)
throw new IllegalArgumentException(
"Row #" + firstNonDirective + " of the first column of your breadboard " +
"array is not a scaling directive, " +
"but other rows contain scaling directives. " +
"Either the first column of your breadboard array must contain only the " +
"NOSCALE, SHRINKS, EXPANDS, or BISCALE scaling directives (which would " +
"make it a header), or else it must contain none of these keywords.");
if (rowCount == 0 && colCount == 0)
result = true;
else
result = false;
return result;
}
// Returns an array in which all columnar (left) and row (above)
// dittos have been expanded into explicit object references
//
// Ditto expansion occurs in a naive manner that works independently
// of if the plain or header-ed breadboard format is being used; errors
// that result from ditto-ing a header into a non-header area will
// be caught later when the non-header area is validated.
private Object[][] decodeDittos(Object[][] breadboard) {
requireRectangularBreadboardArray(breadboard);
Object[][] result = new Object[breadboard.length][breadboard[0].length];
for (int i = 0; i < breadboard.length; i++) {
for (int j = 0; j < breadboard[0].length; j++) {
if (breadboard[i][j] instanceof String &&
((String) breadboard[i][j]).equals(ROWWISE_DITTO)) {
if (0 == i)
throw new IllegalArgumentException(
"Error at breadboard[" + i + "][" + j + "]: " +
"the \"copy of reference immediately above\" ditto keyword (\"\") " +
"cannot be used in row 0, since there is no row above row 0.");
else
result[i][j] = result[i-1][j];
}
else if (breadboard[i][j]==ColumnarDitto.COLUMNAR_DITTO) {
if (0 == j)
throw new IllegalArgumentException(
"Error at breadboard[" + i + "][" + j + "]: " +
"the \"copy of reference immediately to the left\" ditto " +
"keyword ( __ ) cannot be used in column 0, since " +
" there is no column to the left of column 0.");
else
result[i][j] = result[i][j-1];
}
else {
result[i][j] = breadboard[i][j];
}
}
}
return result;
}
/***********************************************************************
Defines this parent container's child components and their layout. The
breadboard array argument defines the rectangular block of cells occupied by
each child component within a flexible rectilinear grid, as well as the
conditions under which each row and column of this grid scales up and/or
scales down in response to the availability of space in the parent
container. This available space is determined, ultimately, by the size
of the top level Dialog or Frame container, which can typically be
altered via the sizing border by the end user.
The breadboard array must be rectangular (all rows must have the same number of
elements) and must have the following general format:
null scalingDirective scalingDirective scalingDirective ...
scalingDirective jComponentRef jComponentRef jComponentRef ...
scalingDirective jComponentRef jComponentRef jComponentRef ...
scalingDirective jComponentRef jComponentRef jComponentRef ...
... ... ... ... ...
In the above:
scalingDirective must be either
NOSCALE , SHRINKS , EXPANDS , or BISCALE . These
row and column headers define how extra/deficit space in the parent is distributed across each row
and column. They do not correspond to any physical space on the form itself.
jComponentRef must be either null , a reference to a JComponent ,
a reference to a 1-D array of JComponents (a JComponent[] ) or
a reference to a rectangular 2-D array of JComponents (a JComponent[][] all
of whose rows have the same number of elements), or a row or column ditto keyword
("" or __) that resolves to such a reference. The area of the breadboard
that contains these jComponentRef s defines the grid positions occupied
by each JComponent on the form, which in turn defines the relative placement of these
JComponents on the form.
The upper left corner cell, though usually left null , may optionally
contain a scalingDirective that can be referenced via row
or column dittos in the row and column headers.
A special no-header format is also supported, in which
the row and column headers are completely eliminated. In this case
each row and column is given, by default, the NOSCALE scaling directive.
Rules governing the placement of jComponentRefs in the breadboard array :
The breadboard array must have at least one non-header cell (to represent an empty breadboard, you can place null into this single cell).
The special keyword "" (conventional ditto) is allowed in any row
except the first. When present, for every such cell, the nearest non-"" reference above
the cell but in the same column is copied into the cell, and the rules
below are then applied to the so-transformed breadboard array.
The special keyword __ (columnar ditto) is allowed in any column
except the first. When present, for every such cell, the nearest non-__ reference to the left of
the cell in the same row is copied into the cell, and the rules
below are then applied to the so-transformed breadboard array.
Individual JComponent references must be placed
into a single rectangular sub-block within the breadboard array. The single
component occupies the corresponding position within the breadboard grid,
which determines the relative placement of this component on the form.
All 2-D array references must refer to rectangular arrays (arrays all of whose
rows have the same number of elements).
All 2-D array references that reference arrays with more than one row and more than one
column must be placed into a single rectangular sub-block of
the breadboard array that has exactly the same number of rows and columns
as the 2-D array (the JComponents in the 2-D array will be placed into
the corresponding cells in the breadboard array).
All 2-D arrays references that reference an array with only one row and more than
one column must be placed into a rectangular sub-block of the breadboard with the same number
of columns and one or more rows. The single JComponent in each column of the 2-D
array is placed into the entire corresponding column of the breadboard sub-block.
All 2-D arrays references that reference an array with only one column and more than
one row must be placed into a rectangular sub-block of the breadboard with the same number
of rows and one or more columns. The single JComponent in each row of the 2-D
array is placed into the entire corresponding row of the breadboard sub-block.
All 1-D array references that reference arrays with more than one element
must be placed into either a single row, or a single
column, that has exactly the same number of elements as the 1-D array. The
JComponents in the array will be placed into the corresponding cells in
the breadboard array.
All 2-D and 1-D array references that contain a single JComponent element, are replaced
with a reference to that single element, and then follow the
same rules as for individual JComponent references.
No JComponent, regardless of if it is placed on the breadboard grid directly,
or indirectly via an array reference,
can be mapped into more than one rectangular sub-block of
the breadboard array.
Array elements may also be null , which indicates unoccupied
space in the grid.
Any references (for example, a reference to a 2-D array with
three zero-element rows) not explicitly mentioned above are illegal.
Failure to comply with the above rules will cause this method to throw
an IllegalArgumentException that is informative enough so that
you don't need to refer back to this list.
How component placement and sizing are determined (the layout algorithm)
Note : The code that implements the layout manager is so simple that you might
prefer to consult it directly (c.f. the layoutContainer method
in JComponentBreadboard.java ) rather than relying on the more
intuitive description below.
There are three basic steps in the determination of component placement and sizing:
The preferred size determined height of each row, and the preferred size
determined width of each column of the breadboard grid are calculated.
If the parent container's actual size is different than the sum of these
preferred size determined heights and widths,
certain rows and columns (as indicated by the row and column scalingDirectives)
are either uniformly scaled up or uniformly scaled down such that (if possible)
the so-rescaled grid exactly fits within the parent container.
Components are positioned and sized within the rectangular block of grid cells
that they occupy.
As a simple example of this process, consider a grid in which each grid
cell is occupied by just a single JComponent (no multi-cell sub-blocks),
and where every row and column has the BISCALE scaling directive:
The preferred size determined height of each row is the maximum
preferred height of all the JComponents within that row. Similarly, the
preferred size determined width of each column is the maximum
preferred width of all JComponents within that column.
Because each row and column contains the BISCALE directive, each
preferred size determined row height is scaled up or down uniformly
such that the sum of all so-rescaled row heights exactly equals the
parent's height. Similarly, each preferred size determined column
width is scaled up or down uniformly such that the sum of all
so-adjusted column widths equals the parent container's actual
width.
With these adjusted grid-sizes in hand, the exact location of the
grid cell that contains each JComponent is determined. Although
the JComponent will always be placed within that cell, the exact location
and sizing within that grid-cell will be determined by consulting additional
layout attributes (such as those governing alignment) described below.
The key role of preferredSize . The
built-in preferred size estimates provided by Swing are central to how
JComponentBreadboard places your components on the form. The key reason
preferred sizes work so well to define basic grid-sizes
is that Swing, by default, computes its preferred size
estimates as the smallest size each JComponent requires to display
itself well. For example, with the default font size, a JButton with the
label "OK" will have a smaller preferred width than one with the label
"Cancel", because part of the preferred width computation takes into
account the amount of space required to display the button's label
without truncating it.
Determining the preferred size determined row and column heights
For each JComponent on the breadboard array we manufacture a rigid steel
bar whose length is equal to the (possibly yUpsize -adjusted)
preferred height of that JComponent.
We construct a series of parallel rigid steel rectangular shelves (each
of negligible thickness), and hang them from the ceiling via a special
system of pulleys that allows them to move up and down while remaining
parallel to the ceiling. These dividers correspond to the bottom edges
of each of the rows in the breadboard array (with the ceiling
representing the top edge of the first row) so there will be N such
dividers, where N is the number of rows in the breadboard array.
We take each rigid bar and weld it to the bottom edge of the steel
divider (or ceiling) that represents the top edge of the grid-cell-block that
contains the corresponding JComponent, so that the bar is perpendicular
to the row dividers, pointing down. We then drill holes immediately
under the spot where the bar was welded in every row divider that
is inside that component's grid-cell-block, but not in the row divider
that represents the bottom edge of this JComponent's grid-cell block. (note that this
implies that JComponents that occupy only a single row in the grid don't require
any drilling). The bar is
threaded through these holes, so that the inner row dividers can move
freely around the bar.
We repeat this process for each JComponent on the grid, adding
appropriately sized bars and drilling holes for intermediary row
dividers where needed, until there is one bar in place for each
JComponent on the breadboard grid.
The pulleys are pulled as tight as possible, until every row-divider-shelf, while
still parallel to the ceiling, is
as close to the ceiling as possible. Namely, each divider is either flush against the bottom
tip of one or more of the steel bars, or else flush against the row
divider immediately above it (or against the ceiling for the first row
divider). The final vertical distances between each metal divider equals the
preferred sized determined height of each row. Note that if we give each
breadboard grid row these heights, the rigid bars assure us that it
will always be possible to fit each JComponent into it's corresponding
grid-cell-block at it's preferred height. Simultaneously, the tight pulleys assure us that
the sum of all these heights is as small as it can possibly be.
This entire process is repeated, in an exactly analogous manner, to
determine the preferred size determined column widths. Specifically, we
use the same apparatus, except that the number of dividers is equal to
the number of columns in the breadboard array, and the bar lengths are
equal to the widths of each component, and the welded-to dividers are
those corresponding to the left-edge of the grid-cell block containing
the component, and holes are drilled in the shelves corresponding to the
inner column dividers of the
each component's grid-cell-block. (Or, if you prefer, just imagine rotating
the breadboard array clockwise through a right angle, so that all the
column dividers become row dividers).
The preferred height and width of the JComponentBreadboard as a whole
(which in turn impacts its own placement within its own parent container) equals
the sum of all of their preferred-height-determined row heights and the
sum of all of their preferred-width-determined column widths.
Note that in the above algorithm, the row heights and column widths are
determined independently . For example, changes to the preferred height of
a component can never have any impact on the preferred size determined column
widths. This generates more easily predicted layout decisions than
layout algorithms (such as that used by GridBagLayout) that lack such
independence.
Scaling up and down to fit within the actual size of the parent container
The sum of all the "preferred row heights" as determined by the above algorithm
may either be:
Equal to the actual height of the parent container
Less than the actual height of the parent container (space surplus in parent)
Greater than the actual height of the parent container (space deficit in parent)
For a top-level JComponentBreadboard occupying the entire content pane,
the size of its parent container changes whenever the
user resizes the Dialog or Frame via it's sizing border.
In the first case, no further rescaling of the row heights is required, and
they simply remain at their preferred-size-determined values.
In the second case, every row with a non-zero preferred height whose row header
contains either the
EXPANDS or BISCALE scaling directive is uniformly scaled up so as to use up the
extra space in the parent (if there are no such "up-scaleable" rows,
no changes are made to the row heights from their preferred size determined values).
In the third case, every row whose row header contains either the SHRINKS or BISCALE
scalingDirective is scaled down uniformly until the so-rescaled grid has a height
that exactly equals the height of the parent container, or until all "down-scalable"
rows have zero height, whichever comes first.
Note that the above rules imply that in some cases it will not be
possible for the grid to be scaled up or down to exactly equal the
height of the parent container. In such cases, the grid will be
positioned as determined by the layout manager of its own parent container.
Exactly analogous rules are applied to distribute any horizontal surplus or deficit
space in the parent container across the columns of the grid.
Placement of each JComponent within it's grid-cell block
The final (possibly rescaled) row heights and column widths determine
exactly the position (within this container's x-y coordinate system) of the
grid-cell-block that contains each JComponent. Although the algorithm
assures that the initial preferred-size-determined grid produces
grid-cell-blocks that are large enough to contain every JComponent at
their preferred sizes, due to the presence of larger components in
nearby cells, as well as the impact of any "scaling up" that may have
been done, the final grid-cell-block could be larger than the JComponent
it contains. Furthermore, due to the impact of any "scaling down" that
may have been done, the final grid-cell-block could also be too small to
contain the JComponent at it's preferred size.
In light of the above, JComponentBreadboard uses the following rules to
determine the exact size and placement of each component within it's
grid-cell block:
If the grid-cell-block and the JComponent are exactly the same height,
(an exact fit) no adjustment is required and the JComponent is simply placed
into the grid-cell block at it's preferred height.
If the block is shorter than the preferred height of the JComponent it contains,
the JComponent's height is simply set equal to the smaller, grid-cell-block's height.
If the block is taller than the JComponent it contains, and if the
JComponent's yFill attribute is true , then the JComponent's height is also set equal
to the height of the grid-cell-block that contains it.
If the block is taller and the yFill attribute is false, then the
JComponent remains at it's preferred height, and will be flush against the
top edge of its grid-cell-block if it's alignmentY property equals 0,
flush against the bottom edge if it's alignmentY property equals 1,
and a fraction "f" of the way between these two extremes if the alignmentY
property equals a number "f" between 0 and 1.
Exactly analogous rules (using xFill instead of yFill , alignmentX
instead of alignmentY , etc.) apply independently to the horizontal
placement and sizing of the JComponent relative to the width of it's
containing grid-cell-block.
For an example of how these rules work themselves out in defining the
placement of components within a specific form, including examples of
each of the four allowed scaling directives, see the
CelsiusFahrenheitConverter application in the JComponentBreadboard
User's Guide.
@param breadboard an array that defines the relative positions of all
components on this form within a flexible, rectilinear grid.
@see #xFill(boolean, JComponent) xFill
@see #yFill(boolean, JComponent) yFill
@see #xAlign(double, JComponent) xAlign
@see #yAlign(double, JComponent) yAlign
@see #xUpsize(int, JComponent) xUpsize
@see #yUpsize(int, JComponent) yUpsize
@see #NOSCALE NOSCALE
@see #SHRINKS SHRINKS
@see #EXPANDS EXPANDS
@see #BISCALE BISCALE
*********************************************************************/
public void setBreadboard(Object[][] breadboard) {
Object[][] dittoDecoded = decodeDittos(breadboard);
if (isPlainBreadboard(dittoDecoded)) {
setAllScales(dittoDecoded,
DEFAULT_SCALING_DIRECTIVE, DEFAULT_SCALING_DIRECTIVE);
setPlainBreadboard(dittoDecoded);
}
else {
setScales(dittoDecoded);
Object[][] plainBreadboard =
new Object[dittoDecoded.length-1][dittoDecoded[0].length-1];
for (int i = 0; i < plainBreadboard.length; i++)
for (int j = 0; j < plainBreadboard[0].length; j++)
plainBreadboard[i][j] = dittoDecoded[i+1][j+1];
setPlainBreadboard(plainBreadboard);
}
}
// Code below this point manages a simplified "signals and
// slots"-like, "Qt connect" facility.
// This list stores the JComponent <==> {object, "getter/setter convention" property name}
// connections created via calls to jbConnect
private ArrayList jbConnections = new ArrayList();
// Does the plugged into object have a public method with the specified
// name and type signature? These signatures are the specific method
// signatures of the various special "getter/setter" methods that are
// recognized and exploited whenever you use jbConnect on one of the
// breadboard's JComponents .
//
// For example:
//
// hasMethod(theObj, "aMethod",int.class,String.class, 0)
//
// would return true if and only if the object "theObj" had a method with the signature:
//
// public int aMethod(String s)
//
// if the last, nArrayIndexes, arg is > 0, looks for a method with a similar signature,
// but with up to two extra int "array indexing" args. For example:
//
// hasMethod(theObj, "aMethod",long.class,String.class, 2)
//
// would return true if and only if the object "theObj" had a method with the signature:
//
// public long aMethod(int iRow, int iCol, String s)
//
private static boolean hasMethod(Object theObj, String methodName,
Class retType, Class argType, int nArrayIndexes) {
Method[] theMethods = theObj.getClass().getMethods();
boolean result = false;
int nValidatedArgs = 0;
if (argType != null && argType != Void.TYPE) nValidatedArgs++;
nValidatedArgs += nArrayIndexes;
for (int i = 0; i < theMethods.length && !result; i++) {
if (theMethods[i].getName().equals(methodName) &&
retType.equals(theMethods[i].getReturnType())) {
// method with same name and return type found...
int iArgs = theMethods[i].getParameterTypes().length;
if (nValidatedArgs == iArgs) { // # of args must match
if ((nArrayIndexes < 1 ||
int.class.equals(theMethods[i].getParameterTypes()[0])) &&
(nArrayIndexes < 2 ||
int.class.equals(theMethods[i].getParameterTypes()[1])) &&
(argType == null || argType == Void.TYPE ||
argType.equals(theMethods[i].getParameterTypes()[iArgs-1])))
result = true;
}
}
}
return result;
}
// represent a possibly null object reference as a string
private static String objRefToString(Object obj) {
String result = "null";
if (obj != null)
result = obj.toString();
return result;
}
// represents an argument list as a string for error reporting purposes
private static String argsToString(Object[] args) {
String result = "";
for (int i = 0; args != null && i < args.length; i++) {
result += objRefToString(args[i]);
if (i < args.length-1) result += ", ";
}
return result;
}
// Returns copy of given string whose first character is in uppercase.
private static String firstToUpper(String s) {
String result = s.substring(0,1).toUpperCase() + s.substring(1, s.length());
return result;
}
// Turns out code below makes refreshes of forms with lost of
// connections an order of magnitude faster, simply by keeping track of
// the object/method pairs that don't exist, and not trying to access
// them via Java reflection more than once. Empirically, the overhead
// of using Java's reflection to determine non-existence is much higher
// than just looking it up on a HashSet. Exploits the fact that most
// methods that COULD be plugged into a connected component (optional,
// auxiliary, methods) are not used.
// Can be shown that memory used by the hash has an upper bound equal
// to a small constant times the number of connectable properties on
// all JComponentBreadboard derived classes (usually, it will be much
// less than this--layout only forms have no such costs, for
// example)--so it can be viewed as a (reasonable) fixed overhead
// memory cost per form.
private static HashSet noSuchMethodHash = new HashSet();
private static Object[] tmpHolder = new Object[3]; // static to avoid "heap-stress"
private static int methodHashCode(Object obj, String methodName, Object[] args) {
tmpHolder[0] = obj.getClass();
tmpHolder[1] = methodName;
// Hidden dependency alert: the only possible args that matter for a
// unique hash code (given other checks/requirements specific to how
// this method is used within JComponentBreadboard) are integer indexing
// arguments, of which there can be 0, 1, or 2, so the number of args is
// sufficient for uniqueness. Watch out for bugs here if you make major
// method signature related changes (e.g. introducing string-indexed array support).
tmpHolder[2] = (null == args) ? 0 : args.length;
int result = Arrays.hashCode(tmpHolder);
return result;
}
private static void addNoSuchMethodMethod(Object obj, String methodName, Object[] args) {
int thisMethodsCode = methodHashCode(obj, methodName, args);
noSuchMethodHash.add(thisMethodsCode);
}
private static boolean methodMayExist(Object obj, String methodName, Object[] args) {
boolean result = true;
int thisMethodsCode = methodHashCode(obj, methodName, args);
if (noSuchMethodHash.contains(thisMethodsCode))
result = false;
return result;
}
// Gets value of the property with a given name in the given object.
// Returns null if property does not exist.
private static Object getMethodValue(Object obj, String methodName, Object[] args) {
Object result = null;
if (methodMayExist(obj, methodName, args)) {
Expression getExpr = new Expression(obj, methodName, args);
try {
result = getExpr.getValue();
}
catch(NoSuchMethodException e) {
addNoSuchMethodMethod(obj, methodName, args);
result = null; // null used to indicate "no such property"
}
catch (Exception e) {
throw new IllegalStateException(objRefToString(obj) + "." + methodName + "(" + argsToString(args) + ") failed unexpectedly.",e);
}
}
return result;
}
/*
// Gets value of the property with a given name in the given object.
// Returns null if property does not exist.
private static Object getMethodValue(Object obj, String methodName, Object[] args) {
Object result = null;
Expression getExpr = new Expression(obj, methodName, args);
try {
result = getExpr.getValue();
}
catch(NoSuchMethodException e) {
result = null; // null used to indicate "no such property"
}
catch (Exception e) {
throw new IllegalStateException(objRefToString(obj) + "." + methodName + "(" + argsToString(args) + ") failed unexpectedly.",e);
}
return result;
}
*/
// Gets specified property by call a getXxx or isXxx format method
// with specified args.
//
// The args contain int indexes associated with array connections.
//
private Object getObjectProperty(Object obj, String propName, Object[] args) {
Object result = Void.TYPE;
if (propName != null) {
result = getMethodValue(obj, "get" + firstToUpper(propName), args);
if (result == null) { // in case it is a boolean isXXX() type method
// For example, without this line, JCheckBox, which uses isSelected
// instead of getSelected, won't work.
result = getMethodValue(obj, "is" + firstToUpper(propName), args);
}
}
return result;
}
// Gets an getXxx or isXxx method defined property of the specified object
private Object getObjectProperty(Object obj, String propName) {
return getObjectProperty(obj, propName, null);
}
private Object getObjectProperty(Object obj, String propName, Object arg1) {
Object[] args = {arg1};
return getObjectProperty(obj, propName, args);
}
// Set the specified property of the JComponent to the given value
// Assumes a "set" method associated with the specified property exists
private void setObjectProperty(Object obj, String propName, Object[] args) {
Expression setExpr = new Expression(obj, "set" + firstToUpper(propName), args);
try {
setExpr.execute();
}
catch (Exception e) {
throw new IllegalStateException(objRefToString(obj) + ".set"+firstToUpper(propName) + "(" + argsToString(args) + ") failed unexpectedly.",e);
}
}
// Calls the setter of given object associated with given property
// name, passing it the given value.
private void setObjectProperty(Object obj, String propName, Object propValue) {
Object[] args = {};
if (propValue!=Void.TYPE) args = new Object[] {propValue};
setObjectProperty(obj , propName, args);
}
// Creates and returns an appropriate pluggable adapter for the given user
// manipulated JComponent when neccessary, otherwise, simply returns the
// manipulated component as the pluggable component.
private static Object maybeMakepluggableObject(JComponent manipulatedComponent) {
Object result = manipulatedComponent;
// The new-s or make-s below throw appropriate IllegalArgumentExceptions
// when the specific configuration of the JComponent fails to meet
// additional requirements needed to make that JComponent
// "JComponentBreadboard connectable".
if (manipulatedComponent instanceof JButton) {
result = new JButtonAdapter((JButton) manipulatedComponent);
}
else if (manipulatedComponent instanceof JComboBox) {
result = new JComboBoxAdapter((JComboBox) manipulatedComponent);
}
else if (manipulatedComponent instanceof JSpinner) {
result = JSpinnerAdapter.makeSpinnerAdapter((JSpinner) manipulatedComponent);
}
// else normal case: user manipulated and pluggable components are the
// same, and there are no special connectability requirements.
return result;
}
// returns second object when not null, otherwise returns first
private Object fixNull(Object forNull, Object obj) {
if (null == obj)
return forNull;
else
return obj;
}
// The {JComponent <==> {modelObj, rootName}} relation defining a connection
// JComponentBreadboard uses the setPropName/getPropName naming convention, unless
// a get cannot be found, in which case it looks for an isPropName
// method in lieu of the missing getter.
// Note: the pluggedIntoObject is always just the JComponentBreadboard
// itself. Initially I thought I would allow plugging into other objects,
// and maybe I will later, but for now I decided against it.
private class JBConnection {
// Below, the manipulated component is always the component that the programmer
// creates, places on their breadboard form, etc., and that the end user directly
// clicks on, enters stuff into, etc.
//
// The pluggable component is the component that accepts all of the
// setter/getter related requests made by JComponentBreadboard as a result of it
// being connected to the form via jbConnect. Usually, this is just another
// reference to the manipulated component. However, for JSpinners and
// JComboBoxes, such requests get routed through special JCBAdapter classes.
// Because of how these two classes employ their related ComboBoxModel and
// SpinnerModel classes to change their behaviors, they lack the simple,
// type-specific, JCheckBox-style-I-just-do-booleans setter/getter interface
// that JComponentBreadboard requires. JCBAdapter classes smooth out such
// incompatibilities. For example, with JSpinners, there are actually four
// different adapter classes (JSpinnerIntAdapter, JSpinnerDoubleAdapter,
// JSpinnerDateAdapter and JSpinnerStringAdapter) for use with JSpinners whose
// models have been configured to allow them to manipulate integers, doubles,
// dates or strings.
public JComponent manipulatedComponent; // JComponent manipulated directly by user
public Object pluggableObject; // Object directly plugged into the form
// (either a JComponent or a JCBAdapter)
protected Object pluggedIntoObject; // The JComponentBreadboard form itself
protected String pluggedIntoProperty; // Main manipulated getter/setter property
// in the JComponentBreadboard form (i.e.
// if "X" that implies "getX" and "setX"
// methods will define the main
// manipulated property).
JBConnection(JComponent manipulatedComponent, Object obj, String prop) {
pluggableObject = maybeMakepluggableObject(manipulatedComponent);
this.manipulatedComponent = manipulatedComponent;
pluggedIntoObject = obj;
pluggedIntoProperty = prop;
}
// returns the user manipulated model property
public Object getManipulatedModelProperty() {
return getObjectProperty(pluggedIntoObject,pluggedIntoProperty);
}
// sets the model property that user can manipulate through the JComponent
public void setManipulatedModelProperty(Object value) {
setObjectProperty(pluggedIntoObject, pluggedIntoProperty, value);
}
// Returns an error indicating string (suitable for display to
// user or else one of the keywords REVERT_QUIETLY or
// REVERT_AND_BEEP), the keyword DATA_IS_VALID if there is nothing
// wrong, or null if no InvalidDataMessage tagged method for the
// component exists. Thus this method both flags when an error is
// present, and returns information that determines how
// JComponentBreadboard responds to that error.
// JComponentBreadboard's data validation loop calls this method
// to determine if user entered valid text or not, and what to do
// about it if they didn't.
//
// This method is only used by connections that involve JComponents whose
// main manipulated property is of type String (so, for example,
// connected JTextFields can implement it but JCheckboxes cannot).
public String getInvalidDataMessage(String testInput) {
return (String) fixNull(DATA_IS_VALID, getObjectProperty(pluggedIntoObject,
pluggedIntoProperty + "InvalidDataMessage", testInput));
}
// properties that can be read from the model object, but that the
// user cannot manipulate directly via the JComponent (e.g. enabled status,
// visibility, the drop-down list of a combobox, etc.
public Object getViewonlyProperty(String propName) {
return getObjectProperty(pluggedIntoObject,
pluggedIntoProperty + firstToUpper(propName));
}
}
// similar to a JBConnection, except that methods of accessing the
// model's data contain an additional row index arg associated with a
// 1-D array (or a similarly indexed data structure).
class JBArrayConnection1D extends JBConnection {
private int rowIndex = 0; // each connection has a fixed location in the array
JBArrayConnection1D(JComponent jc, Object obj, String prop, int rowInd) {
super(jc, obj, prop);
rowIndex = rowInd;
}
private Object getObjectArrayProperty(Object obj, String propName, int iRow) {
Object[] args = {iRow};
return getObjectProperty(obj, propName, args);
}
private void setObjectArrayProperty(Object obj, String propName,
int iRow, Object propValue) {
Object[] args;
if (propValue==Void.TYPE)
args = new Object[] {iRow};
else
args = new Object[] {iRow, propValue};
setObjectProperty(obj, propName, args);
}
private Object getObjectArrayProperty(Object obj, String propName,
int iRow, String testInput) {
Object[] args = {iRow, testInput};
return getObjectProperty(obj, propName, args);
}
// These methods are direct analogues of those for the parent class, except the methods
// must accept one additional arg that provides the row index.
public Object getManipulatedModelProperty() {
return getObjectArrayProperty(pluggedIntoObject,pluggedIntoProperty, rowIndex);
}
public void setManipulatedModelProperty(Object value) {
setObjectArrayProperty(pluggedIntoObject, pluggedIntoProperty, rowIndex, value);
}
public String getInvalidDataMessage(String testInput) {
return (String) fixNull(DATA_IS_VALID, getObjectArrayProperty(pluggedIntoObject,
pluggedIntoProperty + "InvalidDataMessage", rowIndex, testInput));
}
public Object getViewonlyProperty(String propName) {
return getObjectArrayProperty(pluggedIntoObject,
pluggedIntoProperty + firstToUpper(propName), rowIndex);
}
}
// similar to a JBConnection, except that methods of accessing the
// model's data contain additional row, col index args associated with a
// 2-D array (or a similarly indexed data structure).
class JBArrayConnection2D extends JBConnection {
private int rowIndex = 0; // each connection has a fixed location in the array
private int colIndex = 0;
JBArrayConnection2D(JComponent jc, Object obj, String prop,
int rowInd, int colInd) {
super(jc, obj, prop);
rowIndex = rowInd;
colIndex = colInd;
}
private Object getObjectArrayProperty(Object obj, String propName,
int iRow, int iCol) {
Object[] args = {iRow, iCol};
return getObjectProperty(obj, propName, args);
}
private void setObjectArrayProperty(Object obj, String propName,
int iRow, int iCol, Object propValue) {
Object[] args;
if (propValue==Void.TYPE)
args = new Object[] {iRow, iCol};
else
args = new Object[] {iRow, iCol, propValue};
setObjectProperty(obj, propName, args);
}
private Object getObjectArrayProperty(Object obj, String propName,
int iRow, int iCol, String testInput) {
Object[] args = {iRow, iCol, testInput};
return getObjectProperty(obj, propName, args);
}
// these methods are direct analogues of those for the parent class, except the methods
// must accept two additional args that provide the row, col indexes.
public Object getManipulatedModelProperty() {
return getObjectArrayProperty(pluggedIntoObject,pluggedIntoProperty, rowIndex, colIndex);
}
public void setManipulatedModelProperty(Object value) {
setObjectArrayProperty(pluggedIntoObject, pluggedIntoProperty, rowIndex, colIndex, value);
}
public String getInvalidDataMessage(String testInput) {
return (String) fixNull(DATA_IS_VALID, getObjectArrayProperty(pluggedIntoObject,
pluggedIntoProperty + "InvalidDataMessage", rowIndex, colIndex, testInput));
}
public Object getViewonlyProperty(String propName) {
return getObjectArrayProperty(pluggedIntoObject,
pluggedIntoProperty + firstToUpper(propName), rowIndex, colIndex);
}
}
// Returns the connection associated with the given JComponent, or null
// if there is no such connection.
private JBConnection connection(JComponent jc) {
JBConnection result = null;
for (int i = 0; i < jbConnections.size(); i++) {
if (jc.equals(jbConnections.get(i).manipulatedComponent)) {
if (result != null) throw new
IllegalStateException("Connection "+i+" involves a JComponent used in an earlier connection. Each JComponent can only be plugged into a single getter/setter based property.");
result = jbConnections.get(i);
}
}
return result;
}
// combines two delimited elements to form a longer delimited element
private static String addDelimitedElements(String elem1, String delim, String elem2) {
String result;
if (elem1.equals(""))
result = elem2;
else if (elem2.equals(""))
result = elem1;
else
result = elem1 + delim + elem2;
return result;
}
// return simple signature string for use in error messages and the like
private static String signatureText(String methodName, Class retType, Class argType, int nArrayIndexes) {
String argList = (nArrayIndexes > 0) ? "int iRow" : "";
argList = addDelimitedElements(argList, ", ",
(nArrayIndexes > 1) ? "int iCol" : "");
argList = addDelimitedElements(argList, ", ",
argType.equals(void.class) ? "" : (argType.getSimpleName() + " value"));
String result =
"public " + retType.getSimpleName() + " " + methodName + "(" + argList + ")";
return result;
}
// Comment in front of blocks of empty-bodied required or optional injected methods
// associated with a jbConnect (as indicated via the specified parameters)
private static String getInjectedMethodComment(boolean isRequired,
int nArrayIndexes,
JComponent connectedComponent,
JBComponentInfo jci,
String pluggedIntoProperty) {
String result = "// ";
result += isRequired ? "REQUIRED" : "OPTIONAL";
result += " 'jbConnect(" + ((null==jci.prototypicalInstance) ?
connectedComponent.getClass().getSimpleName() : jci.prototypicalInstance) +
((0==nArrayIndexes) ? "" : (1==nArrayIndexes) ? "[]": "[][]") + "," +
"\"" + pluggedIntoProperty + "\"" +
")' ";
if (!isRequired) {
result += "injected methods that can\n";
result += "// be implemented to define auxiliary properties of this " +
connectedComponent.getClass().getSimpleName() + ":";
}
else if (jci.requiresSetter() && jci.requiresGetter()) {
result += "injected methods\n" +
"// that define the " +
jci.pluggablePropertyType[MAIN_PROP].getSimpleName() +
" viewed and controlled by this " +
connectedComponent.getClass().getSimpleName() + ":";
}
else if (jci.requiresSetter()) {
result += "injected method that is\n" +
"// invoked by this " +
connectedComponent.getClass().getSimpleName() + "'s " +
"event handler:";
}
else if (jci.requiresGetter()) { // no components currently use this branch
result += "injected method that defines\n" +
"// this " +
connectedComponent.getClass().getSimpleName() + "'s " +
" main value:";
}
else {
result += "injected methods (none)";
}
return result;
}
// Connecting a JComponent to a property implicitly defines an interface that
// the JComponentBreadboard MUST implement, namely, the getter and setter
// that define the model property the component is connected to (for example,
// the boolean that a connected checkbox allows the user to manipulate). Such
// a connection also implicitly defines a series of optional (a.k.a. mix-in)
// interfaces that the JComponentBreadboard MAY implement to dynamically
// define view-only properties of the component (for example, the component's
// enabled status, visibility, tool tip text, etc.)
//
// This funtion returns a string (suitable for sending to the console during
// exceptions thrown when the user fails to implement the required
// part of the injected interface) that the user can cut and paste into
// their JComponentBreadboard class to save them the time of looking up
// and typing in the exact signatures of the methods of these injected interfaces.
//
// It's not as good as having the IDE paste such stub methods right into your
// code, but it gets you 80% of the value of that, is very easy to implement,
// and it works with any (or even without any) IDE.
//
private static String getInjectedMethodBoilerplate(JComponent connectedComponent,
String pluggedIntoProperty, int nArrayIndexes) {
boolean isRequired = true;
JBComponentInfo jci = componentInfo(
maybeMakepluggableObject(connectedComponent));
String requiredBlock = getInjectedMethodComment(isRequired,nArrayIndexes,
connectedComponent,jci, pluggedIntoProperty);
requiredBlock += "\n";
isRequired = false;
String optionalBlock = getInjectedMethodComment(isRequired,nArrayIndexes,
connectedComponent,jci, pluggedIntoProperty);
optionalBlock += "\n";
if (jci.requiresGetter()) {
String getterSignature = "// " +
signatureText("get" + firstToUpper(pluggedIntoProperty),
jci.pluggablePropertyType[MAIN_PROP], void.class,
nArrayIndexes) + " {}";
requiredBlock += getterSignature;
requiredBlock += "\n";
}
if (jci.requiresSetter()) {
requiredBlock += "// " +
signatureText("set" + firstToUpper(pluggedIntoProperty),
void.class,jci.pluggablePropertyType[MAIN_PROP],
nArrayIndexes) + " {}";
requiredBlock += "\n";
// every setter, if present, is a required setter
}
if (jci.hasManipulatedModelString()) {
String invalidDataGetterSignature = "// " +
signatureText("get" + firstToUpper(pluggedIntoProperty) + "InvalidDataMessage",
String.class, String.class, nArrayIndexes) + " {}";
optionalBlock += invalidDataGetterSignature;
optionalBlock += "\n";
}
// All properties after the first, key, property are optional, view-only, getters
for (int i=MAIN_PROP+1; i < jci.pluggableProperty.length; i++) {
optionalBlock += "// " +
signatureText("get" + firstToUpper(pluggedIntoProperty) +
firstToUpper(jci.pluggableProperty[i]),
jci.pluggablePropertyType[i], void.class,
nArrayIndexes) + " {}";
optionalBlock += "\n";
}
String result = "\n//\n" + requiredBlock + "//\n" + optionalBlock;
return result;
}
/** @deprecated - only used in the generation the injected interfaces table
** used within the javadocs of JComponentBreadboard */
public static String getInjectedInterfacesTable(JComponent[] prototypes,
String manipulatedPropName) {
StringBuffer result = new StringBuffer();
StringBuffer injectedInterfaces = new StringBuffer();
result.append("\n");
// loop over each JBComponentInfo
// append a specific description of the class, type, and
// links to the injected interface to the
// main value table
result.append(
"JComponent Type of main property Injected interface \n");
for (int i=0; i < prototypes.length; i++) {
JBComponentInfo jci = componentInfo(maybeMakepluggableObject(prototypes[i]));
result.append(
"" +
prototypes[i].getClass().getSimpleName() + " " +
((null == jci.pluggablePropertyType[MAIN_PROP]) ?
"--none--"
:jci.pluggablePropertyType[MAIN_PROP].getSimpleName()) + " " +
"injected interface \n"
);
// for each protype JComponent, get the injected interface comment block,
// prefixing it with an appropriate anchor tag
injectedInterfaces.append(
" \n");
injectedInterfaces.append("\n" +
getInjectedMethodBoilerplate(prototypes[i],manipulatedPropName, 0) +
"\n ");
}
result.append("
");
// concatenate the table and injected interface sections the table links to
result.append(injectedInterfaces);
return result.toString();
}
// validates that a connection between a JComponent and a JComponentBreadboard
// property satisfies various requirements of such a connection.
private void validateConnection(JComponent jbComponent, Object pluggedIntoObj, String pluggedIntoProperty, int nArrayIndexes) {
// JComponent has got to be of a type JComponentBreadboard can handle:
JBComponentInfo jci = componentInfo(maybeMakepluggableObject(jbComponent));
if (null == jci) {
throw new IllegalArgumentException("Objects of class: " + jbComponent.getClass().getName() + " are not JComponentBreadboard-connectable.");
}
else if (!Modifier.isPublic(pluggedIntoObj.getClass().getModifiers())) {
throw new IllegalStateException(
"Your " + pluggedIntoObj.getClass().getSimpleName() +
" object must be declared with the \"public\" modifier, if you want to jbConnect to it. " +
"So, you must add \"public\" to your \"" + pluggedIntoObj.getClass().getSimpleName() +"\" class declaration statement.");
}
else if (jci.requiresSetter() &&
!hasMethod(pluggedIntoObj, "set" + firstToUpper(pluggedIntoProperty),void.class,jci.pluggablePropertyType[MAIN_PROP],nArrayIndexes)) {
throw new IllegalStateException(
"Your " + pluggedIntoObj.getClass().getSimpleName() +
" object must have a setter method \"" +
signatureText("set" + firstToUpper(pluggedIntoProperty),void.class,jci.pluggablePropertyType[MAIN_PROP],nArrayIndexes) +
"\" because your code contains a jbConnect(" +
jbComponent.getClass().getSimpleName() +
((0==nArrayIndexes) ? "" : (1==nArrayIndexes) ? "[]" : "[][]") +
",\"" + pluggedIntoProperty + "\") method call." +
getInjectedMethodBoilerplate(jbComponent, pluggedIntoProperty, nArrayIndexes));
}
else if (jci.requiresGetter() &&
!hasMethod(pluggedIntoObj, "get" + firstToUpper(pluggedIntoProperty),
jci.pluggablePropertyType[MAIN_PROP],null,nArrayIndexes) &&
(boolean.class != jci.pluggablePropertyType[MAIN_PROP] ||
!hasMethod(pluggedIntoObj, "is" + firstToUpper(pluggedIntoProperty),
jci.pluggablePropertyType[MAIN_PROP],null,nArrayIndexes))
) {
throw new IllegalStateException(
"Your " + pluggedIntoObj.getClass().getSimpleName() +
" object must have a getter method \"" +
signatureText("get" + firstToUpper(pluggedIntoProperty), jci.pluggablePropertyType[MAIN_PROP], void.class, nArrayIndexes) +
"\" because your code contains a jbConnect(" +
jbComponent.getClass().getSimpleName() +
((0==nArrayIndexes) ? "" : (1==nArrayIndexes) ? "[]" : "[][]") +
",\"" + pluggedIntoProperty + "\") method call." +
getInjectedMethodBoilerplate(jbComponent, pluggedIntoProperty, nArrayIndexes));
}
else if (connection(jbComponent) != null) {
throw new IllegalArgumentException
("Attempt to jbConnect a JComponent to a " + pluggedIntoObj.getClass().getSimpleName() + " object, twice. " +
"Only one jbConnect based connection per JComponent is allowed." +
getInjectedMethodBoilerplate(jbComponent, pluggedIntoProperty, nArrayIndexes));
}
}
/**
** @deprecated - for internal use only, must be public for technical
** reasons.
**
** This is a specially tagged JMenuItem used exclusively for
** JComponentBreadboard's hot key support.
**
*/
public static class JCBKeyStroke extends JMenuItem {
public JCBKeyStroke() {
super();
}
public JCBKeyStroke(String string) {
super(string);
}
}
// validates a connection involving a KeyStroke
private void validateConnection(KeyStroke keyStroke,
Object pluggedIntoObj,
String pluggedIntoProperty,
int nArrayIndexes) {
// first check that they didn't connect the same keystroke twice:
if (0 != hotKeyMenuBar.getMenuCount()) {
if (1 != hotKeyMenuBar.getMenuCount()) // this should be impossible
throw new AssertionError("A hotKeyMenuBar can only have 0 or 1 menus on it.");
Component[] c = hotKeyMenuBar.getMenu(0).getMenuComponents();
for (int i = 0; i < c.length; i++) {
if (((JCBKeyStroke) c[i]).getAccelerator() == keyStroke) {
throw new IllegalArgumentException(
"Attempt to jbConnect the KeyStroke \"" + keyStroke.toString() +
"\" to a specific " + pluggedIntoObj.getClass().getSimpleName() + " object, twice. " +
"Only one jbConnect-based connection per KeyStroke is allowed." +
getInjectedMethodBoilerplate((JCBKeyStroke) c[i], pluggedIntoProperty, nArrayIndexes));
}
}
}
// else, this is the first connected keyStroke of any kind, so it's OK
// next, make sure that the special connection interface for a
// keystroke-representing JMenuItem (tagged via the JCBKeyStroke
// class) has been implemented
validateConnection(new JCBKeyStroke(),
pluggedIntoObj, pluggedIntoProperty, nArrayIndexes);
}
// adds a standard JComponentBreadboard listener that responds appropriately to
// user manipulations of the single "user manipulated property" of
// the JComponent, validating, setting object value, etc. as needed.
@SuppressWarnings("unchecked")
private void addJBListener(JComponent jbComponent, String rootName) {
JBComponentInfo jbInfo = componentInfo(maybeMakepluggableObject(jbComponent));
try {
// this is the parameter list that gets passed to the addXXXListener
// method (EventHandler.create's generic, getter/setter bound, listener)
if (jbInfo.requiresSetter()) {
// The next line creates an appropriate listener class that calls JComponentBreadboard's
// setUserManipulatedModelProperty() method, passing the source
// JComponent in as an argument. setUserManipulatedModelProperty looks up (on the
// jbConnections table) the object/property associated with the JComponent, and
// sets it to the current value of that JComponent's user manipulated property (e.g.,
// that would be the user entered string for a JTextField). See the JDK's
// EventHandler docs for more info about this next line.
Object[] pList = {EventHandler.create(jbInfo.changeIndicatingListenerInterface,
this, "userManipulatedModelProperty", "source",
jbInfo.changeIndicatingMethodName)};
// if an "ActionListener" is the change indicating interface,
// the addListenerMethodName below would be "addActionListener"
String addListenerMethodName =
"add" + jbInfo.changeIndicatingListenerInterface.getSimpleName();
Expression addXXXListenerCmd =
new Expression(jbComponent, addListenerMethodName, pList);
addXXXListenerCmd.execute();
}
// else no user manipulated property, hence no event handler (e.g.
// a "view only" JComponent such as JLabel)
}
// this just is a stopgap for whatever validateConnection() doesn't catch
catch (Exception ex) {
throw new IllegalStateException(
"Error connecting a " + jbComponent.getClass().getName() +
" to the JComponentBreadboard property " + rootName, ex);
}
}
/*********************************************************************************
Injects an interface into this JComponentBreadboard-based form that binds the
connectable properties of the specified JComponent to the form via
correspondingly-named (getter/setter defined) properties of the interface.
The injected interface of a connectable JComponent of a
specific sub-type (JCheckBox , JLabel , etc.) contains
methods associated with up to 1 main (viewable and controllable)
property, and 1 or more auxiliary (viewable only) properties. For
example, a JTextField contains 1 main property corresponding to
its user editable String whereas a JLabel does not have a main
property, since it is not possible for the user to directly change a
JLabel 's state by interacting with it. However, both components
support auxiliary properties corresponding their enabled and
visible properties.
When present, the part of the injected interface corresponding to the
main property must be implemented in the JComponentBreadboard
whose jbConnect method is invoked (an
IllegalStateException will be raised otherwise). This required
part of the interface has the follow general format:
public MainPropertyType get RootName ()
public void set RootName (MainPropertyType value )
In the above:
RootName is simply the rootName String passed in as the second parameter
of jbConnect , except that, in accord with Java naming conventions,
the first character is capitalized.
MainPropertyType is the type of the main property associated with
the connected JComponent . For example, this would be a
String for a JTextField , a boolean for a
JCheckBox and a double for a JSpinner configured so
as to allow the entry of floating point values.
JButton and JMenuItem have a MainPropertyType of void
and are a special case.
For connections involving such components, the setter method has no arguments
(it is called for it's side-effects--for example, it performs some
calculation when the user clicks the button)
and the getter method (why would you need to get nothing?) is eliminated.
For example, if a JComponentBreadboard's constructor contained the line:
jbConnect(myJTextField, "userName");
Then, that JComponentBreadboard's class would be required to implement two
methods such as those shown below:
{@code
private String theUserName = "";
public String getUserName() {
return theUserName;
}
public void setUserName(String userEnteredUserName) {
theUserName = userEnteredUserName;
}
}
Because the jbConnect was executed, JComponentBreadboard assures
that whenever the user finalizes their edits of myJTextField 's String
(for example, by tabbing to the next field), the
setUserName method is invoked with the user-modified String as
its argument. Also, it assures that whenever the
theUserName field is changed as a side-effect of the user
interacting with some other connected component on the form , (e.g.
by clicking a "login as JohnSmith" checkbox whose bound setter method contains the line
theUserName = "JohnSmith"; ) the getUserName method is
invoked and the value it returns is stuffed back into the
String displayed by myJTextField .
For a complete example that uses jbConnect to synchronize the
checked/unchecked state of a JCheckBox (its main property) with
a boolean setter/getter defined property of a JComponentBreadboard-based
form, see the HelloWorldPlus
application in the JComponentBreadboard User's Guide.
For an example of using jbConnect to keep more than one component in synch
whenever the user changes any one of them, see the
CelsiusFahrenheitConverter example application in the JComponentBreadboard
User's Guide. In this application, two JSliders and two JSpinners are
all connected to the form so that they all view and control a single underlying double
that represents the temperature in degrees Celsius. Whenever the user
changes the temperature via any one of these connected JComponents , the
newly selected temperature is automatically properly displayed by all
four connected JComponents .
The auxiliary (view-only) part of the injected interface has the following
general format:
public TypeOfAuxiliaryProp1 get RootName NameOfAuxiliaryProp1 ()
public TypeOfAuxiliaryProp2 get RootName NameOfAuxiliaryProp2 ()
...
public TypeOfAuxiliaryPropN get RootName NameOfAuxiliaryPropN ()
In the above:
RootName , as before, is the first-letter-capitalized string passed in
as jbConnect 's second parameter.
TypeOfAuxiliaryProp1 is the type of the first auxiliary property. For example,
for the enabled property this would be boolean .
NameOfAuxiliaryProp1 is the first-letter-capitalized name of the first
auxiliary property. This generally corresponds to the part of the name of a
corresponding setter method of the JComponent that comes after "set". For example,
the name corresponding to setVisible would be "Visible",
the name corresponding to setEnabled , "Enabled", etc.
In a small number of cases, the name does not correspond to any setter method
in the connected JComponent, but rather represents a "grafted on" JComponentBreadboard
convenience extension of the class. For example, a connected JComboBox supports
a String[] "items" property (which lets you dynamically change list items
by implementing, say, public String[] getMyComboBoxItems() ).
However, JComboBox does not actually contain a
setItems(String[] listItems) method. Behind the scenes,
JComponentBreadboard emulates such a setter using the JComboBox 's
setModel method.
For a simple auxiliary property example, see the HelloWorldPlus
application in the User's Guide, which implements an auxiliary property
getter with the signature public String
getUseExpectedPhraseToolTipText() to dynamically change the
toolTipText property of a connected JCheckBox .
Data Validation Method Signatures
In additional to the above methods, JComponents that have a main,
user manipulated, property of of type String can optionally
validate user entered Strings by implementing a data validation method
with the following signature:
public String get RootName InvalidDataMessage (String testInput )
This method must be defined so as to return either:
DATA_IS_VALID if the testInput passed to it is valid.
REVERT_QUIETLY if the testInput passed to it is not valid,
and you want to quietly reject the user's entry by reverting to the last validated
entry.
REVERT_AND_BEEP if the testInput passed to it is not valid,
and you want to beep and then to reject the user's entry by reverting to the last validated entry.
An arbitrary String message (which can include the Swing subset of HTML) that is
suitable for display in a pop-up dialog describing the
error if the testInput is not valid.
This popup allows the user either to correct the entry or
to revert to the last validated value.
Use the HTML <title> tag within this string
to define the title of the pop-up error dialog (for example,
<title>Invalid Numeric Entry</title> ). If the title tag
isn't included in the returned string, the default title of the
popup dialog is "Invalid Input".
This method, if defined, will be automatically invoked immediately after the
user finishes entering a new String into the associated JComponent (the entry
is finalized when the JComponent loses the focus). Note that,
though such interactive user edits are always validated, progammatically
made changes to the bound-properties associated with the JComponent are not .
Connecting one JComponentBreadboard to another
JComponentBreadboard extends JPanel , and the injected interface
of a connected JComponentBreadboard is the same as for a
JPanel . However, there is one important functional difference.
Whenever the user interacts with one of the JComponents connected to the
connected-from JComponentBreadboard in a manner than changes its state,
any JComponents connected to the connected-to JComponentBreadboard
will be automatically updated to reflect any changes to the underlying
objects they are bound to.
Uni-directional, bi-directional and circular connections, to any depth, are supported.
For example if two JComponentBreadboards
are connected to each other, user initiated changes to components connected
to either of them will automatically be reflected in both.
Detailed Injected Interface Specifications
The exact signatures of the various injected methods discussed above
depend upon the exact subclass of the JComponent being connected,
and in some cases also on how that object has been configured. For
example, floating point JSpinners and integer
JSpinners , though of exactly the same class, have different
injected interfaces that reflect the fact that, due to their different
configurations, their main properties have different types (double vs
int).
To help you determine the exact injected interface for a given
connection, JComponentBreadboard provides a feature called
exceptional code generation that displays the complete set of
method signatures (the so-called injected interface for the
connection) as part of any exception raised by this method.
When implemented, these methods must be public methods of the
JComponentBreadboard sub-class that issued the jbConnect that
injected the interface.
A handy
trick is to issue the exact same jbConnect call twice. Since
each JComponent can only be connected once, this produces an
exception that includes the full interface specs for the connected
JComponent in question.
The non-array forms of every possible exceptional code generated
interface are tabulated below for your convenience (for the 1-D or 2-D
array forms of these interfaces, simply prepend one or two integer
arguments to each method's argument list). Follow the "injected
interface" link on each row of the table to see the interface associated
with JComponents of that type (or their sub-classes). The
generated code shown for each interface was obtained by passing in an
object of that table row's class as the first,
and a dummy rootName argument of
"xxx" as the second, parameter of jbConnect .
Internally, JComponentBreadboard stores a table each row of which
defines the injected interfaces associated with each connectable class,
and always chooses the interface associated with the most closely
matching class as the interface for the specific JComponent type you are
connecting. For example, connections to instances of
JPasswordField (an descendent of JTextComponent ) in effect
inheirit JTextComponent's injected interface, because there is no explicitly
defined JPasswordField row on the table. The table
includes a JComponent row, so technically all JComponents are
connectable. However, the injected interface associated with this
least-common-denominator JComponent row (which is used by the
JSeparator class, for example) does not have a main property,
and supports only limited, generic, auxiliary properties
(enabled , visible , etc.)
Major connectable JComponent types, and their injected interfaces
{@code.sample AS_IS doc-files\injected_interfaces.html}
Adding or Expanding a Type-Specific Injected Interface
In cases in which the connectable properties of the JComponent have
simple setter/getter based interfaces it is quite easy to add support
for a new JComponent sub-class. However, this can only be done
by changing the source code. You simply add a new row to the
jbComponents table (defined statically in the
JComponentBreadboard.java file) and then fill in the various slots
defining the component's injected interface (these slots include places
for the names/types of the main property and auxiliary properties) in
that table row (each such row contains an instance of the class
JBComponentInfo ). See existing table rows in the source code
(indeed, you can often copy such an existing row and use it as a starting point) for
examples. It's even easier to extend the interface of an already
supported JComponent class by adding additional auxiliary methods.
@param jcomponent the JComponent whose interface is to be injected into this
JComponentBreadboard.
@param rootName all the method names in the injected interface begin with either "get" or "set"
and are all followed by the specified rootName (except that it's first letter will
be capitalized).
@see #DATA_IS_VALID DATA_IS_VALID
@see #REVERT_AND_BEEP REVERT_AND_BEEP
@see #REVERT_QUIETLY REVERT_QUIETLY
@see #DEFAULT_INVALID_DATA_MESSAGE_TITLE DEFAULT_INVALID_DATA_MESSAGE_TITLE
****************************************************************************/
public void jbConnect(JComponent jcomponent, String rootName) {
validateConnection(jcomponent, this, rootName, 0); // consistency checks
JBConnection conn = new JBConnection(jcomponent, this, rootName);
jbConnections.add(conn);
addJBListener(jcomponent, rootName);
}
/**
Injects an interface into this JComponentBreadboard-based form that
binds the connectable properties of the specified 1-D JComponent array to the form
via correspondingly-named, 1-D-array-indexed, getter/setter defined, properties of the interface.
Except for an extra, initial, integer argument indicating the 0-based position of each component
within the array , the injected interface for each JComponent of the array
is the same as it would have been had each individual JComponent
been connected via code such as:
jbConnect(componentArray[i], rootName);
Note: null elements are allowed--they are quietly skipped.
For example, if the array contained JCheckBoxes , and were connected via the line:
jbConnect(myCheckBoxArray, "myCheckboxes");
Then the following code would be one possible implementation of the
required part of the associated interface injected into the JComponentBreadboard
whose jbConnect method was invoked:
private boolean[] myBooleans = new boolean[myCheckBoxArray.length];
public boolean getMyCheckboxes(int iRow) {
return myBooleans[iRow];
}
public void setMyCheckboxes(int iRow, boolean isChecked) {
myBooleans[iRow] = isChecked;
}
See the RandomWalks
application in the User's Guide for an example that, by
jbConnect ing to an array of JSpinners, allows the user to specify
an array of integer walk lengths via the spinners.
The types of the JComponents in the array need not be the same, but if
they have different injected interfaces you will have to implement the
required methods of every applicable interface. For example, if your
array contained both JCheckBox and JTextField
elements, you would be required to implement both a getter of the form
public boolean getXxx(int iRow) and another of the form
public String getXxx(int iRow) (plus the two corresponding
setter methods).
@param componentArray the array of JComponents to be connected to this form via
an injected, array element offset indexed, interface
@param rootName the root name to be included immediately after the get or set
(except with the first letter capitalized) in the method names of the injected
interface
@see #jbConnect(JComponent, String) jbConnect(JComponent, String)
*/
public void jbConnect(JComponent[] componentArray, String rootName) {
for (int iRow=0; iRow < componentArray.length; iRow++) {
if (null != componentArray[iRow]) {
validateConnection(componentArray[iRow], this, rootName, 1); // consistency checks
JBConnection conn = new JBArrayConnection1D(
componentArray[iRow], this, rootName, iRow);
jbConnections.add(conn);
addJBListener(componentArray[iRow], rootName);
}
}
}
/**
Injects an interface into this JComponentBreadboard-based form that
binds the connectable properties of the specified 2-D JComponent array to the form
via correspondingly-named, 2-D-array-indexed, getter/setter method defined,
properties of the interface.
Except for two extra, initial, integer arguments indicating the 0-based row and
column positions of each component
within the array , the injected interface for each JComponent of the array
is the same as it would have been had each individual JComponent
been connected via code such as:
jbConnect(componentArray[i][j], rootName);
Note: null array elements are allowed--they are quietly skipped.
For example, if the array contained JCheckBoxes , and were connected via the line:
jbConnect(myCheckBoxArray, "myCheckboxes");
Then the following code would be one possible implementation of the
required part of the associated interface injected into the JComponentBreadboard
whose jbConnect method was invoked:
private boolean[] myBooleans = new boolean[myCheckBoxArray.length][myCheckBoxArray[0].length];
public boolean getMyCheckboxes(int iRow, int iCol) {
return myBooleans[iRow][iCol];
}
public void setMyCheckboxes(int iRow, int iCol, boolean isChecked) {
myBooleans[iRow][iCol] = isChecked;
}
See the DateChooser
sample application for an example that connects a 2-D array of
JToggleButtons to facilitate the implementation of a traditional
monthly-calendar-style date selection pop-up modal dialog.
The types of the JComponents in the array need not be the same, but if
they have different injected interfaces you will have to implement the
required methods of every applicable interface. For example, if your
array contained both JCheckBox and JTextField
elements, you would be required to implement both a getter of the form
public boolean getXxx(int iRow, int iCol) and another of the
form public String getXxx(int iRow, int iCol) (plus the two
corresponding setter methods).
@param componentArray the 2-D array of JComponents to be connected to the form via
an injected, array element offset indexed, interface.
Any null elements will be quietly skipped.
@param rootName the root name to be included immediately after the get or set
(except with the first letter capitalized) in the method names of the injected
interface.
@see #jbConnect(JComponent, String) jbConnect(JComponent, String)
@see #jbConnect(JComponent[], String) jbConnect(JComponent[], String)
*/
public void jbConnect(JComponent[][] componentArray, String rootName) {
for (int iRow=0; iRow < componentArray.length; iRow++) {
for (int iCol=0; componentArray[iRow] != null &&
iCol < componentArray[iRow].length; iCol++) {
if (null != componentArray[iRow][iCol]) {
validateConnection(componentArray[iRow][iCol], this, rootName, 2); // consistency checks
JBConnection conn = new JBArrayConnection2D(
componentArray[iRow][iCol], this, rootName, iRow, iCol);
jbConnections.add(conn);
addJBListener(componentArray[iRow][iCol], rootName);
}
}
}
}
// adds, to the hidden JMenuBar used for the keystroke binding or hot key feature,
// a specially tagged JMenuItem to capture the specified keystroke
private JCBKeyStroke addHiddenKeyStrokeMenuItem(KeyStroke keyStroke) {
JMenu hotKeyMenu;
if (0 == hotKeyMenuBar.getMenuCount()) {
hotKeyMenu = new JMenu();
// zero size so it will be hidden, but visible so it's hot keys will work
hotKeyMenu.setPreferredSize(new Dimension(0,0));
hotKeyMenu.setVisible(true);
hotKeyMenuBar.add(hotKeyMenu);
}
else {
hotKeyMenu = hotKeyMenuBar.getMenu(0);
}
JCBKeyStroke result = new JCBKeyStroke();
result.setAccelerator(keyStroke);
hotKeyMenu.add(result);
return result;
}
/**
Injects an interface into this JComponentBreadboard-based form that binds the
connectable properties of the specified KeyStroke to the form via
correspondingly-named, getter/setter defined, properties of the interface.
The KeyStroke interface works similarly to that of a JButton or
JMenuItem ,
except that pressing the keystroke, rather than clicking the button
or selecting the menu item, fires the event handling code.
The KeyStroke handler is only active when this JComponentBreadboard, or
one of the JComponents it contains, has the focus. Thus,
different regions of the form, each associated with a different
JComponentBreadboard sub-form, could each have, for example, different
arrow-key handlers.
However, for most applications KeyStroke handling is
uniform across the entire form, so you will want to connect KeyStrokes to a
single, top-level, JComponentBreadboard that is the only top-level
JComponent within the parent Dialog or Frame.
In the special case where every focusable JComponent on a top-level
JComponentBreadboard is disabled, Swing's default focus handling will
result in a situation in which the top-level JComponentBreadboard does
not have the focus, and thus the KeyStroke handlers for the top-level
JComponentBreadboard will not work. If this is not what you want in
this (very unusual) situation, you will need to invoke the
requestFocusInWindow method of the top-level
JComponentBreadboard.
Connected KeyStroke s work exactly as if a hidden JMenuItem with the
specified KeyStroke as it's accelerator , were added and connected to
the JComponentBreadbreadboard in lieu of the KeyStroke . Though you usually won't
have to know this to use it, in fact, KeyStroke support is currently
implemented by adding and connecting to such a hidden JMenuItem .
Because connecting to a KeyStroke works exactly like connecting to
a hidden JMenuItem (of the special tagging subclass of JMenuItem called
JCBKeyStroke ), all of the discussion under the jbConnect(JComponent, String)
method applies equally to the KeyStroke connections created by this method. In
particular, see the JCBKeyStroke row of the injected interfaces table in
that method's javadocs for the interface supported by a connected KeyStroke .
See the NumericInputDialog
application of the JComponentBreadboard User's Guide for an example that
jbConnects to a KeyStroke in order to support
canceling the dialog by pressing the ESCAPE key.
@param keyStroke the KeyStroke for which an interface is to be injected into
this JComponentBreadboard form
@param rootName the root name that always follows immediately after the get or set
(except that the first letter will be capitalized) in the method names of the injected
interface.
@see #jbConnect(JComponent, String) jbConnect(JComponent, String)
*/
public void jbConnect(KeyStroke keyStroke, String rootName) {
validateConnection(keyStroke, this, rootName, 0); // consistency checks
JCBKeyStroke hiddenMenuItem = addHiddenKeyStrokeMenuItem(keyStroke);
jbConnect(hiddenMenuItem, rootName);
}
/**
Injects an interface into this JComponentBreadboard-based form that
binds the connectable properties of the specified 1-D KeyStroke array to the form
via correspondingly-named, 1-D-array-indexed, properties of the interface.
Works in a manner exactly analogous to jbConnect(JComponent[],String) except
that the elements of the array are KeyStrokes, rather than JComponents . See that
method for further details.
@param keyStrokeArray the array of KeyStrokes to be connected to the form via
an injected, array element offset indexed, interface
@param rootName the root name to be included immediately after the get or set
(except with the first letter capitalized) in the method names of the injected
interface.
@see #jbConnect(JComponent[],String) jbConnect(JComponent[],String)
@see #jbConnect(KeyStroke, String) jbConnect(KeyStroke, String)
*/
public void jbConnect(KeyStroke[] keyStrokeArray, String rootName) {
JCBKeyStroke[] menuItemArray = new JCBKeyStroke[keyStrokeArray.length];
for (int iRow=0; iRow < keyStrokeArray.length; iRow++) {
if (null != keyStrokeArray[iRow]) {
validateConnection(keyStrokeArray[iRow], this, rootName, 1); // consistency checks
menuItemArray[iRow] = addHiddenKeyStrokeMenuItem(keyStrokeArray[iRow]);
}
}
jbConnect(menuItemArray, rootName);
}
/**
Injects an interface into this JComponentBreadboard-based form that
binds the connectable properties of the specified 2-D KeyStroke array to the form
via correspondingly-named, row and column indexed, properties of the interface.
Works in a manner exactly analogous to jbConnect(JComponent[][],String) except
that the elements of the array are KeyStrokes , rather than JComponents . See that
method for further details.
@param keyStrokeArray the array of KeyStrokes to be connected to the form via
an injected, row and column offset indexed, interface
@param rootName the root name to be included immediately after the get or set
(except with the first letter capitalized) in the method names of the injected
interface.
@see #jbConnect(JComponent[][],String) jbConnect(JComponent[][],String)
@see #jbConnect(KeyStroke, String) jbConnect(KeyStroke, String)
*/
public void jbConnect(KeyStroke[][] keyStrokeArray, String rootName) {
JCBKeyStroke[][] menuItemArray = new JCBKeyStroke[keyStrokeArray.length][];
for (int iRow=0; iRow < keyStrokeArray.length; iRow++) {
if (null != keyStrokeArray[iRow]) {
menuItemArray[iRow] = new JCBKeyStroke[keyStrokeArray[iRow].length];
for (int iCol=0; iCol < keyStrokeArray[iRow].length; iCol++) {
if (null != keyStrokeArray[iRow][iCol]) {
validateConnection(keyStrokeArray[iRow][iCol], this, rootName, 2); // consistency checks
menuItemArray[iRow][iCol] =
addHiddenKeyStrokeMenuItem(keyStrokeArray[iRow][iCol]);
}
}
}
}
jbConnect(menuItemArray, rootName);
}
/**
** @deprecated - for internal use only, must be public for technical reasons
**
** A JCBAdapter provides a specific setter/getter interface for a JComponent
**
** Most JComponents have only one such interface, and that interface is
** provided directly by the component itself. Such JComponents do not require
** any JCBAdapter classes. These JComponents typically allow the user to
** manipulate a single well-defined value (the boolean of a JCheckBox,
** the String of a JTextField, etc.)
**
** By contrast, some JComponents allow the type of the user manipulated
** value to change, depending on how they are configured. For example, a
** JSpinner allows the user to manipulate a Date when configured to use a
** SpinnerDateModel, but allows them to select a specific String from a list
** when configured with a SpinnerListModel. In general a separate JCBAdapter
** class is needed for each distinct data type that a JComponent allows the
** user to manipulate.
**
** Each such adapter contains a reference to the underlying JComponent it
** provides an interface to. Calls to the adapter's interface are translated
** into appropriate calls to this underlying JComponent. The adapter also
** contains compatibility checks to make sure that the JComponent is
** configured so as to be compatible with the adapter (e.g. that a
** JSpinnerIntegerAdapter isn't used with a JSpinner that uses a
** SpinnerDateModel).
**
** Appropriate adapters from those below are automatically wrapped around
** the original user-specified JComponents behind the scenes, when those
** JComponents get jbConnected(). Thus users don't directly work with these
** adapter classes (although their names can appear in certain error
** messages). Both these adapters, as well as the original JComponents, are
** stored on the jbConnections table, so that both can be accessed whenever
** needed.
**
**/
public static class JCBAdapter {} // common parent class is just for tagging purposes
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JComboBoxAdapter extends JCBAdapter {
JComboBox theComboBox;
public JComboBoxAdapter(JComboBox theComboBox) {
if (!(theComboBox.getModel() instanceof DefaultComboBoxModel)) {
throw new IllegalArgumentException(
"Your JComboBox uses the " + theComboBox.getModel().getClass().getSimpleName() +
" model. Only a JComboBox that utilizes the DefaultComboBoxModel " +
" can be plugged into JComponentBreadboard via jbConnect.");
}
this.theComboBox = theComboBox;
}
// the main setter/getter pair for the combobox
public void setSelectedItem(String selectedItem) {
theComboBox.setSelectedItem(selectedItem);
}
public String getSelectedItem() {
String result = (String) theComboBox.getSelectedItem();
return result;
}
// the view only properties of the combobox. Since the interface is only
// used by JComponentBreadboard to pump the model properties into the
// JComponent during jbRefresh() calls, only setters are needed for these
// view-only properties
public void setEnabled(boolean isEnabled) {
theComboBox.setEnabled(isEnabled);
}
public void setVisible(boolean isEnabled) {
theComboBox.setVisible(isEnabled);
}
public void setToolTipText(String toolTipText) {
theComboBox.setToolTipText(toolTipText);
}
public void setForeground(Color foreground) {
theComboBox.setForeground(foreground);
}
public void setBackground(Color background) {
theComboBox.setBackground(background);
}
public void setEditable(boolean editable) {
theComboBox.setEditable(editable);
}
public void setPreferredSize(Dimension d) {
theComboBox.setPreferredSize(d);
}
// This method is the main reason we need an adapter: the JComboBox
// provides no simple setter method for the list items themselves.
public void setItems(String[] newList) {
// Note: see http://forum.java.sun.com/thread.jspa?threadID=542445&tstart=225
// for why using the removeAllItems() and then addItem() method to add each
// string in the array, does not work:
theComboBox.setModel(new DefaultComboBoxModel(newList));
}
}
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JButtonAdapter extends JCBAdapter {
JButton theButton;
public JButtonAdapter(JButton theButton) {
this.theButton = theButton;
}
// the main setter/getter pair for the button
public void setText(String text) {
theButton.setText(text);
}
public String getText() {
String result = theButton.getText();
return result;
}
// the view only properties of the button. Since the interface is only
// used by JComponentBreadboard to pump the model properties into the
// JComponent during jbRefresh() calls, only setters are needed for these
// view-only properties
public void setEnabled(boolean isEnabled) {
theButton.setEnabled(isEnabled);
}
public void setVisible(boolean isEnabled) {
theButton.setVisible(isEnabled);
}
public void setToolTipText(String toolTipText) {
theButton.setToolTipText(toolTipText);
}
public void setForeground(Color foreground) {
theButton.setForeground(foreground);
}
public void setBackground(Color background) {
theButton.setBackground(background);
}
public void setPreferredSize(Dimension d) {
theButton.setPreferredSize(d);
}
public void setContentAreaFilled(boolean contentAreaFilled) {
theButton.setContentAreaFilled(contentAreaFilled);
}
// JButtons have no "isDefaultButton" property. However,
// it's handy to be able to act as if such a property existed.
//
// Logically, user's should not set multiple default buttons, but
// if they do, the last executed setter will define the default.
//
public void setIsDefaultButton(boolean isDefaultButton) {
JRootPane root = theButton.getRootPane();
if (null != root) {
if (isDefaultButton) {
if (root.getDefaultButton() != theButton)
root.setDefaultButton(theButton);
// else this button is already the default, so do nothing
}
else {
if (theButton == root.getDefaultButton())
root.setDefaultButton(null);
// else some other button is already the default, so do nothing
}
}
}
}
/**
** @deprecated - for internal use only, must be public for technical reasons
**
** Convenient place for common methods used by all JSpinner adapters.
**
**/
public abstract static class JSpinnerAdapter extends JCBAdapter {
protected JSpinner theSpinner;
// The specific adapter chosen for a JSpinner depends upon the type and
// configuration of the "SpinnerModel" employed by the JSpinner. Only the
// most common four configurations: Integer, Double, Date, and selectable
// String from a list of Strings, are supported. Any other more esoteric
// combinations (e.g a Long() or a Byte() spinner) will raise an exception).
public static JSpinnerAdapter makeSpinnerAdapter(JSpinner realSpinner) {
JSpinnerAdapter result = null;
if (JSpinnerIntegerAdapter.isAdaptable(realSpinner))
result = new JSpinnerIntegerAdapter(realSpinner);
else if (JSpinnerDoubleAdapter.isAdaptable(realSpinner))
result = new JSpinnerDoubleAdapter(realSpinner);
else if (JSpinnerDateAdapter.isAdaptable(realSpinner))
result = new JSpinnerDateAdapter(realSpinner);
else if (JSpinnerStringAdapter.isAdaptable(realSpinner))
result = new JSpinnerStringAdapter(realSpinner);
else
throw new IllegalArgumentException(
"Your JSpinner isn't configured in a way that is JComponentBreadboard-connectable. " +
" Only JSpinners that are configured so that the manipulate either an Integer, a Double,a Date, or that select a String from " +
" an array of Strings, are JComponentBreadboard-connectabled.");
return result;
}
@SuppressWarnings("unchecked")
protected static String wrongModelErrorMessage(JSpinner spinner,
Class requiredSpinnerModel) {
String result = null;
if (!(requiredSpinnerModel.isAssignableFrom(spinner.getModel().getClass()))) {
result =
"Your JSpinner argument's SpinnerModel must be an instance of the " +
requiredSpinnerModel.getSimpleName() + " class. " +
"But, your SpinnerModel is instead of the class " +
spinner.getModel().getClass().getSimpleName() + ".";
}
return result;
}
@SuppressWarnings("unchecked")
protected static String notAdaptableNumberSpinnerErrorMsg(JSpinner spinner,
Class requiredSpinnerModel,
Class spunObjectType) {
String result = null; // null return means "it's OK"
String spinType = requiredSpinnerModel.getSimpleName();
String spunType = spunObjectType.getSimpleName();
if (null != wrongModelErrorMessage(spinner, requiredSpinnerModel))
result = wrongModelErrorMessage(spinner, requiredSpinnerModel);
else if (null == spinner.getModel().getValue())
result = "Your JSpinner argument's " + spinType + " has a null " +
"\"value\" property. A non-null " + spunType + " is required.";
else if (null != ((SpinnerNumberModel) (spinner.getModel())).getMinimum() &&
!spunObjectType.isAssignableFrom(((SpinnerNumberModel) spinner.getModel()).getMinimum().getClass()))
result = "Your JSpinner argument's " + spinType + "'s " +
"\"minimum\" property is an instance of the " +
((SpinnerNumberModel) (spinner.getModel())).getMinimum().getClass().getSimpleName() +
" class. An instance of the " + spunType + " class is required.";
else if (null != ((SpinnerNumberModel) (spinner.getModel())).getMaximum() &&
!spunObjectType.isAssignableFrom(((SpinnerNumberModel) spinner.getModel()).getMaximum().getClass()))
result = "Your JSpinner argument's " + spinType + "'s " +
"\"maximum\" property is an instance of the " +
((SpinnerNumberModel) (spinner.getModel())).getMaximum().getClass().getSimpleName() +
" class. An instance of the " + spunType + " class is required.";
else if (null != ((SpinnerNumberModel) (spinner.getModel())).getStepSize() &&
!spunObjectType.isAssignableFrom(((SpinnerNumberModel) (spinner.getModel())).getStepSize().getClass()))
result = "Your JSpinner argument's " + spinType + "'s " +
"\"stepSize\" property is an instance of the " +
((SpinnerNumberModel) (spinner.getModel())).getStepSize().getClass().getSimpleName() +
" class. An instance of the " + spunType + " class is required.";
else if (!spunObjectType.isAssignableFrom(spinner.getModel().getValue().getClass()))
result = "Your JSpinner argument's " + spinType + "'s " +
"\"value\" property is an instance of the " +
spinner.getModel().getValue().getClass().getSimpleName() +
" class. An instance of the " + spunType + " class is required.";
// else SpinnerModel is configured properly for a JSpinner that has the
// specified spinner model that is configured to spin a data value of
// the given "spunType".
return result;
}
// various setter methods that simply pass through to the JSpinner,
// or do generic stuff that is the same for all flavors of JSpinner
public void setEnabled(boolean enabled) {
theSpinner.setEnabled(enabled);
}
public void setVisible(boolean visible) {
theSpinner.setVisible(visible);
}
public void setToolTipText(String toolTipText) {
theSpinner.setToolTipText(toolTipText);
}
// Setting the spinner's foreground and background does nothing, so we
// pass these requests on to the "editor" piece (a JTextComponent that displays
// and lets the user edit the spinner's value) of the JSpinner
public void setForeground(Color foreground) {
theSpinner.getEditor().setForeground(foreground);
}
public void setBackground(Color background) {
theSpinner.getEditor().setBackground(background);
}
public void setPreferredSize(Dimension d) {
theSpinner.setPreferredSize(d);
}
};
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JSpinnerIntegerAdapter extends JSpinnerAdapter {
public static String notAdaptableErrorMsg(JSpinner toBePluggedIntoThisAdapter) {
return notAdaptableNumberSpinnerErrorMsg(toBePluggedIntoThisAdapter,
SpinnerNumberModel.class,
Integer.class);
}
public static boolean isAdaptable(JSpinner spinner) {
boolean result = (null == notAdaptableErrorMsg(spinner));
return result;
}
private void requireAdaptableSpinner(JSpinner spinner) {
if (!isAdaptable(spinner))
throw new IllegalArgumentException(notAdaptableErrorMsg(spinner));
}
JSpinnerIntegerAdapter(JSpinner theSpinner) {
requireAdaptableSpinner(theSpinner);
this.theSpinner = theSpinner;
}
// setter/getter pair associated with an "int" JSpinner
public int getValue() {
requireAdaptableSpinner(theSpinner);
int result = ((Integer) (theSpinner.getModel().getValue())).intValue();
return result;
}
public void setValue(int value) {
theSpinner.getModel().setValue(new Integer(value));
}
public void setMaximum(int max) {
if (getValue() > max) setValue(max);
((SpinnerNumberModel) theSpinner.getModel()).setMaximum(new Integer(max));
}
public void setMinimum(int min) {
if (getValue() < min) setValue(min);
((SpinnerNumberModel) theSpinner.getModel()).setMinimum(new Integer(min));
}
public void setStepSize(int stepSize) {
((SpinnerNumberModel) theSpinner.getModel()).setStepSize(new Integer(stepSize));
}
}
// ***********************************************
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JSpinnerDoubleAdapter extends JSpinnerAdapter {
public static String notAdaptableErrorMsg(JSpinner toBePluggedIntoThisAdapter) {
return notAdaptableNumberSpinnerErrorMsg(toBePluggedIntoThisAdapter,
SpinnerNumberModel.class,
Double.class);
}
public static boolean isAdaptable(JSpinner spinner) {
boolean result = (null == notAdaptableErrorMsg(spinner));
return result;
}
private void requireAdaptableSpinner(JSpinner spinner) {
if (!isAdaptable(spinner))
throw new IllegalArgumentException(notAdaptableErrorMsg(spinner));
}
JSpinnerDoubleAdapter(JSpinner theSpinner) {
requireAdaptableSpinner(theSpinner); // catch incompatibilities up front
this.theSpinner = theSpinner;
}
// setter/getter pair associated with a "double" JSpinner
public double getValue() {
// in case the programmer changes the model in mid-stream, next line will
// catch it on the next call to jbRefresh()
requireAdaptableSpinner(theSpinner);
Double obj = (Double) ((SpinnerNumberModel) theSpinner.getModel()).getValue();
double result = 0.0;
if (obj != null) result = obj.doubleValue();
return result;
}
public void setValue(double value) {
theSpinner.getModel().setValue(new Double(value));
}
public void setMaximum(double max) {
((SpinnerNumberModel) theSpinner.getModel()).setMaximum(new Double(max));
}
public void setMinimum(double min) {
((SpinnerNumberModel) theSpinner.getModel()).setMinimum(new Double(min));
}
public void setStepSize(double stepSize) {
((SpinnerNumberModel) theSpinner.getModel()).setStepSize(new Double(stepSize));
}
}
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JSpinnerDateAdapter extends JSpinnerAdapter {
private static String notAdaptableDateSpinnerErrorMsg(JSpinner spinner,
Class requiredSpinnerModel) {
String result = null;
if (null != wrongModelErrorMessage(spinner, requiredSpinnerModel))
result = wrongModelErrorMessage(spinner, requiredSpinnerModel);
else if (spinner.getValue().getClass() != Date.class)
result =
"A JSpinnerDateAdapter's value must be of the Date type.";
return result;
}
public static String notAdaptableErrorMsg(JSpinner toBePluggedIntoThisAdapter) {
return notAdaptableDateSpinnerErrorMsg(toBePluggedIntoThisAdapter,
SpinnerDateModel.class);
}
public static boolean isAdaptable(JSpinner spinner) {
boolean result = (null == notAdaptableErrorMsg(spinner));
return result;
}
private void requireAdaptableSpinner(JSpinner spinner) {
if (!isAdaptable(spinner))
throw new IllegalArgumentException(notAdaptableErrorMsg(spinner));
}
JSpinnerDateAdapter(JSpinner theSpinner) {
requireAdaptableSpinner(theSpinner); // catch incompatibilities up front
this.theSpinner = theSpinner;
}
// setter/getter pair associated with a "date" JSpinner
public Date getValue() {
// in case the programmer changes the model in mid-stream, next line will
// catch it on the next call to jbRefresh()
requireAdaptableSpinner(theSpinner);
Date result = (Date) theSpinner.getModel().getValue();
return result;
}
public void setValue(Date value) {
theSpinner.getModel().setValue(value);
}
public void setStart(Date start) {
((SpinnerDateModel) theSpinner.getModel()).setStart(start);
}
public void setEnd(Date end) {
((SpinnerDateModel) theSpinner.getModel()).setEnd(end);
}
// Note: because the user can change the calendar field, and because that
// would represent a second user manipulated property if we supported it
// (violating our basic "one JComponent, one user-manipulated property"
// principle, we don't support connecting to the calendarField
}
/** @deprecated - for internal use only, must be public for technical reasons */
public static class JSpinnerStringAdapter extends JSpinnerAdapter {
private static String notAdaptableStringSpinnerErrorMsg(JSpinner spinner,
Class requiredSpinnerModel) {
String result = null;
if (null != wrongModelErrorMessage(spinner, requiredSpinnerModel))
result = wrongModelErrorMessage(spinner, requiredSpinnerModel);
else if (0 == ((SpinnerListModel) spinner.getModel()).getList().size() ||
((SpinnerListModel) spinner.getModel()).getList().get(0).getClass() != String.class)
result =
"A JSpinnerStringAdapter's list of objects must contain at least one " +
" element, and all of the elements must be of the String type.";
return result;
}
public static String notAdaptableErrorMsg(JSpinner toBePluggedIntoThisAdapter) {
return notAdaptableStringSpinnerErrorMsg(toBePluggedIntoThisAdapter,
SpinnerListModel.class);
}
public static boolean isAdaptable(JSpinner spinner) {
boolean result = (null == notAdaptableErrorMsg(spinner));
return result;
}
private void requireAdaptableSpinner(JSpinner spinner) {
if (!isAdaptable(spinner))
throw new IllegalArgumentException(notAdaptableErrorMsg(spinner));
}
JSpinnerStringAdapter(JSpinner theSpinner) {
requireAdaptableSpinner(theSpinner); // catch incompatibilities up front
this.theSpinner = theSpinner;
}
// setter/getter pair associated with a "date" JSpinner
public String getValue() {
// in case the programmer changes the model in mid-stream, next line will
// catch it on the next call to jbRefresh()
requireAdaptableSpinner(theSpinner);
String result = (String) theSpinner.getModel().getValue();
return result;
}
public void setValue(String value) {
theSpinner.getModel().setValue(value);
}
public void setItems(String[] items) {
((SpinnerListModel) theSpinner.getModel()).setList(Arrays.asList(items));
}
}
// defines the information needed to dynamically bind JComponents to
// JComponentBreadboard attributes (something that would normally require manually
// creating inner listener classes).
// Array index of the single "main, user-manipulated" property. If the JComponent
// lacks such a property, the first slot of the corresponding arrays should be null.
private static final int MAIN_PROP = 0;
private static class JBComponentInfo {
// JComponent adapter class (JButtonAdapter). Frequently, the JComponent
// is its own adapter. All sets/gets on the component generated as a consequence
// of it having been connected via jbConnect are executed through this adapter.
public Class adapterClass;
//Used in boilerplate comments to stand for a generic JComponent
//instance. Typically, it is null, and then .getSimpleName() on the
//connected component is used in these comments.
public String prototypicalInstance;
// Name of the event listener interface responsible for "hearing"
//the "final/notifying" change event associated with the first listed
//("main manipulated" ) pluggable property of JComponents of this
//type. For example, for the JCheckBox class, this is an
//ActionListener (all other pluggable properties besides the first
//are view only--they cannot be directly manipulated by users, and
//hence do not have listeners). This field must be null for
// JComponents, like JLabels, that are not directly user
// manipulated.
Class changeIndicatingListenerInterface;
// The method name within the above listener interface that is
// associated with "final/notifying" changes to the first
// "pluggable property" of this type of JComponent. For example,
// for the JCheckBox class, this is the actionPerformed method,
// for the JTextField class it is focusLost. Leave null for
// JComponents, like JLabels, that are not directly user
// manipulated.
public String changeIndicatingMethodName;
// Lists names of the JComponent properties that represent the
// "pluggable properties" of this JComponent. The first on the
// list is the component property that can be directly manipulated
// by the user, and is bound to a corresponding getter/setter
// based property defined in the form (=this
// JComponentBreadboard). From JComponentBreadboard's perspective,
// the main purpose of a JComponent is usually to view and allow
// the user to control this first listed "main property". For
// example, this property would be "text" for a JTextField, the
// boolean representing its checked state ("selected") for a
// JCheckBox, etc. Subsequent properties on the list represent
// "auxiliary" properties that, though not controlled directly by
// the user can, via specially named getter methods in the form,
// be bound to programmatically defined form properties. Examples
// include "enabled", "visible", etc.
public String[] pluggableProperty;
// Corresponding data types of each of the named properties of the
// preceeding field (correspondence is based on position in the array).
public Class[] pluggablePropertyType;
// Components that have non-void user manipulated properties, like
// JTextFields, must have a way for the component to read the
// model values (a getter) that the user is manipulating and that
// the component is connected to. Components that lack user
// manipulated properties, like JLabels, or whose user manipulated
// property is void (like JButtons) don't require such a getter.
public boolean requiresGetter() {
return null != pluggablePropertyType[MAIN_PROP] &&
void.class != pluggablePropertyType[MAIN_PROP];
}
// Do connections to this component require the user to create an
// associated setter method. For components that have user
// manipulated properties, the setter must set the main manipulated
// property (the one returned by the getter). For components, like
// JButtons, whose main manipulated property is void, the
// setter merely triggers some code to execute (a click response handler)
// but it still must be implemented. JLabels have a null main manipulated
// property and do not require setters--the user has no way of changing
// the program's state by directly interacting with a JLabel.
public boolean requiresSetter() {
return null != pluggablePropertyType[MAIN_PROP];
}
// Do components of the adapterClass type have a user-manipulated
// string? (only such components support getXXXInvalidDataMessage()
// based data validation)
public boolean hasManipulatedModelString() {
boolean result = (String.class == pluggablePropertyType[MAIN_PROP]);
return result;
}
JBComponentInfo(Class adapterClass, String prototypicalInstance,
Class changeIndicatingListenerInterface, String changeIndicatingMethodName,
String[] pluggableProperty, Class[] pluggablePropertyType) {
this.adapterClass = adapterClass;
this.prototypicalInstance = prototypicalInstance;
this.changeIndicatingListenerInterface = changeIndicatingListenerInterface;
this.changeIndicatingMethodName = changeIndicatingMethodName;
this.pluggableProperty= pluggableProperty;
this.pluggablePropertyType = pluggablePropertyType;
}
}
// The information needed to treat a JComponent as a viewer/controller of
// a main property, and a viewer of one or more auxiliary properties,
// is stored in this table, one record for each JComponentBreadboard
// compatible JComponent type.
private static JBComponentInfo[] jbComponents = {
new JBComponentInfo(JButtonAdapter.class, null,
ActionListener.class, "actionPerformed",
new String[] {null, "text", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "contentAreaFilled", "isDefaultButton"},
new Class[] {void.class, String.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, boolean.class, boolean.class}
),
new JBComponentInfo(JCBKeyStroke.class, "KeyStroke",
ActionListener.class, "actionPerformed",
new String[] {null, "enabled"},
new Class[] {void.class, boolean.class}
),
new JBComponentInfo(JMenuItem.class, null,
ActionListener.class, "actionPerformed",
new String[] {null, "text", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "contentAreaFilled"},
new Class[] {void.class, String.class, boolean.class,boolean.class, String.class, Color.class, Color.class, Dimension.class, boolean.class}
),
// JToggleButton is the common parent of JCheckBox and JRadioButton so this
// row handles all three
new JBComponentInfo(JToggleButton.class, null,
ActionListener.class, "actionPerformed",
new String[] {"selected", "text", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "contentAreaFilled"},
new Class[] {boolean.class, String.class,boolean.class,boolean.class, String.class, Color.class, Color.class, Dimension.class, boolean.class}
),
// For simplicity, interface only supports JComboBoxes whose dropdown lists contain Strings
new JBComponentInfo(
JComboBoxAdapter.class, null,
ActionListener.class, "actionPerformed",
new String[] {"selectedItem", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "items", "editable"},
new Class[] {String.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, String[].class, boolean.class}
),
// Following the advice of the JDK, that user defined borders only work right
// with JLabels and JPanels, border property is only supported for descendents of these.
new JBComponentInfo
(JLabel.class, null,
null, null,
new String[] {null, "text", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "border", "icon", "disabledIcon", "horizontalTextPosition", "verticalTextPosition", "iconTextGap"},
new Class[] { null, String.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, Border.class, Icon.class, Icon.class, int.class, int.class, int.class}
),
new JBComponentInfo
(JProgressBar.class, null,
null, null,
new String[] {null, "value", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "minimum", "maximum", "stringPainted", "indeterminate"},
new Class[] {null, int.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, int.class, int.class, boolean.class, boolean.class}
),
new JBComponentInfo
(JScrollBar.class, null,
AdjustmentListener.class, "adjustmentValueChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "minimum", "maximum", "unitIncrement", "blockIncrement", "visibleAmount"},
new Class[] {int.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, int.class, int.class, int.class, int.class, int.class}
),
new JBComponentInfo
(JSlider.class, null,
ChangeListener.class, "stateChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "extent", "inverted", "labelTable", "majorTickSpacing","maximum","minimum", "minorTickSpacing","paintLabels","paintTicks", "paintTrack", "snapToTicks"},
new Class[] {int.class,boolean.class,boolean.class,String.class, Color.class, Color.class, Dimension.class, int.class,boolean.class,Dictionary.class,int.class, int.class,int.class,int.class, boolean.class,boolean.class,boolean.class,boolean.class}
),
// setForeground and setBackground do nothing with spinners (not sure why), so I
// removed "foreground" and "background" properties from these rows.
new JBComponentInfo
(JSpinnerIntegerAdapter.class, "JSpinner(SpinnerNumberModel)",
ChangeListener.class, "stateChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "preferredSize", "minimum", "maximum", "stepSize"},
new Class[] {int.class, boolean.class, boolean.class,String.class, Dimension.class, int.class, int.class, int.class}
),
new JBComponentInfo
(JSpinnerDoubleAdapter.class, "JSpinner(SpinnerNumberModel)",
ChangeListener.class, "stateChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "preferredSize", "minimum", "maximum", "stepSize"},
new Class[] {double.class, boolean.class, boolean.class, String.class, Dimension.class, double.class, double.class, double.class}
),
new JBComponentInfo
(JSpinnerDateAdapter.class, "JSpinner(SpinnerDateModel)",
ChangeListener.class, "stateChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "preferredSize", "start", "end"},
new Class[] {Date.class, boolean.class, boolean.class, String.class, Dimension.class, Date.class, Date.class}
),
new JBComponentInfo
(JSpinnerStringAdapter.class, "JSpinner(SpinnerListModel)",
ChangeListener.class, "stateChanged",
new String[] {"value", "enabled", "visible", "toolTipText", "preferredSize", "items"},
new Class[] {String.class, boolean.class, boolean.class, String.class, Dimension.class, String[].class}
),
// this row handles JTextField, JTextArea, and other descendents of
// JTextComponent in a least-common denominator fashion:
new JBComponentInfo
(JTextComponent.class, null,
FocusListener.class, "focusLost",
new String[] {"text", "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "disabledTextColor", "selectedTextColor", "selectionColor", "editable", "margin"},
new Class[] {String.class, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, Color.class, Color.class, Color.class, boolean.class, Insets.class}
),
// This row handles JComponentBreadboards themselves, which extend
// JPanel. Connected JComponentBreadboards also support a special
// recursive refresh feature that is implemented directly in the
// jbRefresh method.
new JBComponentInfo
(JPanel.class, null,
null, null,
new String[] {null, "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize", "border"},
new Class[] {null, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class, Border.class}
),
// Technically, all JComponents are supported by virtue of this last, "generic
// JComponent" support row. But a mere JComponent doesn't have any user
// manipulated properties, so connecting it only allows you to control a few
// view-only properties. To make full use of an unsupported JComponent within
// JComponentBreadboard, this limited support will usually not be enough, and
// you will need to define a specific row for the new class, similar to the rows
// above this one. Note that the algorithm always chooses the most specific
// class match on this table, so the generic JComponent row won't match, say, a
// JTextField, since, as a descendent of JTextComponent, the JTextComponent row
// would provide a closer match.
new JBComponentInfo
(JComponent.class, null,
null, null,
new String[] {null, "enabled", "visible", "toolTipText", "foreground", "background", "preferredSize"},
new Class[] {null, boolean.class, boolean.class, String.class, Color.class, Color.class, Dimension.class}
),
};
// Returns an object that provides all the info required for JComponents
// of the type of the argument to be "plugged into" a JComponentBreadboard.
//
// Returns null if the given JComponent isn't "JComponentBreadboard-pluggable".
//
@SuppressWarnings("unchecked")
private static JBComponentInfo componentInfo(Object pluggableObject) {
JBComponentInfo result = null;
for (int i = 0; i < jbComponents.length; i++) {
if (jbComponents[i].adapterClass.isInstance(pluggableObject)) {
if (null == result)
result = jbComponents[i];
else if (result.adapterClass.isAssignableFrom(jbComponents[i].adapterClass))
// a more specific hit (a subclass of previous hit); use it:
result = jbComponents[i];
// else it's a superclass of current "best hit", so ignore it.
}
}
return result;
}
// checks that name and type arrays on component info table have same # of elements
private static void validateComponentInfoTable() {
for (int i=0; i < jbComponents.length; i++) {
if (jbComponents[i].pluggableProperty.length !=
jbComponents[i].pluggablePropertyType.length) {
throw new IllegalStateException(
"jbComponents["+i+"].pluggableProperty.length (=" +
jbComponents[i].pluggableProperty.length + ") != " +
"jbComponents["+i+"].pluggablePropertyType.length (=" +
jbComponents[i].pluggablePropertyType.length + "). " +
"(Row " + i + " of jbComponents contains the " +
jbComponents[i].adapterClass.getSimpleName() +
" class' component information.)");
}
if (jbComponents[i].changeIndicatingListenerInterface==null &&
jbComponents[i].pluggablePropertyType[MAIN_PROP] != null)
throw new IllegalStateException( "jbComponents["+i+"]."+
"changeIndicatingListenerInterface is null but " +
"jbComponents["+i+"]."+
"pluggablePropertyType[MAIN_PROP] isn't. " +
"Either both or neither must be null." +
"(Row " + i + " of jbComponents contains the " +
jbComponents[i].adapterClass.getSimpleName() +
" class' component information.)");
if (jbComponents[i].changeIndicatingListenerInterface!=null &&
jbComponents[i].pluggablePropertyType[MAIN_PROP] == null)
throw new IllegalStateException( "jbComponents["+i+"]."+
"pluggablePropertyType[MAIN_PROP] is null but " +
"jbComponents["+i+"]."+
"changeIndicatingListenerInterface isn't. " +
"Either both or neither must be null." +
"(Row " + i + " of jbComponents contains the " +
jbComponents[i].adapterClass.getSimpleName() +
" class' component information.)");
}
}
// there is only one component-info table, so validate it at class init time:
static {validateComponentInfoTable();}
// Sets the specified "view only property" of the JComponent to the
// given value. View-only properties can represent "implicit" properties within
// a JComponent for which there is not an explicit set method available. For
// example, a String array defining the list of items in a JComboBox'
// drop-down list isn't explicitly settable through the JComboBox
// interface, but an "Items" View-only property allows it to be treated
// as if it were.
private void setJComponentViewonlyProperty(Object pluggableObject,
String propName, Object propValue) {
Object[] pList = {propValue};
Expression setExpr = new Expression(
pluggableObject, "set" + firstToUpper(propName), pList);
try {
setExpr.execute();
} catch (Exception ex) {
throw new IllegalStateException("Execution of '" +
pluggableObject.getClass().getSimpleName() +
".set"+firstToUpper(propName) + "(" + argsToString(pList) + ")' failed unexpectedly.",ex);
}
}
// If the component is a JTextComponent whose caret is an instance of
// DefaultCaret, and caret tracking isn't already disabled, sets its
// caret updating policy so that programmatic changes to the text
// field will not result in unintended scrolling effects.
//
// Returns the original caret policy (the one before any changes were
// made)
//
// This method and it's companion, maybeRestoreCaretTracking(),
// implement a workaround for a known limitation (some would say, bug)
// of JTextComponents that makes them scroll around as if a human
// user were editing them when their contents are changed
// programmatically.
//
// I got the basic idea for this workaround from somewhere in sun's
// bug tracking database (searching it for
// "DefaultCaret" and "setUpdatePolicy" should locate it).
//
// Before this workaround was put in place, JTextComponents seemed to
// change their "caret" positions at random--and this important class of
// JComponents were often rendered almost unusable as a result.
//
// I don't really like this fix, though, so if anyone knows something
// cleaner, please help!
private int maybeDisableCaretTracking(JComponent jc) {
int result = DefaultCaret.UPDATE_WHEN_ON_EDT;
if (jc instanceof JTextComponent) {
JTextComponent jtc = (JTextComponent) jc;
if (jtc.getCaret() instanceof DefaultCaret) {
result = ((DefaultCaret) (jtc.getCaret())).getUpdatePolicy();
if (result != DefaultCaret.NEVER_UPDATE)
((DefaultCaret) (jtc.getCaret())).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
}
return result;
}
// Under conditions analogous to those described in its companion
// method, maybeDisableCaretTracking(), restores a caret update
// policy to one previously stored.
private void maybeRestoreCaretTracking(JComponent jc, int oldCaretUpdatePolicy) {
if (jc instanceof JTextComponent) {
JTextComponent jtc = (JTextComponent) jc;
if (jtc.getCaret() instanceof DefaultCaret) {
int policy = ((DefaultCaret) (jtc.getCaret())).getUpdatePolicy();
if (policy != oldCaretUpdatePolicy)
((DefaultCaret) (jtc.getCaret())).setUpdatePolicy(oldCaretUpdatePolicy);
}
}
}
/**
** Refreshes all the jbConnected JComponents and/or KeyStrokes
** contained on this JComponentBreadboard.
**
**
**
** For each connected JComponent or KeyStroke on this form,
** jbRefresh will call the getter methods associated with that
** component or key stroke's injected interface, and then use the results
** so obtained to set the associated properties of the connected
** JComponent or KeyStroke .
**
**
**
** JComponentBreadboards that have been connected to another
** JComponentBreadboard will automatically invoke jbRefresh
** on the connected-to JComponentBreadboard whenever they are
** refreshed. Circular connections are allowed, each so-connected
** JComponentBreadboard is guaranteed not to be refreshed more than
** once per top-level jbRefresh invocation.
**
**
**
** To avoid all possibility of thread-related errors, it is safest to
** call this method only from the Swing thread (for example, via
** invokeLater , or within a jbConnect -bound setter
** method, which JComponentBreadboard always invokes from within the
** Swing thread). In particular, if you need to request a refresh
** of the parent form from within a jbRun launched process
** (these processes do not run in the Swing thread) use
** jbRunRefresh , not this method.
**
**
Note: JComponentBreadboard automatically invokes
** jbRefresh when the component is first added to a top
** level Frame or Dialog (via the
** addNotify method) and after each time that the user
** changes the state of any connected component. Thus, explicit
** calls to this method are expected to be very rare.
**
** @see #jbRunRefresh jbRunRefresh
** @see #addNotify addNotify
**/
public void jbRefresh() {
HashSet refreshedPreviously = new HashSet(); // nothing refreshed yet
jbRefresh(refreshedPreviously);
}
// Because of how Swing configures event listeners, setting a JComponent's
// property programmaticaly during a refresh can generate events that emulate
// user actions. For example, a call to setMinimum on a SpinnerNumberModel
// will send a stateChanged message to the associated JSpinner that will
// produce a series of unfortunate events emulating what would have happened
// if the end user had directly interacted with the spinner. For example, it
// can cause incorrect values (present, say, during initialization) to be
// read as if they had been user entries. When refreshingConnection is true, such
// bogus "user manipulations" generated as side-effects of refresh are ignored.
private boolean refreshingConnection = false;
// refreshConnection - refreshes the JComponent associated with a connection
// by bringing it's state into synch with the properties it is connected to.
private void refreshConnection(JBConnection conn) {
JBComponentInfo compInfo = componentInfo(conn.pluggableObject);
int iProp = 0;
refreshingConnection = true;
try {
// Note that key properties, placed first on the table, must be refreshed last,
// because setting certain view-only properties can, as a side-effect, change
// the key properties. Similarly, the order of other property placement in the
// table can sometimes make a difference as well, since some settings can be
// constrained by others. For example, the JComboBox "selectedValue" can be changed by
// setting the drop-down list items. Similarly, allowed settings of JScrollBar extents
// depend on the current minumum and maximum settings, and thus must come after
// these view-only properties in the property arrays.
for (iProp = MAIN_PROP+1; iProp < compInfo.pluggableProperty.length; iProp++) {
Object obj = conn.getViewonlyProperty(compInfo.pluggableProperty[iProp]);
if (obj != null && compInfo.pluggableProperty[iProp] != null)
setJComponentViewonlyProperty(conn.pluggableObject,
compInfo.pluggableProperty[iProp], obj);
}
iProp = MAIN_PROP;
// main getter/setter; the user-manipulated or "key" property done last:
Object obj = conn.getManipulatedModelProperty();
if (obj != null && compInfo.pluggableProperty[MAIN_PROP] != null) {
int oldCaretUpdatePolicy = maybeDisableCaretTracking(
conn.manipulatedComponent);
setObjectProperty(conn.pluggableObject,
compInfo.pluggableProperty[MAIN_PROP], obj);
maybeRestoreCaretTracking(conn.manipulatedComponent,
oldCaretUpdatePolicy);
}
}
// (most errors should have been caught earlier, within jbConnect,
// and thus should have already thrown more specific exceptions)
catch (Exception ex) {
throw new IllegalStateException(
"Exception while refreshing a " + conn.pluggableObject.getClass().getName() +
"'s \"" + compInfo.pluggableProperty[iProp] +
"\" JComponentBreadboard-connected property." +
" Exception: " + ex.toString(), ex);
}
finally {
refreshingConnection = false;
}
}
// refreshes this JComponentBreadboard, recursively refreshing any connected JComponentBreadboards
private void jbRefresh(HashSet refreshedPreviously) {
// next 2 lines assure that circular jbConnections (e.g.
// jcBreadboard1 connected to jcBreadboard2, and visa versa) will not be
// a problem. In other words, that all "reachable" JComponentBreadboards get
// refreshed exactly once per public method jbRefresh() call.
if (refreshedPreviously.contains(this)) return;
refreshedPreviously.add(this);
// for each connection between a JComponent and a JComponentBreadboard
// "connection related set of properties":
for (int iConnected = 0; iConnected < jbConnections.size(); iConnected++) {
JBConnection conn = jbConnections.get(iConnected);
refreshConnection(conn);
// usually, next line is not needed, but if a JPanel's
// preferredSize is changed via its bound auxiliary property in a
// tabbed pane (and perhaps in other cases?) updates don't get
// propagated immediately without next line (not sure why):
conn.manipulatedComponent.invalidate();
// connecting a JComponentBreadboard causes a recursive refresh
if (conn.pluggableObject instanceof JComponentBreadboard)
((JComponentBreadboard) (conn.pluggableObject)).jbRefresh(
refreshedPreviously);
}
// Usually, the screen updates just fine without this next line. However,
// changes in the position of a JLabel's text relative to it's icon
// or changes in the size of a JComponentBreadboard's titled border (and
// possibly other special cases as well?) won't be properly reflected in
// how the components get laid out unless we include:
revalidate();
}
/**
** Constructs a new JComponentBreadboard.
**
**
**
** The breadboard array must be set later via the setBreadboard
** method.
**
** @see #setBreadboard setBreadboard
**/
public JComponentBreadboard() {
super();
}
/** Constructs a new JComponentBreadboard from the specified breadboard array.
**
** Equivalent to constructing via the no-arg constructor and then immediately
** calling setBreadboard(breadboard) .
**
** @see #setBreadboard setBreadboard
**
**/
public JComponentBreadboard(Object[][] breadboard) {
super();
setBreadboard(breadboard);
}
// removes the first
tagged section, returning the title-free result
private String messagePart(String s) {
String result = s.replaceFirst(
"<[tT][iI][tT][lL][eE]>.*[tT][iI][tT][lL][eE]>","");
return result;
}
/** The default title of the popup error message dialog if no HTML title
** tag is included in the string returned from a data validation method function.
**
** See the Data Validation Method Signatures section of the
** {@link #jbConnect(JComponent, String) jbConnect} method for more information.
**
** @see #DATA_IS_VALID DATA_IS_VALID
** @see #REVERT_AND_BEEP REVERT_AND_BEEP
** @see #REVERT_QUIETLY REVERT_QUIETLY
** @see #jbConnect(JComponent,String) jbConnect
**/
public final String DEFAULT_INVALID_DATA_MESSAGE_TITLE = "Invalid Input";
// returns the first
tagged section within the given string
private String titlePart(String s) {
String result = DEFAULT_INVALID_DATA_MESSAGE_TITLE;
if (!s.equals(messagePart(s))) {
result = s.replaceFirst("^.*<[tT][iI][tT][lL][eE]>", "");
result = result.replaceFirst("[tT][iI][tT][lL][eE]>.*","");
}
return result;
}
private boolean correctingInvalidInput = false;
// prompts user to correct an invalid string entry, returning the previous (presumed valid)
// entry if the user cancels, or the corrected entry if they press OK.
private String userCorrectedEntry(String invalidDataMessage, String newInput, String prevInput) {
String result = null;
try {
// popping up this form can generate focusLost events that can cause headaches, so we
// use this flag to ignore any setUserManipulatedModelProperties calls so generated
correctingInvalidInput = true;
if (isDisplayable()) {
result = (String) JOptionPane.showInputDialog(
this, messagePart(invalidDataMessage), titlePart(invalidDataMessage),
JOptionPane.ERROR_MESSAGE,
null, null, newInput);
}
// else the parent may have been destroyed (via dispose()). In
// Swing, it is quite possible for a dialog to be destroyed
// before all events arising from it are processed, and we
// treat such situations as an implicit cancel (earlier, when we popped
// up the validation dialog is such cases, the system hung).
}
finally {
correctingInvalidInput = false;
}
return (result == null) ? prevInput : result;
}
// If there is a JComponent (other than the given excluded component)
// connected to this form within the current JComponentDialog's
// parent frame or dialog that has the focus, this method returns its
// connection. Otherwise, it returns null.
private JBConnection getOtherFocusedConnection(JComponent excludedComponent) {
JBConnection result = null;
Window parentWindow = (Window) getParentFrameOrDialog(this);
if (null != parentWindow) {
Component focusOwner = parentWindow.getFocusOwner();
if (excludedComponent != focusOwner && focusOwner instanceof JComponent)
result = connection((JComponent) focusOwner);
}
return result;
}
// returns stringized object, or "" if object is null.
private String nullToEmpty(Object obj) {
String result = "";
if (null != obj) result = obj.toString();
return result;
}
// Returns a keyword indicating if the connection involves a user
// manipulated string (only such strings support validation) that
// was modified, and if that modified string is valid or not.
private final int NOT_A_MODIFIED_STRING = 1;
private final int MODIFIED_STRING_AND_VALID = 2;
private final int MODIFIED_STRING_AND_INVALID = 3;
private int getStringValidationStatus(JBConnection conn) {
int result = NOT_A_MODIFIED_STRING;
if (null != conn) {
JBComponentInfo compInfo = componentInfo(conn.pluggableObject);
if (compInfo.hasManipulatedModelString()) {
String enteredValue = nullToEmpty(getObjectProperty(
conn.pluggableObject, compInfo.pluggableProperty[MAIN_PROP]));
String modelValue = nullToEmpty(conn.getManipulatedModelProperty());
if (enteredValue.equals(modelValue))
result = NOT_A_MODIFIED_STRING;
else {
String invalidDataMessage = conn.getInvalidDataMessage(enteredValue);
if (invalidDataMessage.equals(DATA_IS_VALID))
result = MODIFIED_STRING_AND_VALID;
else
result = MODIFIED_STRING_AND_INVALID;
}
}
}
return result;
}
// Gets the main user manipulated property associated with a
// connected JComponent, enforcing validation rules (possibly via a
// pop-up dialog) as needed.
private Object getValidatedComponentProperty(JBConnection conn) {
JBComponentInfo compInfo = componentInfo(conn.pluggableObject);
Object result = getObjectProperty(conn.pluggableObject,
compInfo.pluggableProperty[MAIN_PROP]);
if (MODIFIED_STRING_AND_INVALID == getStringValidationStatus(conn)) {
result = nullToEmpty(result);
String modelValue = nullToEmpty(conn.getManipulatedModelProperty());
String invalidDataMessage = conn.getInvalidDataMessage(result.toString());
while (!(invalidDataMessage.equals(DATA_IS_VALID) ||
modelValue.equals(result))) {
if (!(invalidDataMessage.equals(REVERT_QUIETLY) ||
invalidDataMessage.equals(REVERT_AND_BEEP))) {
result = userCorrectedEntry(
invalidDataMessage, result.toString(), modelValue);
invalidDataMessage = conn.getInvalidDataMessage(
result.toString());
}
if ((invalidDataMessage.equals(REVERT_QUIETLY) ||
invalidDataMessage.equals(REVERT_AND_BEEP))) {
result = modelValue;
if (invalidDataMessage.equals(REVERT_AND_BEEP))
Toolkit.getDefaultToolkit().beep();
}
}
}
return result;
}
// Modifies the model property connected to the given component so
// that it reflects the consequences of the most recent user
// initiated component state changing activity (checkbox clicks,
// button clicks, finishing up a text entry by tabbing to next field,
// etc.). These settings can have side effects beyond just setting
// the property and indeed, in the case of buttons and menu items
// (whose manipulated property type is void) ONLY have side effects.
/** @deprecated - for internal use only (public for implementation purposes only) */
public void setUserManipulatedModelProperty(JComponent boundComponent) {
// Short circuits the "event cascade" as one programmatically
// initiated component change generates additional
// as-if-by-the-user "fired off" events. Ignore these dangerous
// and confusing end-user impersonaters.
if (refreshingConnection || correctingInvalidInput) return;
JBConnection otherConnection = getOtherFocusedConnection(boundComponent);
JBConnection originalConnection = connection(boundComponent);
int otherStatus = getStringValidationStatus(otherConnection);
if (MODIFIED_STRING_AND_INVALID == otherStatus) {
// This branch can be reached, for example, after pressing a connected
// JMenuItem's accelerator while a connected JTextField whose entry is
// invalid has the focus (user is forced to finalize entry, original
// action bound to the accelerator is implicitly canceled). You might
// (as I did) expect that Swing would send a focusLost to the JTextField
// BEFORE firing the the event associated with the JMenu's accelerator,
// but it doesn't--hence the need for this branch.
Object componentProperty = getValidatedComponentProperty(otherConnection);
otherConnection.setManipulatedModelProperty(componentProperty);
otherConnection.manipulatedComponent.requestFocusInWindow();
}
else {
if (MODIFIED_STRING_AND_VALID == otherStatus) {
// Same scenario as above, except with a valid entry in the
// JTextField will get us into this branch. Just need to make
// sure the incomplete entry gets pushed back into the attached object.
// (no need to cancel--falls through to handle the original event).
Object componentProperty = getValidatedComponentProperty(otherConnection);
otherConnection.setManipulatedModelProperty(componentProperty);
}
// These lines are the normal case: get a (possibly validated)
// form of the value associated with the component, then set the
// connected-to model property
Object componentProperty = getValidatedComponentProperty(originalConnection);
originalConnection.setManipulatedModelProperty(componentProperty);
}
// The setter method may, say, be handling windowClosing and then ".dispose()"
// of the parent dialog (=making it undisplayable). This "if" prevents
// various exceptions that would otherwise occur in such cases.
if (isDisplayable()) jbRefresh();
}
/** @deprecated -- public for implementation reasons only */
public class ProgressMonitoringBreadboard extends JComponentBreadboard {
ProgressMonitoredWorker monitoredWorker;
boolean canBeCanceled;
// Swing timers (javax.swing.Timer), due to our use of a popup
// modal dialog for progress/cancel feedback, cannot update that
// dialog's parent (the EDT event handling ignores events targeted
// to the parent as part of making the modal dialog modal). A
// java.util.Timer allows the parent, as well as the pop-up modal
// progress dialog, to be updated, because the update requests
// are initiated from a completely different thread.
java.util.Timer refreshTimer;
java.util.TimerTask refreshTimerTask;
ProgressMonitoringBreadboard(ProgressMonitoredWorker w, boolean canBeCanceled) {
final int ZERO_PERCENT_VALUE = 0;
final int HUNDRED_PERCENT_VALUE = Integer.MAX_VALUE;
monitoredWorker = w;
this.canBeCanceled = canBeCanceled;
refreshTimer = new java.util.Timer(true);
JProgressBar progressBar = new JProgressBar(ZERO_PERCENT_VALUE,
HUNDRED_PERCENT_VALUE);
progressBar.setStringPainted(true);
JComponent cancel = new JButton("Cancel");
JComponent note = new JLabel();
Dimension d = JComponentBreadboard.this.getRootPane().getSize();
setBreadboard(new Object[][] {
{null, NOSCALE, BISCALE},
{SHRINKS, ySpace(20), xSpace(d.width-20)},
{NOSCALE, null, yAlign(1, note)},
{SHRINKS, ySpace(10), null},
{NOSCALE, null, progressBar},
{SHRINKS, ySpace(10), null},
{NOSCALE, null, xAlign(0.5, cancel)},
{SHRINKS, ySpace(20), null},
});
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
jbConnect(progressBar, "progressBar");
jbConnect(cancel, "cancel");
jbConnect(KeyStroke.getKeyStroke("ESCAPE"),"cancel");
jbConnect(CLOSE_BUTTON, "close");
jbConnect(note, "note");
}
public int getProgressBarValue() {
int result = 0;
double progressFraction = monitoredWorker.getProgressGlobally();
if (!Double.isNaN(progressFraction))
result = (int) (progressFraction * Integer.MAX_VALUE);
return result;
}
// if they never set progress, it will remain indeterminate for entire run
public boolean getProgressBarIndeterminate() {
boolean result = Double.isNaN(monitoredWorker.getProgressGlobally());
return result;
}
public boolean getProgressBarStringPainted() {
return !getProgressBarIndeterminate();
}
public void setCancel() {
monitoredWorker.setProgressCanceled(true);
}
public boolean getCancelEnabled() {
return canBeCanceled;
}
public void setCancelEnabled(boolean cancelEnabled) {
canBeCanceled = cancelEnabled;
}
public void setClose() {
if (monitoredWorker.getState() == Thread.State.TERMINATED)
jbReturn(); // this branch should be very rarely executed, if ever
else if (canBeCanceled)
monitoredWorker.setProgressCanceled(true);
else
Toolkit.getDefaultToolkit().beep(); // cannot cancel, so beep
}
public String getNoteText() {
return monitoredWorker.getProgressNoteGlobally();
}
private boolean shutdownRequested = false;
class RefreshTimerTask extends TimerTask {
public void run() {
if (shutdownRequested) {
stopRefreshTimer();
SwingUtilities.invokeLater(new Runnable() { public void run() {
jbReturn();
}});
}
else if (monitoredWorker.getParentRefreshRequested()) {
monitoredWorker.setParentRefreshRequested(false);
SwingUtilities.invokeLater(new Runnable() { public void run() {
jbRefresh();
// this refreshes the form from which the jbRun was launched:
JComponentBreadboard.this.jbRefresh();
}});
}
else {
SwingUtilities.invokeLater(new Runnable() { public void run() {
jbRefresh();
}});
}
}
}
public void startRefreshTimer(int refreshInterval) {
if (refreshTimerTask != null)
refreshTimerTask.cancel();
refreshTimerTask = new RefreshTimerTask();
refreshTimer.schedule(refreshTimerTask, 0, refreshInterval);
}
public void stopRefreshTimer() {
refreshTimer.cancel();
}
public void requestShutdown() {
shutdownRequested = true;
}
}
// A thread that can accept and retrieve information regarding it's
// progress, and that is viewed by a special companion
// ProgressMonitoringBreadboard, which it refreshes whenever it's
// progress-state-info changes.
//
// Note: because the Swing thread queries this thread to get
// progress, notes, etc. it needs to update the progress dialog and
// the thread itself also sets these properties, methods must be
// synchonized to avoid inconsistencies that can cause exceptions to
// be thrown.
private static class ProgressMonitoredWorker extends Thread {
// JComponentBreadboard that contains progress bar, cancel button, etc.
private ProgressMonitoringBreadboard monitoringBreadboard = null;
private double progressGlobally = jbRunDefaultProgress; // fraction finished so far
private Runnable theJob = null; // the work that is to be done
private boolean progressCanceled = false; // flags cancel requests
// Requests a refresh of parent form from which jbRun was launched. By
// default, this form only gets refreshed after the process finishes.
// Refresh occurs at the next schedualed progress/cancel form refresh.
private boolean parentRefreshRequested = false;
private String progressNoteDelimiter = jbRunDefaultNoteDelimiter;
private ProgressMonitoredWorker(Runnable theJob) {
super();
// worker's low priority helps asure swing thread gets enough
// time to update promptly
setPriority(Thread.MIN_PRIORITY);
setDaemon(true); // just in case it gets loose, won't prevent app quiting
this.theJob = theJob;
}
// sets, retrieves, a flag representing a request to cancel the process
synchronized public void setProgressCanceled(boolean progressCanceled) {
this.progressCanceled = progressCanceled;
}
synchronized public boolean getProgressCanceled() {
return progressCanceled;
}
// sets the JComponentBreadboard that contains the progress bar and such
synchronized public void setMonitoringBreadboard(ProgressMonitoringBreadboard monitoringBreadboard) {
this.monitoringBreadboard = monitoringBreadboard;
}
synchronized public ProgressMonitoringBreadboard getMonitoringBreadboard() {
return monitoringBreadboard;
}
synchronized public void setParentRefreshRequested(boolean parentRefreshRequested) {
this.parentRefreshRequested = parentRefreshRequested;
}
synchronized public boolean getParentRefreshRequested() {
return parentRefreshRequested;
}
// runs the job, then does some cleanup work (closing monitoring dialog, etc.)
public void run() {
try {
monitoringBreadboard.startRefreshTimer(jbRunDefaultRefreshInterval);
beginProgressMonitoredSection(0, 1.0);
theJob.run();
endProgressMonitoredSection();
// It's a programming error not to correctly delimit progress monitored sections
// If job completes normally, without exception, there should not be
// any opened progress monitored sections on the stack
if (sections.size() > 0)
throw new IllegalStateException(
sections.size() + " unbalanced beginProgressMonitoredSection call(s) detected. " +
" Every beginProgressMonitoredSection call must have an associated " +
" endProgressMonitoredSection call.");
}
finally {
monitoringBreadboard.requestShutdown();
}
}
// defines a sub-range, within the progress bar, into which a progress
// monitored section's progress fractions will be mapped.
static private class ProgressContext {
public double minFraction;
public double maxFraction;
public String progressNote = null;
ProgressContext(double minFrac, double maxFrac) {
minFraction = minFrac;
maxFraction = maxFrac;
}
}
// more than this many sub-sections is most likely due to a user error
private final static int MAX_SECTIONS = 100;
// sections can be further sub-divided into sub-sub-sub, etc. sections which
// nested subdivisions this stack keeps track of.
private static Stack sections = new Stack();
synchronized public void beginProgressMonitoredSection(
double startFraction, double endFraction) {
if (startFraction < 0.0 || startFraction > 1.0 ||
endFraction < 0.0 || endFraction > 1.0)
throw new IllegalArgumentException("startFraction, endFraction must be " +
"in the range 0 to 1. But your startFraction, endFraction arguments were " +
startFraction + ", " + endFraction);
else if (startFraction > endFraction)
throw new IllegalArgumentException("startFraction must be less than or " +
"equal to endFraction. But your startFraction, endFraction arguments were " +
startFraction + ", " + endFraction);
else if (sections.size() > MAX_SECTIONS) {
throw new IllegalStateException("Attempted to add one more (nested) monitored section than " +
MAX_SECTIONS + ", which is the maximum number allowed. Your program " +
" likely contains a bug such than beginProgressMonitoredSection is called " +
" repeatedly without required matching calls to endProgressMonitoredSection");
}
else if (sections.empty()) { // no enclosing section--use fractions as-is
sections.push(new ProgressContext(startFraction, endFraction));
}
else {
// fraction range is interpolated between enclosing section's min and max
ProgressContext top = sections.peek();
sections.push(new ProgressContext(
top.minFraction*(1-startFraction) + top.maxFraction*startFraction,
top.minFraction*(1-endFraction) + top.maxFraction*endFraction));
}
}
synchronized public void endProgressMonitoredSection() {
if (sections.empty())
throw new IllegalStateException("An endProgressMonitoredSection call without " +
"the required matching beginProgressMonitoredSection " +
"call was encountered. Begin/end pairs must be balanced, " +
"like Java's curley brackets.");
else
sections.pop();
}
// Sets the fraction of the way we are through the current
// progress monitored section. Global progress fraction is
// determined by interpolating between the current progress
// monitored section's limits.
synchronized public void setProgressLocally(double fraction) {
ProgressContext limits = sections.peek();
if (Double.isNaN(fraction))
progressGlobally = Double.NaN;
else
progressGlobally = limits.minFraction * (1.-fraction) +
limits.maxFraction * fraction;
}
// we, "set locally, get globally" are far as progress fractions are concerned
synchronized public double getProgressGlobally() {
return progressGlobally;
}
synchronized public void setProgressNoteDelimiter(String delimiter) {
progressNoteDelimiter = delimiter;
}
synchronized public void setProgressNote(String note) {
ProgressContext limits = sections.peek();
limits.progressNote = note;
}
synchronized public String getProgressNoteGlobally() {
StringBuffer result = new StringBuffer("");
for (int i = 0; i < sections.size() &&
(0==result.length() || progressNoteDelimiter != null); i++) {
String s = ((ProgressContext) sections.elementAt(i)).progressNote;
if (s != null) {
if (result.length() > 0) result.append(progressNoteDelimiter);
result.append(s);
}
}
String s = result.toString();
if (null != jbRunDefaultNote) s = jbRunDefaultNote + s;
return s;
}
}
// returns current thread if it is a (the) ProgressMonitoredWorker, otherwise null
private static ProgressMonitoredWorker getCurrentProgressMonitoredWorker() {
Thread result = Thread.currentThread();
if (!(result instanceof ProgressMonitoredWorker))
result = null;
return (ProgressMonitoredWorker) result;
}
/**
** When called from within a jbRun -created thread,
** indicates the beginning of a progress monitored section of
** the jbRun launched process. An associated
** endProgressMonitoredSection line indicates the end
** of the progress monitored section.
**
**
** These two methods delimit sections of the jbRun launched process roughly the same
** way that curly brackets do in normal program text. For
** example, nesting is allowed, and any "begins" unmatched by "ends"
** that remain after the jbRun launched thread finishes will
** result in an IllegalStateException being thrown.
**
**
** This method does nothing
** if called from a non-jbRun -created thread.
**
**
The startFraction , endFraction
** parameters of this, and of any enclosing (higher level)
** progress monitored sections, recursively determine the range
** within the progress bar into which settings made with
** setJbRunProgress(fraction) are mapped.
**
**
** Progress monitored sections make it easier for jbRun launched processes
** to provide percent-done type feedback via setJbRunProgress
** when the processes involve multiple, complex, nested, subsections.
** For a simple example of such a process, see the
** RandomWalks example
** application in the JComponentBreadboard User's Guide.
**
**
Typically, when a process is divided into several
** sequential sections, the startFraction, endFraction pairs
** will be adjacent, disjoint ranges that cover the interval from 0
** to 1 (for a two section process you might have (0,0.5) for
** the first section and (0.5, 1.0) for the second). However,
** such intervals are not required and, for special situations
** (for example, when the fraction complete is revised downward
** to reflect new information) may not always be appropriate.
**
** @param startFraction an estimate of the fraction (on a elapsed time basis)
** of the enclosing (parent) progress
** monitored section that has been completed when this progress monitored
** section begins. A number in the range 0 to 1 (and that is
** less than or equal to endFraction ) is required.
**
** @param endFraction an estimate of the fraction (on an elapsed time basis)
** of the enclosing (parent) progress
** monitored section that will be completed when this progress monitored
** section ends. A number in the range 0 to 1 (and that is
** greater than or equal to startFraction ) is required.
**
** @see #endProgressMonitoredSection endProgressMonitoredSection
** @see #jbRun jbRun
**
**/
public static void beginProgressMonitoredSection(double startFraction,
double endFraction) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
w.beginProgressMonitoredSection(startFraction, endFraction);
}
/**
** When called from within a jbRun -launched thread,
** ends a progress monitored section begun via beginProgressMonitoredSection .
**
** When called from some other kind of thread, does nothing.
**
** @see #beginProgressMonitoredSection beginProgressMonitoredSection
**/
public static void endProgressMonitoredSection() {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
w.endProgressMonitoredSection();
}
/**
** When called from code executing within a jbRun -launched
** thread, returns true if the cancel flag has been raised
** (for example, by the user clicking on the cancel button) since
** the run started, or false otherwise.
**
**
** When called from a non-jbRun -launched thread, always returns false .
**
**/
public static boolean getJbRunCanceled() {
boolean result = false;
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
result = w.getProgressCanceled();
return result;
}
/**
** When executed from with a jbRun launched process, sets or unsets that
** thread's "cancelRequested" flag. Otherwise, does nothing.
**
**
** Typically, this flag will be set as a consequence of the user
** clicking on the Cancel button of the progress/cancel feedback
** dialog displayed by the jbRun method. Hence this method is only
** needed in those rare cases where you need to request such a
** cancel operation programmatically, or when you need to "un-cancel" a previous
** cancel request.
**
** @param progressCanceled true to set the progress cancel flag, false
** to reset this flag (to "un-cancel")
**
** @see #getJbRunCanceled getJbRunCanceled
** @see #jbRun jbRun
**
**/
public static void setJbRunCanceled(boolean progressCanceled) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
w.setProgressCanceled(progressCanceled);
}
/**
** If executed from within the jbRun launched process,
** sets the portion of the note (located just above the progress
** bar) associated with the lowest-level, currently executing,
** progress monitored section (the "local" note).
**
**
**
** If executed outside of a jbRun launched process sets the
** initial note that gets prepended to whatever other notes are set
** within any future jbRun launched processes (if there are
** no setJbRunNote calls within the launched process,
** this prefix becomes the fixed note for the entire run)
**
** @param note the text of the note, in plain text or in the Swing subset
** of HTML.
**
** @see #getJbRunNoteGlobally getJbRunNoteGlobally
** @see #jbRun jbRun
**
**/
public static void setJbRunNote(String note) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w)
jbRunDefaultNote = note;
else
w.setProgressNote(note);
}
/**
** If executed from within the jbRun launched process,
** returns the text of the note that would be displayed just above the
** progress bar on jbRun 's progress cancel dialog, if that
** dialog were immediately refreshed.
**
**
** The nested sequence of progress monitored sections forms a
** heirarchy, similar to a heirarchy of folders in a directory
** tree. Each monitored section can potentially have a distinct
** note. This method starts from the top-most or "root" enclosing
** progress monitored section and works its way down this heirarchy
** to the lowest level, currently executing, progress monitored
** section. Non-null messages from each section (specified via
** setJbRunNote ) are concatenated together in this order,
** interspersed with the current setJbRunNoteDelimiter
** defined delimiter (a space by default). The resulting note is
** then prefixed with any non-null note specified (via
** setJbRunNote ) before the jbRun process was
** launched, to form the note returned by this method.
**
**
**
** If executed outside of a jbRun launched process,
** returns the last note specified outside of any jbRun launched
** process via setJbRunNote , or null if no such
** specification has been made.
**
**
** @see #setJbRunNoteDelimiter setJbRunNoteDelimiter
** @see #setJbRunNote setJbRunNote
** @see #jbRun jbRun
**
**/
public static String getJbRunNoteGlobally() {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
return w.getProgressNoteGlobally();
else
return jbRunDefaultNote;
}
/**
**
** When called from within a jbRun launched process, specifies the
** fraction of the lowest level current progress monitored section
** of the process that has been completed so far.
**
**
** When called outside of a jbRun launched process, defines the initial
** default fraction complete for newly launched jbRun processes
** (by default, this is Double.NaN , which places the progress bar
** in indeterminate mode).
**
** @param fraction the fraction completed so far (on a computing
** time basis) of the lowest level currently executing progress
** monitored section. The only allowed value outside of the range
** 0 to 1 is Double.NaN , which means "fraction complete is unknown", and
** places the progress bar in indeterminate mode.
**
** @see #jbRun jbRun
**
**/
public static void setJbRunProgress(double fraction) {
if (!Double.isNaN(fraction)) {
if (fraction < 0.0)
throw new IllegalArgumentException("fraction = " + fraction +
"; fraction argument must be non-negative");
else if (fraction > 1.0)
throw new IllegalArgumentException("fraction = " + fraction +
"; fraction argument cannot exceed 1.0");
}
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w) {
jbRunDefaultProgress = fraction;
}
else {
w.setProgressLocally(fraction);
}
}
/**
** Returns the fraction of the (entire) currently executing
** jbRun -launched process that has been completed so far.
** This fraction corresponds to the fraction of the progress/cancel
** dialog's progress bar that is filled in.
**
** Note that the fraction returned by this method will not in
** general equal the fraction last specified via
** setJbRunProgress since that fraction indicates only the
** "local fraction", that is, the fraction of the immediately
** enclosing progress monitored section that has been
** completed.
**
**
** The fraction returned by this method depends both on
** the last specified "local fraction", and (recursively) on the
** startFraction , endFraction parameters passed
** to all of the beginProgressMonitoredSection methods,
** that with their associated endProgressMonitoredSection methods,
** bracket the currently executing code line.
**
**
** @see #setJbRunProgress setJbRunProgress
** @see #jbRun jbRun
** @see #beginProgressMonitoredSection beginProgressMonitoredSection
**
**/
public static double getJbRunProgressGlobally() {
double result = 0.0;
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
result = w.getProgressGlobally();
return result;
}
/**
** If the executing thread that invokes this method was started by
** jbRun , sets a flag that requests that the parent form
** from which the jbRun was launched be refreshed the
** next time jbRun 's modal progress/cancel dialog gets refreshed.
** Otherwise, does nothing.
**
** @see #jbRun jbRun
** @see #jbRefresh jbRefresh
**
**/
public static void jbRunRefresh() {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null != w)
w.setParentRefreshRequested(true);
}
/** The default interval at which the progress dialog associated with
** a jbRun launched process is updated, in milliseconds.
** This constant equals 1000 milliseconds (which equals 1 second).
**
**
** @see #setJbRunRefreshInterval setJbRunRefreshInterval
**/
public final static int DEFAULT_JBRUN_REFRESH_INTERVAL = 1000; // 1 second
private static int jbRunDefaultRefreshInterval = DEFAULT_JBRUN_REFRESH_INTERVAL;
private static boolean jbRunDefaultCanBeCanceled = false;
private static String jbRunDefaultTitle = "Progress...";
private static String jbRunDefaultNoteDelimiter = " ";
private static String jbRunDefaultNote = null;
private static double jbRunDefaultProgress = Double.NaN;
/**
** If executed from within the jbRun launched process, changes the
** interval at which the progress/cancel dialog is updated, but only for
** that process.
**
** If executed outside of a jbRun launched process, changes the
** initial, default, refresh interval for the progress monitoring
** dialog of any future jbRun launched processes.
**
**
**
Warning: Very long refresh intervals
** can cause user feedback problems, very short refresh intervals
** can cause performance problems. The default of 1000 milliseconds
** (= 1 second) is expected to be a good choice for most
** applications.
**
**
** @see #DEFAULT_JBRUN_REFRESH_INTERVAL DEFAULT_JBRUN_REFRESH_INTERVAL
** @param refreshInterval the time interval, in milliseconds, between successive
** refreshes of the progress monitoring dialog.
**
** @see #jbRun jbRun
**/
public static void setJbRunRefreshInterval(int refreshInterval) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w)
jbRunDefaultRefreshInterval = refreshInterval;
else
w.getMonitoringBreadboard().startRefreshTimer(refreshInterval);
}
/** If executed from within the jbRun launched process, enables or
** disables the Cancel button on the current progress monitoring dialog.
**
** If executed outside of a jbRun launched process, changes the
** initial, default, enabled/disabled state of the progress monitoring
** dialog's Cancel button for any future jbRun launched processes.
**
** If this method is never invoked, the Cancel button is disabled by default.
**
** @param canBeCanceled true to enable the Cancel button on the progress/cancel
** dialog, false to disable it.
**
** @see #jbRun jbRun
**
**/
public static void setJbRunCanBeCanceled(boolean canBeCanceled) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w)
jbRunDefaultCanBeCanceled = canBeCanceled;
else
w.getMonitoringBreadboard().setCancelEnabled(canBeCanceled);
}
private static JDialog jbRunProgressDialog = null;
/** If executed from within the jbRun launched process, sets
** the title on the current progress monitoring dialog.
**
** If executed outside of a jbRun launched process, changes the
** initial, default, title of the progress monitoring
** dialog for any future jbRun launched processes.
**
** @param title the title of the progress/cancel dialog
**
** @see #jbRun jbRun
**
**/
public static void setJbRunTitle(String title) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w)
jbRunDefaultTitle = title;
else if (jbRunProgressDialog != null)
jbRunProgressDialog.setTitle(title);
}
/**
If called within the currently executing progress monitored thread,
specifies the delimiter used between the progress notes associated with each
progress monitored section when these individual notes are concatenated
to produce the note displayed in the progress/cancel dialog of a jbRun -launched
thread.
When invoked from some other thread, sets the default delimiter to be used
the next time jbRun launches such a thread.
Swing-subset HTML may be used (for example, <br> ).
A single space delimiter is used by default.
For complex processes, the nested progress monitored sections containing
the currently executing code line form a hierarchy, with the overall
process at the top and the lowest level currently executing progress
monitored section at the bottom. The formation of the displayed notes
from the notes of each progress monitored section is analogous to how
the names of sub-folders get concatenated (interspersed with
appropriatedly leaning slashes) to define the absolute path of a
specific, lowest-level, folder.
@param delimiter the String to place between the notes of each progress monitored
section when these notes get concatenated together to form the displayed note. Swing-subset
HTML may be used.
@see #jbRun jbRun
*/
public static void setJbRunNoteDelimiter(String delimiter) {
ProgressMonitoredWorker w = getCurrentProgressMonitoredWorker();
if (null == w)
jbRunDefaultNoteDelimiter = delimiter;
else
w.setProgressNoteDelimiter(delimiter);
}
private static boolean withinJbRunRun = false;
/********************************************************
Runs the specified process modally, that is, displaying a modal progress/cancel
dialog until the process ends. The progress dialog has the same basic
layout as that of the JDK's ProgressMonitor class: a progress bar above
which is a text message (describing the progress of the process), and beneath
which is a Cancel button (that can be used to cancel the process).
By default, the progress dialog is configured as follows:
The progress dialog's title contains the string "Progress..."
The note above the progress dialog's progress bar is left blank.
The progress bar is in indeterminate mode (that is, it displays an animation
indicative of ongoing work, but not a numerical percentage completed).
The Cancel button is disabled, and the close box cannot be used to dismiss
the dialog.
These defaults have the distinct advantage of not requiring any changes
to the source code of the executing process. For relatively quick running
processes, the default configuration is hard to improve upon, given it's
convenience. To change the title or introduce a fixed note above the
progress bar (which can still be done without making any changes to the
executing process' code) invoke setJbRunNote or
setJbRunTitle just before invoking jbRun .
However, for longer running processes you often need to give
the user the chance to cancel the process, as well as a numerical
indication of the percentage completed so far, detailed messages, etc. Such detailed
progress feedback will require that the progress-monitored code be
"progress/cancel feedback instrumented" by including appropriate
invocations of the following JComponentBreadboard static methods:
Method What it does
{@link #setJbRunTitle setJbRunTitle}
Changes the title of the progress/cancel dialog
{@link #setJbRunNote setJbRunNote}
Changes the part of the note above the progress/cancel dialog's progress bar that is associated
with the lowest level progress monitored section that the currently executing code line is a part of.
{@link #setJbRunNoteDelimiter setJbRunNoteDelimiter}
Changes the delimiter interspersed between the individual notes from each progress
monitored section when those notes get concatenated to form the
note displayed above the progress bar.
{@link #setJbRunCanBeCanceled setJbRunCanBeCanceled}
Enables/disabled the cancel button on the progress/cancel dialog.
{@link #getJbRunCanceled getJbRunCanceled}
Returns true if the cancel flag has been raised (for example,
by the user clicking on the cancel button) since the jbRun began,
false otherwise. Note that unless the process polls using this method
periodically, user clicks on the cancel button will be ignored.
{@link #setJbRunRefreshInterval setJbRunRefreshInterval}
Defines the interval, in milliseconds, between
refreshes of the progress/cancel dialog (default is 1000 milliseconds,
which equals 1 second)
{@link #setJbRunProgress setJbRunProgress}
Defines the fraction (via an argument between 0 and 1)
of the current "process monitored section" that is completed so far.
{@link #beginProgressMonitoredSection beginProgressMonitoredSection}
Delimits (along with its companion method below) progress monitored sections of the process.
See discussion below.
{@link #endProgressMonitoredSection endProgressMonitoredSection}
Delimits (along with its companion method above) progress monitored sections of the process.
See discussion below.
Specifying Progress Feedback For Complex, Nested, Processes
Except in tutorials, it is very unusual to have a process
that consist of a single loop or similarly straightforward, easily instrumented
computation. Instead, a method X might represent 10% of the total work in some
top-level method A that calls X. But that same X could represent 50% of the total
work when called by
some other top-level method B.
Worse, some future development might incorporate both A and B into some
even higher level method W, and so on, ad-infinituum.
Processes launched by jbRun can easily provide progress/cancel feedback
for such complex, nested, processes, by bracketing each part between
invocations of beginProgressMonitoredSection(startFraction, endFraction) and
endProgressMonitoredSection , as illustrated in the code below:
beginProgressMonitoredSection(0.0, 0.5); // first half
firstHalfOfComputation();
endProgressMonitoredSection();
beginProgressMonitoredSection(0.5, 1.0); // second half
beginProgressMonitoredSection(0.0, 0.5);
firstHalfOfSecondHalfOfComputation();
endProgressMonitoredSection();
beginProgressMonitoredSection(0.5, 1.0);
secondHalfOfSecondHalfOfComputation();
endProgressMonitoredSection();
endProgressMonitoredSection();
beginProgressMonitoredSection 's first argument (startFraction )
represents your best estimate of the fraction of the enclosing (parent) progress
monitored section that has been completed when the new progress monitored
section begins. The second argument (endFraction ) is your best estimate
of the fraction of the enclosing parent progress monitored section that will
have been completed when this sub-section ends (in other words, when the
matching endProgressMonitoredSection method is reached).
Note that there will always be
a system created, top-level, progress monitored section that includes the
entire process and whose associated startFraction is always 0.0 and
whose associated endFraction is always 1.0 (corresponding to
0 to 100% on the progress/cancel dialog's progress bar).
The setJbRunProgress method is aware of the depth and structure
of the progress monitored sections that enclose its invocation, and its settings are
decoded into progress bar positions accordingly. For example, a
setJbRunProgress(0.5) that occurred within
firstHalfOfComputation in the above code would set the progress
bar at the 25% complete mark, but if that same call occured within the
secondHalfOfSecondHalfOfComputation method, it would set the
progress bar at the 87.5% mark (= (75 + 100)/2, that is, halfway through the
fourth quarter). The nesting of such sections can go to virtually any
depth (a limit of 100 is enforced to help catch improper usage). Each
additional level maps the progress fraction settings in that sub-section into
increasingly smaller subdivision of the overall progress bar.
Just as the meaning of the setJbRunProgress method's argument depends on
the progress sections that contain it, the setJbRunNote
method's note is similarly dependent. Specifically, each
setJbRunNote call defines the note only for the lowest
level progress monitored section that the code line that invokes the
method is currently a part of. The displayed message above the progress
bar, however, concatenates the non-null messages from all progress
monitored sections, from the highest to the lowest level, that contain
the currently executing line of code. A delimiter (by default a space,
but specifiable via setJbRunNoteDelimiter ) is interspersed
between the individual messages from each section. Finally, if
setJbRunNote was invoked outside of any jbRun invoked
process, it defines a "global prefix" that gets prepended just before the
note is displayed.
For example, if just before invoking jbRun we had issued a
setJbRunNote("Running") , and then, within the jbRun 's top level
progress monitored section we had issued a
setJbRunNote("Top-Level-Code") and then within a
beginProgressMonitoredSection ...endProgressMonitoredSection
bracketed sub-section we had issued a setJbRunNote("Subsection-1") the
displayed note above the progress bar when executing within subsection 1
(assuming no further sub-sections were introduced and the default single
space delimiter) would be "Running Top-Level-Code Subsection-1" .
For an example that uses jbRun to launch a process
that employs most of the static methods mentioned above to
"progress/cancel instrument" the process, see the RandomWalks example
application.
@param jbJob the process whose code will be executed modally with
progress/cancel feedback. A blocking (modal) progress/cancel dialog
is displayed until the process ends.
********************************************************************************/
public void jbRun(Runnable jbJob) {
if (!SwingUtilities.isEventDispatchThread())
// this requirement assures orderly, one-at-a-time, execution of jbJobs
throw new IllegalStateException("jbRun() must only be invoked from the AWT event dispatching thread (also called the \"Swing thread\").");
else if (withinJbRunRun)
throw new IllegalStateException("Attempt to start a jbRun when another is running. Only one jbRun is allowed at a time.");
else {
withinJbRunRun = true;
try {
ProgressMonitoredWorker w = new ProgressMonitoredWorker(jbJob);
ProgressMonitoringBreadboard bb = new ProgressMonitoringBreadboard(
w, jbRunDefaultCanBeCanceled);
w.setMonitoringBreadboard(bb);
jbRunProgressDialog = createBreadboardDialog(this, bb, jbRunDefaultTitle, true);
// Because initially all contained components are disabled,
// the top level monitoring breadboard won't get the focus,
// and the ESCAPE key will be ignored, without this line:
bb.requestFocusInWindow();
w.start();
jbRunProgressDialog.setVisible(true);
}
finally {
jbRunProgressDialog = null;
withinJbRunRun = false;
}
}
}
/**
** Returns true if a jbRun -launched process is currently
** executing, false
** otherwise.
**
**
** Starting a second jbRun -launched process while one is already running
** will raise an IllegalStateException , so this method can be used to
** check for such a problem and issue an appropriate message to the user
** instead.
**
**
** Note that jbRun always creates and displays a modal
** (blocking) progress/cancel dialog during the run. In most GUIs,
** this will itself be sufficient to prevent the launching of a
** second jbRun process. Thus, explicit checks via this
** method will usually not be needed.
**
**/
public static boolean isJbRunRunning() {
return withinJbRunRun;
}
private Object result = null;
/**
** Sets the result object associated with this JComponentBreadboard.
**
**
** Methods such as showInputBreadboard , that define the
** content pane of a modal dialog using a single
** JComponentBreadboard-defined form, use setResult and
** getResult to specify and retrieve the result to be returned
** by that modal dialog.
**
**
Typically, setResult is not called directly, but
** rather as a side effect of a call such as
** jbReturn(returnValue) . Such a call will invoke
** setResult(returnValue) and then call dispose
** on the Dialog or Frame that contains the JComponentBreadboard.
** (The dispose call closes the modal dialog).
**
**
** The program that invoked the modal JDialog that displayed the
** JComponentBreadboard-based form would then call getResult to
** retrieve the value to be returned to the caller who invoked the modal dialog.
**
** For an example application that uses showInputDialog ,
** and jbReturn in this coordinated manner,
** see NumericInputDialog
** in the User's Guide.
**
**
** @param result the object that will be retrieved by getResult, and that plays
** the role of the returned value from the form in various contexts.
**
** @see #getResult getResult
** @see #jbReturn jbReturn
** @see #showInputBreadboard showInputBreadboard
**
**/
public void setResult(Object result) {
this.result = result;
}
/**
** Retrieves the object reference previously set with setResult .
**
** If no call to setResult has been made, the default returned object
** reference is null .
**
** @see #setResult setResult
**
**/
public Object getResult() {
return result;
}
/**
** Removes (by calling its dispose method) the parent Frame or
** Dialog containing the JComponentBreadboard, if such a Frame or
** Dialog exists, otherwise does nothing.
**
**
**
** This is mainly intended for use by JComponentBreadboards that are
** created via {@link #createBreadboardDialog createBreadboardDialog}, though
** other user created dialogs may find it provides a useful way to close
** down the dialog containing the breadboard, too.
**
** @see #jbReturn(Object) jbReturn(Object)
**
**/
public void jbReturn() {
Component root = getParentFrameOrDialog(this);
if (null != root)
((Window) root).dispose();
}
/**
**
** Closes the parent Dialog or Frame of the form via dispose, and sets the
** result property of this form to the specified Object.
**
** In many
** contexts, this method will play a role analogous to the Java language's
** return statement.
**
** This method is equivalent to: setResult(result); jbReturn();
**
**
**
** @param result the object to be returned when the modal JDialog
** or JFrame that contains this
** JComponentBreadboard-based form closes (via a call
** to dispose ). Though this is the main use
** of the returned result, strictly speaking, this
** param is just an object that various clients (such
** as the modal dialog created via
** showInputBreadboard ) of a form can
** retrieve via getResult in any context in
** which the concept of a returned value from your
** form may be useful.
**
** @see #jbReturn jbReturn
** @see #setResult setResult
** @see #showInputBreadboard showInputBreadboard
**
**/
public void jbReturn(Object result) {
setResult(result);
jbReturn();
}
/** @deprecated - for internal use only, public for technical reasons */
public void setDefaultCloseButtonAction() {
jbReturn();
}
/**
Whenever they receive the windowClosing event,
JDialogs created via {@link #createBreadboardDialog
createBreadboardDialog} are configured to call the
doClick method of the CLOSE_BUTTON of the single top-level
JComponentBreadboard that they contain. To handle
windowClosing in such a JDialog , simply
jbConnect the top level JComponentBreadboard's
CLOSE_BUTTON and then implement the injected setter
method of the JButton interface to handle this "close
button click" just as you would for any other JButton .
For example, the NumericInputDialog
example in the User's Guide employs a single code line
(jbConnect(CLOSE_BUTTON, "ok"); ) to ensure that
closing the dialog is equivalent to clicking the "OK" button.
If you do not connect the top-level JComponentBreadboard's
CLOSE_BUTTON in this manner, that CLOSE_BUTTON will be automatically
connected to a default event handler (containing the single line
"{@link #jbReturn() jbReturn();}") the first time a
windowClosing event is seen by the
createBreadboardDialog -created JDialog .
**/
public final JButton CLOSE_BUTTON = new JButton();
// A special JDialog that holds a single JComponentBreadboard, and calls
// jbReturn() during windowClosing events.
private static class JCBDialog extends JDialog {
private JComponentBreadboard jcb;
private void setup() {
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
// users use jbConnect to link actions to the CLOSE_BUTTON just like
// any other button. If they have not done so, default is to close form
public void windowClosing(WindowEvent e) {
if (null == jcb.connection(jcb.CLOSE_BUTTON)) {
// we cannot just call jbReturn() or dispose() directly --
// we need to do the shutdown within the handler of a
// connected component--because such handlers have the
// needed logic to assures that text entry fields get
// properly finalized and validated before we close.
jcb.jbConnect(jcb.CLOSE_BUTTON, "defaultCloseButtonAction");
}
jcb.CLOSE_BUTTON.doClick();
}
});
add(jcb);
pack();
}
JCBDialog(Dialog parent, JComponentBreadboard jcb,
String title, boolean isModal) {
super(parent, title, isModal);
this.jcb = jcb;
setup();
}
JCBDialog(Frame parent, JComponentBreadboard jcb,
String title, boolean isModal) {
super(parent, title, isModal);
this.jcb = jcb;
setup();
}
}
// returns the component's closest Frame or Dialog ancestor, or null if none
private static Container getParentFrameOrDialog(Component comp) {
Container result = (null==comp) ? null : comp.getParent();
while (null != result &&
!(result instanceof Frame) &&
!(result instanceof Dialog))
result = result.getParent();
return result;
}
/**
** Creates a JDialog whose entire content pane contains
** only the specified single JComponentBreadboard. The
** JDialog also provides a convenient
** JComponentBreadboard-specific windowClosing event
** handling mechanism, using the click event handler of the
** JComponentBreadboard's pre-defined CLOSE_BUTTON field.
**
**
Both the showMessageBreadboard and
** showInputBreadboard methods create their JDialogs using
** this method.
**
**
** The conventions honored by these special JDialogs are listed below:
**
**
**
** The dialog has a single child component--the JComponentBreadboard
** passed in as an argument to this method. Essentially these are
** "JComponentBreadboard-based" JDialogs.
**
**
**
Whenever jbConnect has been invoked to connect
** the special built-in CLOSE_BUTTON object of this
** single child JComponentBreadboard (for example, via a line
** such as jbConnect(CLOSE_BUTTON, "close") within
** the JComponentBreadboard's constructor) the
** windowClosing event handler will call
** CLOSE_BUTTON .doClick (and do nothing
** else). The system will automatically create such a connection
** that simply closes (disposes of) the window, if you have not
** connected your own. Note that, except for simple dialogs,
** simply closing the dialog will rarely be an appropriate closing action.
**
**
**
**
An initial pack is performed on the JDialog
** before it is returned, which assures its content pane will have
** an initial size equal to the preferred size of the top-level
** JComponentBreadboard it contains.
**
**
**
**
**
**
** @see #showMessageBreadboard showMessageBreadboard
** @see #showInputBreadboard showInputBreadboard
** @see #CLOSE_BUTTON CLOSE_BUTTON
**
** @param parent the parent Dialog or Frame of the created dialog. Using null
** will use a default, system-wide parent.
** @param jcb the JComponentBreadboard (a JPanel ) that will be the sole top-level JComponent
** contained by the created JDialog . All the components and behaviours of
** the form are contained within this top-level JComponentBreadboard
** @param title the title of the created JDialog
**
** @param isModal specify true to create a modal dialog,
** false for a modeless dialog
**
**/
public static JDialog createBreadboardDialog(Container parent,
JComponentBreadboard jcb,
String title, boolean isModal) {
JCBDialog result = null;
Container useableParent = getParentFrameOrDialog(parent);
if (null == useableParent || useableParent instanceof Frame)
// disambiguates null (a null first arg matches two constructors)
result = new JCBDialog((Frame) useableParent, jcb, title, isModal);
else if (useableParent instanceof Dialog)
result = new JCBDialog((Dialog) useableParent, jcb, title, isModal);
else // this branch should be impossible to reach
throw new IllegalStateException(
"The getParentFrameOrDialog(parent) must return either null, a Frame, " +
"or a Dialog (instead, it returned an instance of \"" +
useableParent.getClass().getSimpleName() +
"\", which isn't a Frame or Dialog).");
// center in parent (or in screen if no appropriate parent available)
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
int x0 = 0;
int y0 = 0;
if (null != useableParent) {
x0 = useableParent.getX();
y0 = useableParent.getY();
d = useableParent.getSize();
}
// center popup on parent (unless that makes location negative)
int x = Math.max(0, x0 + (d.width - result.getWidth())/2);
int y = Math.max(0, y0 + (d.height - result.getHeight())/2);
result.setLocation(x, y);
return result;
}
/**
*
* After calling the super class' addNotify method, calls
* this class' {@link #jbRefresh jbRefresh} method.
*
*
Because Swing calls this addNotify method when the
* component is first added to a Dialog, Frame, etc., this initial
* refresh assures that all connected components contained in this
* form properly reflect the data they are bound to (via
* jbConnect ) when they first become visible/accessible to
* the user.
*
*
*
*
Note : Because the
* isDefaultButton property of the parent Dialog or Frame
* can be indirectly defined by the getXxxisDefaultButton
* auxiliary property of the injected interface of a JButton, we need to
* assure that the initial refresh is done only AFTER the form is added to the
* Dialog or Frame . Placing it into the addNotify method
* assures this.
*
*/
public void addNotify() {
super.addNotify();
jbRefresh();
}
/**
** This method makes it easy to define JOptionPane-like modal dialogs that
** return a user selectable/configurable object, where the user interface
** for selecting/configuring the returned object is defined by a single
** JComponentBreadboard form.
**
** Analogous to JOptionPane.showInputDialog ,
** except that it is not limited to just accepting String inputs.
**
** The jbReturn method (whose argument is the returned object)
** will usually be
** invoked in, say, a JButtton event handler, to close the modal dialog
** and return a result. The pre-defined CLOSE_BUTTON JButton will typically be
** connected to handle the windowClosing event.
**
**
**
The JComponentBreadboard User's Guide contains examples that
** use showInputBreadboard along with these techniques to
** modally prompt the user for a validated numeric entry (the NumericInputDialog
** application) and a calendar-selected Date entry (the DateChooser
** application).
**
**
** @param parent parent Frame, Dialog, or null to use Swing's system-wide default
** parent. Container objects not Frames or Dialogs will raise
** an IllegalArgumentException .
**
** @param jcb the JComponentBreadboard the user will interact with,
** that defines the content pane of the dialog, and
** that lets the user specify/modify the value the dialog will
** return.
**
** @param title the title of the dialog
**
** @return the Object returned by
** jcb .getResult (). This is the object defined
** by the last jcb.setResult call (note that
** jbReturn(returnValue) , automatically calls
** setResult(returnValue) before closing the dialog).
**
** @see #jbReturn(Object) jbReturn
** @see #setResult setResult
** @see #createBreadboardDialog createBreadboardDialog
**
**/
public static Object showInputBreadboard(Container parent,
JComponentBreadboard jcb,
String title) {
boolean isModal = true;
JDialog jcbd = createBreadboardDialog(parent, jcb, title, isModal);
// Until user clicks close or some other button that invokes jbReturn(),
// they will remain locked in the modal dialog, blocking on this next code line.
jcbd.setVisible(true);
return jcb.getResult();
}
/**
** Convenience method that is equivalent to
** {@link #showInputBreadboard showInputBreadboard(parent, jcb, title)}
** except that it discards the returned result.
**
**
**
** Analogous to JOptionPane.showMessageDialog
**
**
**
** @param parent parent Frame, Dialog, or null to use Swing's system-wide default
** parent. Container objects not Frames or Dialogs will raise
** an IllegalArgumentException .
** @param jcb the JComponentBreadboard the user will interact and that
** defines the content pane of the dialog.
** @param title the title of the dialog
**
**
** @see #showInputBreadboard showInputBreadboard
**
**/
public static void showMessageBreadboard(Container parent,
JComponentBreadboard jcb,
String title) {
JDialog jcbd = createBreadboardDialog(parent, jcb, title, true);
// Until user clicks close or some other button that invokes jbReturn(),
// they will remain locked in the modal dialog, blocking on this next code line.
jcbd.setVisible(true);
}
}