|
Download JComponentBreadboard from its Sourceforge Project Page |
||||||
PREV PACKAGE NEXT PACKAGE | FRAMES NO FRAMES |
See:
Description
Class Summary | |
---|---|
JComponentBreadboard | Represents all aspects of a Java/Swing form--from layout management to how the form is connected to the objects it views and controls. |
JComponentBreadboard uses an electronic breadboard metaphor to integrate Swing's disparate form-related elements into a single coherent class representing the form as a whole:
As with an electronic breadboard, JComponentBreadboard makes it easy to configure a relatively small set of standard JComponents into a wide variety of Swing forms. The User's Guide below illustrates this productivity, beginning with simple instructive examples, and ending with realistically complex JComponentBreadboard-based forms.
If a detailed technical reference, rather than a series of examples, is what you are
looking for, read the javadocs associated with JComponentBreadboard's
setBreadboard
,
jbConnect
,
jbRun
, and
createBreadboardDialog
methods.
The JComponentBreadboard User's Guide
Look here upon this picture, and on this |
-- Bertram Wilberforce Wooster |
JComponentBreadboard makes it an order of magnitude easier to construct Java/Swing form-based desktop GUI applications compared to using classic JDK idioms.
Though I can't prove the above statement, I can do the next best thing: take a simple, well-known, form implemented using classic JDK idioms, re-implement it using JComponentBreadboard, show you both implementations, and let you draw your own conclusions.
The classic JDK application I've chosen (quoted literally below for you convenience) is a short example from the javadocs for the JDK's GridBagLayout class. I won't attempt to explain either the original JDK, or the alternative JComponentBreadboard implementation. Instead, this section is intended to be used as a thought experiment you can perform yourself by attempting to answer the following two questions:
Ready to try this experiment? Below are two screen shots that could have been produced by either implementation. The first screen shot is taken just after startup, the second after a resizing via the sizing border. The Java source code for the two alternative implementations is then shown.
|
|
JComponentBreadboard code to generate these two screen shots
// GridBagJDKExample.java import javax.swing.*; import net.sourceforge.jcomponentbreadboard.JComponentBreadboard; public class GridBagJDKExample extends JComponentBreadboard { private JComponent button(int i) { return xyFill(new JButton("Button " + i)); } public GridBagJDKExample() { setBreadboard(new Object[][] { {null, BISCALE, __, __, __ }, {NOSCALE, button(1), button(2), button(3), button(4)}, {"", button(5), __, __, __ }, {"", button(6), __, __, button(7)}, {"", button(8), button(9), __, __ }, {BISCALE, "", button(10), __, __ }, } ); } public static void main(String[] args) { showMessageBreadboard(null, new GridBagJDKExample(), "GridBagJDKExample"); } }Classic JDK code that generates the exact same two screen shots
Cosmetic changes to the JDK code were made to guarantee identical screen shots. For the original code, see example in the block comment at the top of the GridBagLayout javadocs
// Original GridBagLayout javadocs example. // Note: I changed the Buttons to JButtons, and the Frame's title, so // that the screen shots would look exactly the same as for the // JComponentBreadboard based implementation. Also, to produce // identical screen shots as the JComponentBreaboard implementation, you // must start this code as a Java application, not as an Applet. import java.awt.*; import java.applet.Applet; import javax.swing.JButton; public class GridBagJDKOriginal extends Applet { protected void makebutton(String name, GridBagLayout gridbag, GridBagConstraints c) { JButton button = new JButton(name); gridbag.setConstraints(button, c); add(button); } public void init() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); setFont(new Font("SansSerif", Font.PLAIN, 14)); setLayout(gridbag); c.fill = GridBagConstraints.BOTH; c.weightx = 1.0; makebutton("Button1", gridbag, c); makebutton("Button2", gridbag, c); makebutton("Button3", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button4", gridbag, c); c.weightx = 0.0; //reset to the default makebutton("Button5", gridbag, c); //another row c.gridwidth = GridBagConstraints.RELATIVE; //next-to-last in row makebutton("Button6", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button7", gridbag, c); c.gridwidth = 1; //reset to the default c.gridheight = 2; c.weighty = 1.0; makebutton("Button8", gridbag, c); c.weighty = 0.0; //reset to the default c.gridwidth = GridBagConstraints.REMAINDER; //end row c.gridheight = 1; //reset to the default makebutton("Button9", gridbag, c); makebutton("Button10", gridbag, c); setSize(300, 100); } public static void main(String args[]) { Frame f = new Frame("GridBagJDKExample"); GridBagJDKOriginal ex1 = new GridBagJDKOriginal(); ex1.init(); f.add("Center", ex1); f.pack(); f.setSize(f.getPreferredSize()); f.show(); } }
If your experience is like mine, it's an order of magnitude easier to understand the JComponentBreadboard-based code. At least we can all agree that there are an order of magnitude fewer semi-colons!
This example illustrates some of JComponentBreadboard's layout management advantages. But JComponentBreadboard provides a complete Swing form solution, so many, similarly impressive, code to code comparisons highlighting other classic aspects of Swing form development--from listener classes to SwingWorker--could have been written. Intrigued? Read on!
JComponentBreadboard comes as jcbreadboard.jar, so you can use the standard "add the package's JAR file to your classpath and then import it" approach. That's the approach used in every example below, so you will need to add jcbreadboard.jar to your classpath to run these applications. Except for this JAR file, each example application is implemented in a single self-contained .java file that uses the "default package", so it's straightforward "Java 101" to compile and run them in the Java Integrated Development Environment (IDE) of your choice. Or, from the command line, with the JAR file and sample source file in the current folder, the following approach should work with HelloWorld or any of the example applications discussed below:javac -classpath .;jcbreadboard.jar HelloWorld.java java -classpath .;jcbreadboard.jar HelloWorldHowever, JComponentBreadboard's source code is contained in just one file, JComponentBreadboard.java. For a simple, single package, desktop application, you can just add this file to your project's folder, and then change the package declaration at the top to match the package you are developing. This will eliminate the hassles of a more-than-one-JAR project. It will also let you trace right into our code (and tweak it, if needed) as if it were your own, and, in general, will keep your simple single package application simple. Note that with JComponentBreadboard's no-hassle BSD license it's always legal to modify the code, even for closed-source projects.
JComponentBreadboard requires JDK 1.5 (also known as 5.0) or higher.
We begin with the simplest imaginable (and least imaginative) JComponentBreadboard form-based application:
The first line of main defines a 2-D array of Objects, the so-called
breadboard array, that defines the grid positions of the
JComponents on the screen. In this example, there is only one such
JComponent, so it is a 1 row, 1 column array.
Note that the breadboard array only defines the grid-cell layout. The actual row
heights and column widths of the grid are determined by the
preferredSize of whatever JComponents it contains. In this
example, the single grid cell has expanded to accommodate the huge font size
of its HTML-H1-header-tagged JLabel. Swing defines a
default preferred size that changes dynamically to reflect content, font size,
etc. and this default is usually exactly what you want.
The second line of main uses the static method showMessageBreadboard, which
works much like JOptionPane.showMessageDialog, displaying a
JComponentBreadboard in a modal dialog with a parent defined by the first
parameter (a null argument, as with showMessageDialog, gets you a
default, system-wide parent).
In fact, because JComponentBreadboard extends
JPanel, its instances can be used in any Swing context that
accepts a JPanel. For example, each tab of a JTabbedPane could
contain a single JComponentBreadboard (the Page Setup form in
our FirefoxPrintPreview example
application uses this technique). As a simpler
example, we could have written HelloWorld using
JOptionPane.showMessage as:
This produces the very similar form:
This simple technique of using a JComponentBreadboard as a JOptionPane
dialog's message argument is often useful. However, as we will see
below, JComponentBreadboard's own showInputBreadboard method can be
even more useful, in effect allowing you to quickly create an infinite variety of
customized JOptionPane-like modal dialogs.
If you just need to display a message on the screen, you already have
JOptionPane.showMessageDialog. Our HelloWorldPlus example takes us a
step further by allowing the user switch to an alternative message via
a checkbox:
The first thing you will notice that is different from HelloWorld is
that, rather than simply instantiating a JComponentBreadboard, our
HelloWorldPlus class extends JComponentBreadboard. Though
simply instantiating a JComponentBreadboard is often useful (and often
provides an attractive alternative to the JDK's GridBagLayout) you need
to extend from JComponentBreadboard to create a full-functional form
(that not only defines the form's screen layout, but also allows the user to control
the application's state via the form).
The useExpectedPhrase getter/setter defined property is bound to
the "checked" state of the checkbox via the line:
This line assures that the checked/unchecked state of the checkbox remains in
synch with the useExpectedPhrase property defined by the
setUseExpectedPhrase and getUseExpectedPhrase methods.
In detail, because this line has been executed:
Because our checkbox is the only component that the user can directly interact
with, the second consequence of connecting listed above doesn't apply here. However,
in the CelsiusFahrenheitConverter
example application discussed later, we'll see how this "auto-synching" feature
makes it easy to create a form that allows the user to simultaneously view and
control a value representing a temperature using both the Fahrenheit and Celsius
temperature scales.
Usually, every JComponentBreadboard-connectable JComponent has a single
main property, which is always a relatively simple type (namely, int,
boolean, double, String, or Date). In the JComponentBreadboard view of a form,
most JComponents exist principally to display, and to provide an
interface that allows users to directly manipulate, this special main property.
For example, the main property of a JCheckBox is of type boolean (in
our example, defined by getUseExpectedPhrase and
setUseExpectedPhrase). Similarly, the main property of a
JTextField is a String.
Note, however, that in this example, the JLabel does not have a
main property, or its associated setter/getter methods. This is because, unlike
a JCheckBox, a JLabel does not allow the user to
directly change its state. For the same reason,
JPanels and JSeparators do not have main properties either.
However, regardless of their support for a main property, every
connected JComponent supports one or more auxiliary properties.
You can optionally implement a special getter method associated with each of
these properties to dynamically define it. Each of these auxiliary property
getter methods has a signature of the form:
Here RootName is a first-letter-capitalized version of jbConnect's
second, String, argument. The property type and name are usually the same
as that of an associated property in the connected JComponent.
For example, we connected our JLabel to the form via the code line:
Because of this line, we are free to implement a number of auxiliary
property getters to dynamically define various properties of this
JLabel. The getDisplayedPhraseText getter that we implemented
defines the text displayed by the JLabel. As with the main
property of the checkbox, all of the auxiliary properties are updated
whenever the user interacts with any connected JComponent on the form in
a way that changes it's state (by entering a new String into a JTextField,
selecting a new item from a drop-down list, etc.).
Thanks to the auto-synching aspect of jbConnect discussed above,
this jbConnect, along with the optional getDisplayedPhraseText
method we implemented, assures that the displayed message is always consistent with the
checkbox state. Specifically,
whenever the user checks or unchecks the checkbox,
JComponentBreadboard will call the JLabel's setText method,
passing in the String returned by the getDisplayedPhraseText method
as its argument.
Although implementing the getter associated with an auxiliary property is always
optional, for those JComponents that have a main property, once they are
connected via jbConnect, the associated getter/setter interface
must be implemented. What happens if you jbConnect to a
JCheckBox, but fail to implement the required boolean getter/setter
methods?
To find out, I commented out the setUseExpectedPhrase method, and then
ran the HelloWorldPlus application again.
JComponentBreadboard produced the
IllegalStateException message shown below:
Note that the methods you must implement, and those you may
implement, are shown as two block comments. Simply copy and paste these comments
into your JComponentBreadboard class' code, and then uncomment and
implement only the methods you need (or want).
I call this feature exceptional code generation, because the generated
code is output as part of an Exception message. Though much simpler
than the sophisticated refactoring built into Java Integrated Development
Environments these days, exceptional code generation does have one
important advantage: it works exactly the same way regardless of what IDE you
are using, and it even works with Java's command line development tools.
The end result feels very much like implementing one or more real Java
interfaces. However explicit implements clauses for each
JComponent on the form are not required, which makes the syntax a lot more
managable. The lack of type-safety associated with not having to define and use
real Java interfaces is mitigated by JComponentBreadboard's extensive
run-time checks and detailed exception messages. For these reasons, I call these
various method signatures the injected interface of a jbConnected
JComponent.
Usually, each connectable JComponent class has a single injected
interface. For JSpinners, however, which can variously represent ints,
doubles, Strings or Dates depending on what kind of SpinnerModel they
use, a separate injected interface for each major configuration mode will be
generated. For example, connecting a JSpinner that was instantiated
with a SpinnerDateModel will generate code that requires Date
setter/getter methods whereas a JSpinner instantiated using a
SpinnerNumberModel would require either int or double getter/setters
(depending upon if the SpinnerNumberModel was instantiated via its
integer or double argument constructor).
Although
the REQUIRED methods (associated with the component's main property) are often all you will need, the
additional, OPTIONAL, methods can also be implemented to programmatically
control other JComponent properties. We've already used one of the
optional methods to control the message displayed by our example's
JLabel. Our HelloWorldPlus example also uses
another one of these OPTIONAL methods, as shown below, to
display the user selected message
as the tool tip text of the checkbox:
Note that the last part of one of these optional injected method names is usually
the name of a property associated with a setter of the connected
JComponent. In the above example, the optional injected method name
getUseExpectedPhraseToolTipText ends with ToolTipText and thus
is associated with the setter method setToolTipText of the connected
checkbox. The String returned by the getUseExpectedPhraseToolTipText
method will be
passed in as the argument to setToolTipText each time the user
changes the state of the checkbox.
Once you get the hang of the simple naming conventions these examples
illustrate, you can probably guess the right method signatures most of the time.
Until then, simply exploit the exceptional code generation feature, and
then cut and paste the injected interfaces associated with these connections
into the JComponentBreadboard-derived class that implements your form.
JComponentBreadboard's showMessageBreadboard, which was used in each of
the preceeding examples to actually display the form, can be viewed as providing
a generic JOptionPane.showMessageDialog. In this section, we'll use JComponentBreadboard's
showInputBreadboard (which can be viewed as a generalization of
JOptionPane.showInputDialog) to construct a canned modal dialog that accepts
a number, rather than a String, from the user. By applying the techniques of
this example, you can use JComponentBreadboard to create virtually any
JOptionPane-like canned modal dialog you may require.
A screen shot from this application, as well as its complete source code, is shown
below:
As in the HelloWorldPlus example, numberField, a JTextField
into which the user enters the number, is connected to a property defined by
the form's getNumber and setNumber methods via the line:
However, in our HelloWorldPlus class, the main property of the connected
JCheckBox, a boolean, had exactly the same type as the
useExpectedPhrase property it was connected to. By contrast, in this
example, the main property of the connected JTextField is a
String, but the underlying property in the application it's connected
to is a double (namely, number), so the methods are no longer
very simple setter/getters, but rather must encode/decode between the
JTextField's String, and the associated double
in the application:
This code is complicated by the fact that it allows the dialog to
show and accept a "not-a-number" value (which Java would normally display more
precisely but also more confusingly as "NaN") as an empty string.
However, besides "", there are many other strings that are not valid numbers, so
we need a way to validate the user's entry to assure that it is either "", or a
valid numeric string. Whenever you connect a JComponent that has a user
manipulable String to a property called "xxx", the injected interface
for that component will include an optional method of the form:
When implemented, this routine
should return JComponentBreadboard.DATA_IS_VALID when the
String argument is valid or else an appropriate String (any
String valid within a JLabel's text, which includes the "Swing subset"
of HTML, can be used) that describes the error and how to correct it.
In our example, since we connected our JTextField to a property
called "number", the invalid data message method is called
getNumberInvalidDataMessage and is shown below:
This routine returns the special DATA_IS_VALID keyword if the String equals our
special not-a-number String or if it matches a regular expression for a
valid double, or it is one of the special keyword strings used by class
Double to indicate plus or minus infinity. Otherwise, it returns an
appropriate error message describing the problem.
Whenever JComponentBreadboard recognizes that the user has entered a String for
which the above method does not return DATA_IS_VALID,
it displays a simple error dialog that displays the message, and that gives the
user a chance to correct the problem. The dialog that our example displays after
the user enters the string "1..3" (incorrect due to the extra decimal point)
is shown below:
If the user clicks Cancel, the original numeric entry field reverts to the last
validated value (or, if the user has not entered anything valid yet, to the initial
value). Note that JComponentBreadboard only validates user entries so entries
made programmatically are never validated. Thus, it is important to ensure
that your applications follow their own validation rules--especially during
initialization.
If the user corrects their entry (in the above example, by deleting the extra
decimal point) and then clicks OK, the new entry is re-validated, and, if it is
then valid, that value is accepted. If it is still invalid, the error dialog is
displayed again.
Although a popup is nice to provide detailed error feedback, sometimes you would
prefer to simply reject the user's entry, possibly with an audible beep, and
then restore the last valid value. To do this, instead of a specific error
message text, just return one of the special keywords
JComponentBreadboard.REVERT_QUIETLY or
JComponentBreadboard.REVERT_AND_BEEP.
In this example, we used a regular expression matching a double, and
the match method of the String class, for validation. In
general, regular expressions are quite helpful in building such validators.
Another approach would be to simply attempt to use parseDouble, and
then to catch any NumberFormatException that was raised.
Note that the main property of a JComboBox is also a user manipulable
String, and thus this invalid data message based validation also works
with combo boxes as well as with text fields. For example, you might use an
editable JComboBox to implement a field that allowed arbitrary,
validated, integer entries, but that also provided a dropdown list of
the most commonly used values.
The cancel action is connected to the form via the lines:
Because these lines have been executed, whenever the user either clicks on the
Cancel button or presses the ESCAPE key the following method will be invoked:
When the jbReturn method is invoked, the modal dialog created by
showInputDialog is closed, and then the showInputDialog method
returns the value passed into jbReturn as its result. Thus,
jbReturn provides, in effect, a return statement for
JComponentBreadboard-based modal dialogs.
Note how we can use jbConnect to directly connect to a JDK
KeyStroke. In this case, we connected the cancel button's handler
to the escape key in order
to provide the same kind of escape-key dialog cancellation that
JOptionPane.showInputDialog has.
In general, since the JDK's KeyStroke class provides simple ways of
specifying virtually any complex keyboard combination (even "shift ctrl alt
released HOME" should you ever need that) this means that you no longer have to
limit yourself to the Alt-prefixed keyboard shortcuts supported by the
JDK's setMnemonic method in exchange for one-line-of-code-convenience.
For example, to get your form to pop up its help facility whenever the user
pressed F1 you would only have to write something like:
Note that, for complex forms, adding a real Help menu and defining an appropriate
accelerator key might be a better choice. But that's too much work just to make
F1 do the same thing as clicking your form's help button.
The OK button is connected to the form via the lines:
Because these lines have been executed, the following method will be invoked
after the user either clicks on the OK button, or closes the dialog:
Note that whereas Cancel returned the initial value unchanged, OK returns the
current value of the number field, reflecting the latest user edits.
JComponentBreadboard automatically finalizes and assures the validity of
the numeric input field (using the getNumberInvalidDataMessage method
discussed earlier) which is why our OK button handler can be as simple as one
line and still be correct.
Though it works similarly, CLOSE_BUTTON isn't actually the button that
implements the dialog's close box. Instead, it is a constant JButton
reference whose doClick method gets invoked whenever the special
JComponentBreadboard-aware JDialog that showInputBreadboard
creates recieves the windowClosing event.
Though you can still connect to CLOSE_BUTTON if your
JComponentBreadboard is placed in a differently created Dialog or
Frame, you will
have to invoke CLOSE_BUTTON.doClick() in a windowClosing event
handler of your own creation if you want it to have the same functionality. Note
that there is a subtle but important advantage of closing indirectly through the
CLOSE_BUTTON.doClick: because the click action passes through
JComponentBreadboard's "managed" event stream, JComponentBreadboard
automatically assures
that any of your incomplete text entry fields get finalized and validated before
closing.
An optional getter method in the injected interface of all connected
JButtons, as implemented for
our OK button, is shown below:
When such a method is implemented, and it returns true, JComponentBreadboard
makes that button the "default button" of the button's parent Dialog or Frame
(specifically, it calls the setDefaultButton method of the parent, with
the button as the argument). Though this constraint is not enforced by
JComponentBreadboard, to avoid getting an apparently random default button, it's
best if your code is implemented so that, at any given time, only one of the
connected buttons on your form has a getXxxIsDefaultButton method that
returns true.
Without this method, if the user pressed return after entering
their number, nothing would happen. They would either have to tab to the OK button
and press the space bar, to click the OK button, etc. But, with OK defined as the
default button, they can simply press return after entering their number and
the OK is implicitly clicked--just like it is in
JOptionPane.showInputDialog.
One aspect of Swing's default button handling that can be disconcerting for
user's familiar with the most widely used entry conventions is that, with
Swing's default settings, if the user tabs to select the cancel button and then
presses enter, the form acts just as if the default button (in our example,
that's the OK button!) was pressed. This is by design, but since most users
expect cancel to be executed when it is selected and they press return, they'll
see it as a bug. Fortunately, Swing lets you change the behaviour to what
most users expect with just one line of code, which we've included in the
main method of this example:
The need for this line isn't JComponentBreadboard-specific:
you'd need the same line to make JOptionPane handle default buttons
in the way most users would expect, too.
The code devoted to laying out the components on the screen
is contained in the NumericInputDialog constructor. This code is repeated
below for reference:
There are a couple of techniques in this layout-related code that are frequently
useful that I'd like to draw your attention to:
Also note that, because JComponentBreadboard extends JPanel,
any method (such as the setAligmentX and setBorder methods we
employed in this example) applicable to a JPanel, can be also used to
fine-tune the positioning and format of a JComponentBreadboard.
In general, the rule of thumb in using JComponentBreadboard is
the principle of implied Swing powers: if
it works in a plain Swing form, it will work just the same in a
JComponentBreadboard-based form.
In general,
if you just need a JComponentBreadboard for layout purposes, simply instantiate
it and define the breadboard grid in a single line of code, as shown in this example.
But if you need to connect the form to your application via jbConnect,
you will have to create a new class (such as the NumericInputDialog
class of this
example) that extends JComponentBreadboard.
Although using a single-level-nested, layout-only
JComponentBreadboard (such as our example's buttonBar) is
certainly the most common way to nest, you can freely nest any combination of
full functional and layout-only breadboards, to any depth. You can also
jbConnect one JComponentBreadboard to another, in which case
user initiated changes to connected components on the connected-from breadboard
will automatically generate updates of the connected JComponents on the
connected-to breadboard. And circular connections (for example, A to B, B to C,
C to A) are allowed, and simply guarantee that the connected components on all
of the forms get updated whenever the user initiates a change to a connected
component on any one of them.
In our next example, we'll implement a program that converts back and forth
between the Celsius and Fahrenheit temperature scales. If you're familiar with
the various solutions to this problem presented in the Swing tutorial I think
you'll be impressed with the relative simplicity and directness of the
JComponentBreadboard approach.
Below is a screen shot of the application just after startup, followed by its
complete source code:
The jbConnect lines, and their corresponding encoding/decoding
getter/setter methods, are very similar to what we saw in the last, numeric
input dialog, example. For example, the main property of a JSlider is an
integer, so appropriate conversions between double and int,
such as the rounding via rint in getIntDegreesC, are required.
However, instead of just connecting one
JComponent to an underlying value, here we connect four, each providing
a different way to view and control the same (degreesC) value. Degrees
C, degrees F, slider or spinnner--however users interact with the application,
these connections, and their associated setter/getters, contain all the code
needed to keep everything in synch.
The setBreadBoard line in this example uses the special scaling
directives: NOSCALE, SHRINKS, EXPANDS, and
BISCALE in the breadboard array's row and column headers. These
directives let you control how surplus or deficit space in the parent container
is distributed across the rows and columns of the grid.
These four scaling directives encapsulate all combinations of two boolean
attributes, one which determines if a row or column will scale up in size (from
its preferred size determined height or width) to take advantage of extra space
in the parent, and another which determines if it scales down whenever the
parent is too small to fit everything in at their preferred sizes:
Provided that the scalable rows/columns have sufficient space, all appropriately
scalable rows/columns are scaled up or down until the so-rescaled grid
exactly fits within the parent container.
To see how these directives impact the layout in our example, below are two
screen shots. In the first, the parent container is smaller, and in the second
it is larger, than the size required to display each JComponent within the
breadboard grid at the size returned by its preferredSize method:
In the screen shot whose parent window is too small, the first column
(containing the labels) and first and last rows (containing the sliders) scale
down, because they contain the SHRINKS and BISCALE directives (If the parent
were made small enough, these rows and columns would eventually dissappear
entirely).
In the second "too big" screen shot, the second column (containing the spinners)
and the first and last rows (containing the sliders) scale up because they
contain the EXPANDS and BISCALE directives.
And in both cases, rows 2 and 3 remain the same height, regardless of the parent
container's size, because they contain the NOSCALE directive.
Note that, when these scaling directive headers are completely omitted, as in
our previous examples, by default, every row and column implicitly gets the
NOSCALE directive. It's kind of surprising how frequently this default is
actually a very good choice for defining the layout of a form. In large part
this is because most Swing components do a very good job of choosing
their default preferred sizes.
In addition to the scaling directives, this example's breadboard-array also
employs the JComponentBreadboard layout attribute functions,
xAlign, yAlign, and xFill. These functions define
attributes of a JComponent that impact how it is placed or sized within the
grid-cells that it occupies.
These functions always take a JComponent as their last argument, modify a
grid-cell-layout-related attribute of that JComponent, and then return
a reference to their last, JComponent, argument as their result.
This syntax allows these functions
to be used in a manner that feels like you are specifying attributes or
"qualifiers" of the JComponent. Consider, for example, the expression
found in the upper left corner of our breadboard array:
This yAlign function invokes celsiusSlider.setAlignmentY(0),
and then returns a reference to celsiusSlider as its result.
JComponentBreadboard uses the JComponent's x and y alignment properties
(which must be fractions from 0 and 1) to define, when the preferred size of the
component is less than the size of its containing grid-cell-block, the relative
position within the cell block. For example, for x alignment, 0 is flush left,
0.5 horizontally centered, and 1 flush right and for y alignment 0 is
top-aligned, 0.5 vertically centered, and 1 bottom-aligned. Therefore, in our
example, the above directive instructs that the celsiusSlider be
placed, vertically, along the top edge of the grid cell block it is mapped into
(in this case, that's the entire first row). A similar
yAlign(1,fahrenheitSlider) directive keeps the other slider flush
against the bottom edge of the form.
We could also have invoked the standard setAlignmentY method of the
JComponent on a separate line, and it would have had exactly the same effect.
The advantage of using yAlign isn't just to save a line of code, but
rather to place this information (which is essentially an adjective describing
how this particular component will be aligned within its particular
grid-cell-block) directly in the breadboard array where it belongs.
Similarly, the xAlign method calls
setAlignmentX and thus the
expressions xAlign(1,celsiusLbl) and xAlign(1,fahrenheitLbl)
in the breadboard array serve to right justify these two labels within
their grid cells, bringing them flush up against the spinners they are labeling.
The two middle rows of the breadboard's second column contain the expressions:
The xFill layout attribute function adds a special JComponentBreadboard
recognized client property to its JComponent argument, and then returns that
JComponent as its result. Whenever JComponentBreadboard sees a
JComponent with
this xFill-related attribute on the breadboard, if that
JComponent is narrower
than the grid-cell-block that contains it, it will widen the component until it
fills out the grid-cell-block horizontally. A similar yFill layout
attribute function fills vertically.
One additional family of layout attribute functions, xUpsize and
yUpsize, can be used to treat a component as if, for layout purposes,
it had a preferred size that was a specified number of pixels wider or higher.
As with xAlign and yAlign, the first argument specifies the
number of pixels increase, and the second the JComponent whose layout attribute
is to be specified, and that is returned as the method's result.
Because, with these functions, the JComponent whose attribute is specified is
always the last argument of the function, they can be nested to define multiple
attributes in a manner such that the attributes read much like adjectives
describing the object, as shown in the example below:
Reading from left to right we can see that this is indeed a
right-aligned (from the xAlign(1, ) and top-aligned
(from the yAlign(0,), JLabel.
In this section, by implementing an application that simulates a journey
consisting of one or several random walks, we'll learn how to use JComponentBreadboard to
launch (and provide appropriate progress/cancel feedback for) long computations
with an absolute minimum of effort. We'll also learn how the connection and
layout techniques that we used in our previous examples with individual
JComponents can also be exploited (with the help of some extra indexing
syntax) to connect and layout entire arrays of JComponents.
A screen shot of the application,
along with the complete source code of the RandomWalks class,
is shown below:
This example uses a number of layout techniques we saw in the last example, such
as the xAlign and xFill functions. Note also the use of the
yUpsize directive mentioned earlier to increase the height of each JButton.
Several new techniques are used, however. First, note the use of the "row ditto"
and "column ditto" keywords, "" and __. Whenever the row ditto ( "" ) is
encountered in a breadboard array cell, whatever object reference is on
the row immediately above it is copied into that cell. Similarly, whenever the colum
ditto ( __ ) is placed in a cell, the object reference on the same row, but on the
preceeding column, is copied into that cell. Thus the two keywords act as
conventional and "across the column" ditto marks. These ditto keywords don't
just save you typing. They make the breadboard array more readable because they
allow you to only specify the object reference associated
with each grid-cell-block once, in the upper-left corner of the block.
Another new technique used in the breadboard array is that the
distanceSpinner variable refers to a 1-D array of JComponents,
and this array reference itself, rather than references to the
individual JComponents it contains, is placed onto the breadboard array
(note that by virtue of the ditto marks, this same array reference is copied
into the first three rows of the second column):
This exploits a special array short-hand allowed for use in the breadboard
array. In this case, the
meaning of this short-hand is exactly the same as if we had written
xFill(distanceSpinner[0]), xFill(distanceSpinner[1]), and
xFill(distanceSpinner[2]) in the first three rows of column 2.
In other words, filling a column of the breadboard with the same array
reference simply saves you the trouble of tediously enumerating each array index
down a breadboard column or (which also works just as you would expect) across a
breadboard row. An analogous implicit index expansion is also available
whenever a reference to a 2-D array is placed into a rectangular
area of the breadboard (for an example that uses a 2-D array reference in
this manner, see the DateChooser example
application).
Note also that this array shorthand also works with the layout attribute
functions, such as xFill, xAlign, etc. In this example, the
expression xFill(distanceSpinner) in the breadboard array adds the
xFill attribute to all three spinners at once, and then returns
the distanceSpinner array reference.
Though this layout-related array support is handy, the array support of the
jbConnect method is even more useful, because it allows you to plug
entire arrays of JComponents into arrays (or similarly indexed objects) within
your application. Specifically, in this example, it makes it possible to
directly connect the JComponent[] distanceSpinner array on the
form to the application's int[] distances array:
Because distanceSpinner is an array, the argument lists of every
setter/getter method of the injected interface now include an additional,
integer, array index argument:
Note that the signatures of these getter/setter methods are, apart from the
extra iRow indexing argument, exactly the same as they would have been
for an individual JSpinner configured, as these are, for the entry of integers.
When the user interacts with the first, second, or third spinner in the array to
change it's value, JComponentBreadboard invokes the setDistanceSpinner
method with an iRow argument of 0, 1 or 2, respectively. As this example
illustrates, this array connection feature is particularly helpful when the
underlying objects being manipulated by the array of JComponents (in this case
the distances of our three random walks) are also stored in an array (or a
similarly indexed container).
You can also jbConnect to 2-D arrays of JComponents, in which case two
extra indexing arguments, iRow and iCol are required in the
associated getter/setter methods (the DateChooser
application uses a jbConnected 2-D array of JToggleButtons
to create a traditional monthly-grid-style calendar interface).
Although these method signature indexing conventions for jbConnected
arrays are quite easy to remember, if you forget them (or simply prefer to save
yourself some typing) remember that you can always use the exceptional code
generation feature (as discussed in the HelloWorldPlus section of this document) to
automatically generate appropriately indexed method signatures for whatever
jbConnected arrays your application employs.
These three array-connected JSpinners let the user specify the
distances of a journey consisting of three random walks that the application
simulates. In a one dimensional random walk, the next simulated position is
determined by, with equal probability, either adding or subtracting 1 from
the current position, as implemented in the code line below:
Although the plus and minus steps tend to cancel each other out, given enough
steps, the random nature of the selection process guarantees that,
eventually,
a sufficient number of surplus + or - steps will accumulate
so that any specified distance from the origin can be reached (the number of such random steps required
to drift out to a distance N steps from the origin turns out to be
around N2).
The setter method bound to the click event of the first,
startFirstWalk, button illustrates the common case of providing
progress cancel feedback for a simple, single-stage process:
The jbRun method takes a Runnable representing the process to
to be launched with progress/cancel feedback. Apart from the standard
anonymous inner class syntax used to
to declare such a Runnable on-the-fly, the jbRun invocation
simply says "run this code modally with progress cancel feedback".
The screen shot below shows the
progress cancel dialog that appears after clicking the button of
our sample application attached to the setStartAllWalksTwice method:
As shown above, while a jbRun launched process is running, the root frame or
dialog containing the JComponentBreadboard that launched the application is
disabled. The user just has two choices: watching patiently until the progress bar
reaches 100%, or clicking Cancel to terminate the process.
Because the progress monitoring dialog is a popup modal dialog, although
technically the jbRun invoked process is in fact running in a
separately created thread, it seems to the end-user as if the process was
launched as part of a single threaded application. This is by design. Though it
could be too limiting for certain applications, this "single-threaded feel"
eliminates the need to programmatically handle the different modes of
application operation inherent in multi-tasking. True, this approach may force
your users to check their email rather than using some other aspect of your
application when they want to multi-task. But I've always found that the
simplications associated with jbRun's single-threaded feel more than
make up for any such disadvantages.
Note that the setJbRunCanBeCanceled(true)
invocation in the above code merely enables the cancel button so that the user can raise a
cancel request flag. The process code must use the getJbRunCanceled method to
check for this flag and take appropriate action (as illustrated below).
The takeARandomWalk method that simulates a single random walk that
the above code invokes is repeated below:
The method getJbRunCanceled returns true if the
user has clicked the cancel button, or has closed
the form, and returns false until one of these events occurs. The loop
continues until the user takes one of these actions, or until the
specified distance from the origin has been reached.
The method setJbRunProgress updates the progress bar to show the
percentage of the distance from the origin we have reached so far. The progress
fraction must be a number between 0 and 1 (1 == 100%) and by default, with a
simple single-stage computation like this one, that fraction will be mapped into
the entire range of the progress bar.
The code connected to the second startAllWalks button is shown below:
The code for the takeAMultiStageRandomWalk method invoked above
is repeated below:
The code between the beginProgressMonitoredSection and
endProgressMonitoredSection method calls sets a progress note
that appears above the progress bar, and simply calls the
same routine as the single-stage walk button does, except that rather than
being hard-coded to use the distance of the first random walk, it is indexed so
that it can be applied, sequentially, to all three random walks.
beginProgressMonitoredSection defines a fractional sub-interval
of the enclosing progress monitored section's portion of the progress bar into which any
setJbRunProgress
settings made within this progress monitored sub-section will be
mapped. Because our distance
array has three elements, the expressions we have used to specify the
starting and ending fraction arguments end up breaking
the progress bar into three equal sections. Specifically, the local progress
fraction settings within the first random walk, which, locally,
still go from 0 to 1 as
before, now, by virtue of being inside a progress monitored section whose
limits are 0 to 1/3.,
move the progress bar only from 0 to 1/3. Similarly, the second random walk takes
the progress bar from 1/3. to 2/3., and the third from 2/3. to 1 (1 = 100%).
Note that this simple scheme will, if one random walk is longer than another,
make progress appear to move more slowly in one section, and more quickly in
another. You are free to map each stage into different sized subsections of the
progress bar by adjusting the two parameters of
beginProgressMonitoredSection whenever you have better estimates of the
relative proportion of the entire computation taken up by each begin/end
delimited chunk of your process. However, most users won't care much if the
progress bar moves at different rates--as long as it keeps moving--so a simple,
approximate assignment like the "equally divided" approach we've used here, will
usually be adequate.
You may nest such begin/end delimited sections to virtually any depth
(a limit of 100 is enforced to help you catch incorrect usage). Our example
program illustrates this by nesting the previous routine in yet another, higher
level, function that simply repeats the 3 random walks again:
When the third button, which invokes this method, is clicked,
the first half of the progress bar is devoted to the
first multi-stage random walk, which in turn is subdivided into
three equal sections (each thus a sixth of the entire progress bar), and the
second half of the bar would similarly be devoted to the second multi-stage
invocation, which in turn is also divided into three equal subsections.
Each progress monitored section can have its own progress note; when you
call setJbRunNote, you are only setting the note associated with the
lowest level progress monitored section you happen to be in at the time.
The displayed note concatenates all the notes from all of the nested
progress monitored sections
(from highest to lowest) that the currently executing code line is contained
in,
separating each section's
note with the currently defined jbRunDelimiter String.
By default, that delimiter is a space but you can use setJbRunDelimiter to
change it. The "Swing-subset" of HTML can be used in these notes.
In general, this scheme allows you to compose complex processes from previouly
written stand-alone processes without changing the progress feedback related
instrumentation of the lower level routines at all. If you have ever
written code to do a complex computation, and had to call, say, a standard
deviation routine several times, as part of a larger statistical routine, you
know how aggravating getting all the progress cancel feedback to mesh together
correctly can be. With this approach, to compose a new routine from existing,
progress-monitor-instrumented, pieces, one merely has to sandwich each top-level
piece between beginProgressMonitoredSection and
endProgressMonitoredSection methods whose arguments define a series of
adjacent sequential ranges within the progress bar into which the progress
fraction of each top-level piece will be mapped.
All of the progress intrumentation routines used above are static, and they are
guaranteed to do nothing, both gracefully and quickly, when they are not
called from a jbRun created thread. So, if you have some code you need
to invoke via jbRun and that you also need to invoke within some other
kind of thread, the introduction of the instrumentation above won't prevent your
doing that.
This RandomWalks example application has percentage complete feedback (via
setJbRunProgress), a custom title (via setJbRunTitle),
informational progress notes (via setJbRunNote), and the ability to
cancel the process using the cancel button (via setJbRunCanBeCanceled
and getJbRunCanceled). However, if you are willing to give up all this
functionality and just display a progress cancel dialog with a progress bar in
"indeterminate" mode, a disabled cancel button, an empty note, and a default
title of "Progress..." then you can get away with nothing more than a single
jbRun invocation of the process code you want to launch. This plain
vanilla progress feedback is actually a pretty good choice for the common case
in which the process doesn't take very long to execute, so that detailed
progress information and a method of canceling the run isn't required.
For more information on JComponentBreadboard's support for running worker
threads with progress/cancel feedback, see the
// HelloWorld.java
import javax.swing.*;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class HelloWorld {
public static void main(String[] args) {
Object[][] breadboard = {
{new JLabel("<html><h1>Hello, World !!!")}
};
JComponentBreadboard.showMessageBreadboard(
null, new JComponentBreadboard(breadboard), "HelloWorld");
}
}
// HelloJOptionPane.java
import javax.swing.*;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class HelloJOptionPane {
public static void main(String[] args) {
Object[][] breadboard = {
{new JLabel("<html><h1>Hello, World !!!")}
};
JOptionPane.showMessageDialog(null,
new JComponentBreadboard(breadboard), "HelloJOptionPane", JOptionPane.PLAIN_MESSAGE);
}
}
// HelloWorldPlus.java
import javax.swing.*;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class HelloWorldPlus extends JComponentBreadboard {
public HelloWorldPlus() {
JComponent theCheckBox = new JCheckBox(
"Use the phrase everyone expects to see.");
JComponent theLabel = new JLabel();
setBreadboard(new Object[][] {
{theCheckBox},
{theLabel},
});
jbConnect(theCheckBox, "useExpectedPhrase");
jbConnect(theLabel, "displayedPhrase");
}
private boolean useExpectedPhrase = true;
public void setUseExpectedPhrase(boolean useExpectedPhrase) {
this.useExpectedPhrase = useExpectedPhrase;
}
public boolean getUseExpectedPhrase() {
return useExpectedPhrase;
}
public String getUseExpectedPhraseToolTipText() {
return getDisplayedPhraseText();
}
public String getDisplayedPhraseText() {
if (useExpectedPhrase)
return "<html><h1>Hello, World !";
else
return "<html><h1>Goodbye, GridBag !";
}
public static void main(String[] args) {
showMessageBreadboard(null, new HelloWorldPlus(), "HelloWorldPlus");
}
}
jbConnect(theCheckBox, "useExpectedPhrase");
public AuxiliaryPropertyType getRootNameAuxiliaryPropertyName(),
jbConnect(theLabel, "displayedPhrase");
Exception in thread "main" java.lang.IllegalStateException: Your HelloWorldPlus
object must have a setter method "public void setUseExpectedPhrase(boolean value
)" because your code contains a jbConnect(JCheckBox,"useExpectedPhrase") method
call.
//
// REQUIRED 'jbConnect(JCheckBox,"useExpectedPhrase")' injected methods
// that define the boolean viewed and controlled by this JCheckBox:
// public boolean getUseExpectedPhrase() {}
// public void setUseExpectedPhrase(boolean value) {}
//
// OPTIONAL 'jbConnect(JCheckBox,"useExpectedPhrase")' injected methods that can
// be implemented to define auxiliary properties of this JCheckBox:
// public String getUseExpectedPhraseText() {}
// public boolean getUseExpectedPhraseEnabled() {}
// public boolean getUseExpectedPhraseVisible() {}
// public String getUseExpectedPhraseToolTipText() {}
// public Color getUseExpectedPhraseForeground() {}
// public Color getUseExpectedPhraseBackground() {}
// public boolean getUseExpectedPhraseContentAreaFilled() {}
at net.sourceforge.jcomponentbreadboard.JComponentBreadboard.validateConnection
(JComponentBreadboard.java:2935)
at net.sourceforge.jcomponentbreadboard.JComponentBreadboard.jbConnect(JCompone
ntBreadboard.java:3322)
at HelloWorldPlus.<init>(HelloWorldPlus.java:17)
at HelloWorldPlus.main(HelloWorldPlus.java:45)
Note:
To quickly generate an exception that generates
the injected interface associated with a specific connection, just invoke the
same jbConnect line twice (this works because it is illegal to connect the same
JComponent more than once). For easier browsing of the available interfaces, a table within the
jbConnect method's javadocs
provides
links to example generated code for every supported injected interface.
public String getUseExpectedPhraseToolTipText() {
return getDisplayedPhraseText();
}
For a more advanced application of this same
basic strategy, check out our DateChooser example
application.
// NumericInputDialog.java
import java.awt.Container;
import javax.swing.*;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class NumericInputDialog extends JComponentBreadboard {
private double number;
private double initialValue;
public NumericInputDialog(String prompt, double initialValue) {
this.initialValue = initialValue;
number = initialValue;
JTextField numberField = new JTextField(10);
JButton ok = new JButton("OK");
JButton cancel = new JButton("Cancel");
ok.setPreferredSize(cancel.getPreferredSize()); // makes OK and Cancel same size
JComponentBreadboard buttonBar = new JComponentBreadboard(new Object[][] {
{ok, xSpace(10), cancel}
});
buttonBar.setAlignmentX(0.5f); // centers buttonBar horizontally
setBreadboard(new Object[][] {
{new JLabel(prompt), numberField},
{ySpace(10), null},
{buttonBar, buttonBar}
});
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
jbConnect(numberField, "number");
jbConnect(cancel, "cancel");
jbConnect(KeyStroke.getKeyStroke("ESCAPE"), "cancel");
jbConnect(ok, "ok");
jbConnect(CLOSE_BUTTON, "ok");
}
private final String NOT_A_NUMBER = "";
public String getNumber() {
if (Double.isNaN(number))
return NOT_A_NUMBER;
else
return Double.toString(number);
}
public void setNumber(String value) {
if (value.equals(NOT_A_NUMBER))
number = Double.NaN;
else
number = Double.parseDouble(value);
}
// (optional sign) + (decimal or integer) + (optional exponent)
// as a regular expression (c.f. JDK's Pattern class javadocs)
final String NUMBER_MATCHING_PATTERN =
"[+-]?\\d*((\\d[.])|([.]\\d)|\\d)\\d*([Ee][+-]?\\d+)?";
public String getNumberInvalidDataMessage(String value) {
String result = "<html><title>Invalid Numeric Entry</title>Your entry, \"" + value +
"\" is not a valid number. " +
"<br><br>Correct it below, or cancel to " +
"revert to the previous entry.<br><br> ";
// Non-popup options (uncomment one to try it):
// result = JComponentBreadboard.REVERT_AND_BEEP;
// result = JComponentBreadboard.REVERT_QUIETLY;
if (value.equals(NOT_A_NUMBER) ||
value.equals("Infinity") || value.equals("-Infinity") ||
value.matches(NUMBER_MATCHING_PATTERN))
result = JComponentBreadboard.DATA_IS_VALID;
return result;
}
public void setCancel() {
jbReturn(new Double(initialValue));
}
public void setOk() {
jbReturn(new Double(number));
}
public boolean getOkIsDefaultButton() { return true; }
// Modally prompts user for a number
public static double showNumericInputDialog(Container parent,
String prompt,
String title,
double initialValue) {
double result = (Double) showInputBreadboard(
parent, new NumericInputDialog(prompt, initialValue), title);
return result;
}
public static void main(String[] args) {
UIManager.put("Button.defaultButtonFollowsFocus", Boolean.TRUE);
double result = NumericInputDialog.showNumericInputDialog(
null, "Enter a number: ", "Enter Number", Double.NaN);
JOptionPane.showMessageDialog(null,
"Number entered = " + result);
}
}
jbConnect(numberField, "number");
private final String NOT_A_NUMBER = "";
public String getNumber() {
if (Double.isNaN(number))
return NOT_A_NUMBER;
else
return Double.toString(number);
}
public void setNumber(String value) {
if (value.equals(NOT_A_NUMBER))
number = Double.NaN;
else
number = Double.parseDouble(value);
}
public String getXxxInvalidDataMessage(String inputToValidate) {...}
public String getNumberInvalidDataMessage(String value) {
String result = "<html><title>Invalid Numeric Entry</title>Your entry, \"" + value +
"\" is not a valid number. " +
"<br><br>Correct it below, or cancel to " +
"revert to the previous entry.<br><br> ";
// Non-popup options (uncomment one to try it):
// result = JComponentBreadboard.REVERT_AND_BEEP;
// result = JComponentBreadboard.REVERT_QUIETLY;
if (value.equals(NOT_A_NUMBER) ||
value.equals("Infinity") || value.equals("-Infinity") ||
value.matches(NUMBER_MATCHING_PATTERN))
result = JComponentBreadboard.DATA_IS_VALID;
return result;
}
Note: In this example, the title of the popup dialog was specified by including
an HTML title tag in the returned error message string (specifically, by
including <title>Invalid Numeric Entry</title>). The tag is honored
even if the rest of the string isn't in HTML. Without such a tag, the default
title becomes "Invalid Input".
jbConnect(cancel, "cancel");
jbConnect(KeyStroke.getKeyStroke("ESCAPE"), "cancel");
public void setCancel() {
jbReturn(new Double(initialValue));
}
Why setCancel instead of, say, onCancelClick?
JButtons, KeyStrokes, and JMenuItems have a main property of type void.
Thus, when the user "interacts with a JButton to change it's state" (by clicking
on it) a no-argument setter method gets called (for JComponents with
void
main properties, the associated getter
method is dropped from the interface). The setter method doesn't set anything, so it's really just a click
event handler, not a setter. However, every "real" setter method also
has the option of generating event-handling side effects. For example, clicking
on a checkbox labeled "Display Pi to 1000 places" might invoke a setter method
that launched a long
computation the first time it was called. Thus there is a certain consistency
to calling the event handler for a JButton a no-argument setter. Besides, since
all the other JComponents execute a setter when the user interacts with
the component to change the application's state, sticking with a
"setters for everything" naming convention means there's one less thing to remember.
jbConnect(KeyStroke.getKeyStroke("F1"), "helpButton");
jbConnect(ok, "ok");
jbConnect(CLOSE_BUTTON, "ok");
public void setOk() {
jbReturn(new Double(number));
}
Note:
JComponentBreadboard's
createBreadboardDialog
method returns a
reference to one of these specially configured JDialog's used by
showInputBreadboard, and also allows you to create non-modal forms of
these dialogs.
public boolean getOkIsDefaultButton() { return true; }
UIManager.put("Button.defaultButtonFollowsFocus", Boolean.TRUE);
JTextField numberField = new JTextField(10);
JButton ok = new JButton("OK");
JButton cancel = new JButton("Cancel");
ok.setPreferredSize(cancel.getPreferredSize()); // makes OK and Cancel same size
JComponentBreadboard buttonBar = new JComponentBreadboard(new Object[][] {
{ok, xSpace(10), cancel}
});
buttonBar.setAlignmentX(0.5f); // centers buttonBar horizontally
setBreadboard(new Object[][] {
{new JLabel(prompt), numberField},
{ySpace(10), null},
{buttonBar, buttonBar}
});
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
It's like a poor man's parenthesized reverse-Polish packer!
-- Jay Seage,
commenting on the layout attribute functions discussed in this section
// CelsiusFahrenheitConverter.java
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.BorderFactory;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class CelsiusFahrenheitConverter extends JComponentBreadboard {
final double DEFAULT_C = 25; // room temperature
final double MIN_C = -40; // -40 C == -40 F
final double MAX_C = 100.0; // water's boiling point
final double DELTA_C = 1; // Celsius-spinner change per click
final double DELTA_F = 1; // Fahrenheit-spinner change per click
private double cToF(double c) { // Celsius to Fahrenheit
return (9./5.)*c + 32.0;
}
private double fToC(double f) { // Fahrenheit to Celsius
return (f-32)*(5./9.);
}
public CelsiusFahrenheitConverter() {
JLabel celsiusLbl = new JLabel("Degrees C: ");
JSpinner celsiusSpinner = new JSpinner(new SpinnerNumberModel(
DEFAULT_C, MIN_C, MAX_C, DELTA_C));
JSlider celsiusSlider = new JSlider(
(int) MIN_C, (int) MAX_C);
JLabel fahrenheitLbl = new JLabel("Degrees F: ");
JSpinner fahrenheitSpinner = new JSpinner(new SpinnerNumberModel(
cToF(DEFAULT_C), cToF(MIN_C), cToF(MAX_C), DELTA_F));
JSlider fahrenheitSlider = new JSlider(
(int) cToF(MIN_C), (int) cToF(MAX_C));
setBreadboard(new Object[][] {
{null, SHRINKS, EXPANDS},
{BISCALE, yAlign(0,celsiusSlider), celsiusSlider},
{NOSCALE, xAlign(1,celsiusLbl), xFill(celsiusSpinner)},
{NOSCALE, xAlign(1,fahrenheitLbl), xFill(fahrenheitSpinner)},
{BISCALE, yAlign(1,fahrenheitSlider), fahrenheitSlider},
});
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
jbConnect(celsiusSpinner, "degreesC");
jbConnect(celsiusSlider, "intDegreesC");
jbConnect(fahrenheitSpinner, "degreesF");
jbConnect(fahrenheitSlider, "intDegreesF");
}
double degreesC = DEFAULT_C; // what everything views and controls
public double getDegreesC() { return degreesC; }
public void setDegreesC(double c) { degreesC = c;}
public int getIntDegreesC() {return (int) Math.rint(degreesC);}
public void setIntDegreesC(int c) {degreesC = c;}
public double getDegreesF() { return cToF(degreesC); }
public void setDegreesF(double f) { degreesC = fToC(f);}
public int getIntDegreesF() {return (int) Math.rint(getDegreesF());}
public void setIntDegreesF(int f) {setDegreesF(f);}
public static void main(String[] args) {
showMessageBreadboard(null,
new CelsiusFahrenheitConverter(),
"C to F and F to C");
}
}
Note:The discussion of JComponentBreadboard's layout manager in this
section, though simplified, will likely be all you need to know. For a complete
description
see the
setBreadboard
method's javadocs.
Directive Scales down if parent
is too small to fit
all components into the grid
at their preferred sizesScales up if parent
is bigger than required to fit
all components into the grid
at their preferred sizes
NOSCALE No No
SHRINKS Yes No
EXPANDS No Yes
BISCALE Yes Yes
Note:
"The size required to display each JComponent within the
breadboard grid at the size returned by its preferredSize method"
is also the size the parent will become after the pack method is applied to
it. In all of our examples, this is the initial size of the content pane of the displayed
dialog (before any resizing via it's sizing border).
yAlign(0, celsiusSlider)
xFill(celsiusSpinner)
xFill(fahrenheitSpinner)
xAlign(1,yAlign(0,new JLabel("A right and top aligned JLabel")))
RandomWalks
A journey of a thousand steps begins with a single step of a million
step random walk.
-- Albert E.
Brown
// RandomWalks.java
import javax.swing.*;
import net.sourceforge.jcomponentbreadboard.JComponentBreadboard;
public class RandomWalks extends JComponentBreadboard {
private int[] distances = {2,2,2};
public RandomWalks() {
JSpinner[] distanceSpinner = new JSpinner[distances.length];
for (int i = 0; i < distanceSpinner.length; i++) {
distanceSpinner[i] = new JSpinner(new SpinnerNumberModel(1,1,10,1));
}
JButton startFirstWalk = new JButton(
"Just take the first random walk.");
JButton startAllWalks = new JButton(
"Take all 3 random walks, in sequence.");
JButton startAllWalksTwice = new JButton(
"Take all 3 random walks. Then do it all again.");
setBreadboard(new Object[][] {
{xAlign(1, new JLabel("Walk #1 distance: ")), xFill(distanceSpinner)},
{xAlign(1, new JLabel("Walk #2 distance: ")), "" },
{xAlign(1, new JLabel("Walk #3 distance: ")), "" },
{ySpace(5), null },
{yUpsize(10,xFill(startFirstWalk)), __ },
{yUpsize(10,xFill(startAllWalks)), __ },
{yUpsize(10,xFill(startAllWalksTwice)), __ },
});
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
jbConnect(distanceSpinner, "distanceSpinner");
jbConnect(startFirstWalk, "startFirstWalk");
jbConnect(startAllWalks, "startAllWalks");
jbConnect(startAllWalksTwice, "startAllWalksTwice");
}
public void setDistanceSpinner(int iRow, int distance) {
distances[iRow] = distance;
}
public int getDistanceSpinner(int iRow) {
return distances[iRow];
}
// take a random walk with the specified distance
public void takeARandomWalk(int distance) {
double currentPosition = 0;
while (Math.abs(currentPosition) < distance &&
!getJbRunCanceled()) {
currentPosition += (Math.random() < 0.5) ? 1 : -1;
setJbRunProgress(Math.abs(currentPosition)/distance);
try {Thread.sleep(1000);} catch (InterruptedException e) {} // wait 1 second
}
}
// Take a sequence of random walks, each of a specified distance from the origin
public void takeAMultiStageRandomWalk(int[] distance) {
for (int i = 0;
i < distance.length && !getJbRunCanceled();
i++) {
beginProgressMonitoredSection(
(i+0.)/distance.length, (i+1.)/distance.length);
setJbRunNote("Randomly walking walk #" + (i+1));
takeARandomWalk(distance[i]);
endProgressMonitoredSection();
}
}
// take our entire sequence of 3 random walks, twice
public void takeAMultiStageRandomWalkTwice(int[] distance) {
final int N_SEQUENCES = 2;
for (int i = 0;
i < N_SEQUENCES && !getJbRunCanceled();
i++) {
beginProgressMonitoredSection(
(i+0.)/N_SEQUENCES, (i+1.)/N_SEQUENCES);
setJbRunNote("Sequence #" + (i+1));
takeAMultiStageRandomWalk(distance);
endProgressMonitoredSection();
}
}
public void setStartFirstWalk() {
jbRun(new Runnable() {public void run() {
setJbRunTitle("Walking First Walk");
setJbRunCanBeCanceled(true);
setJbRunNote("Randomly walking just the first walk.");
takeARandomWalk(distances[0]);
}});
}
public void setStartAllWalks() {
jbRun(new Runnable() {public void run() {
setJbRunTitle("Walking All Walks");
setJbRunCanBeCanceled(true);
takeAMultiStageRandomWalk(distances);
}});
}
public void setStartAllWalksTwice() {
jbRun(new Runnable() {public void run() {
setJbRunTitle("Walking All Walks, Twice");
setJbRunCanBeCanceled(true);
takeAMultiStageRandomWalkTwice(distances);
}});
}
public static void main(String[] arg) {
showMessageBreadboard(null, new RandomWalks(), "Random Walks");
}
}
setBreadboard(new Object[][] {
{xAlign(1, new JLabel("Walk #1 distance: ")), xFill(distanceSpinner)},
{xAlign(1, new JLabel("Walk #2 distance: ")), "" },
{xAlign(1, new JLabel("Walk #3 distance: ")), "" },
{ySpace(5), null },
{yUpsize(10,xFill(startFirstWalk)), __ },
{yUpsize(10,xFill(startAllWalks)), __ },
{yUpsize(10,xFill(startAllWalksTwice)), __ },
});
jbConnect(distanceSpinner, "distanceSpinner");
public void setDistanceSpinner(int iRow, int distance) {
distances[iRow] = distance;
}
public int getDistanceSpinner(int iRow) {
return distances[iRow];
}
Can you connect heterogeneous JComponent arrays? You
can, but I find it less confusing to break such an array up into several,
independently connected, homogeneous arrays (arrays all of whose elements are of
exactly the same type). For a detailed discussion of this and other
connection-related issues, see the
jbConnect(JComponent[],String) method's javadocs
.
currentPosition += (Math.random() < 0.5) ? 1 : -1;
public void setStartFirstWalk() {
jbRun(new Runnable() {public void run() {
setJbRunTitle("Walking First Walk");
setJbRunCanBeCanceled(true);
setJbRunNote("Randomly walking just the first walk.");
takeARandomWalk(distances[0]);
}});
}
// take a random walk with the specified distance
public void takeARandomWalk(int distance) {
double currentPosition = 0;
while (Math.abs(currentPosition) < distance &&
!getJbRunCanceled()) {
currentPosition += (Math.random() < 0.5) ? 1 : -1;
setJbRunProgress(Math.abs(currentPosition)/distance);
try {Thread.sleep(1000);} catch (InterruptedException e) {} // wait 1 second
}
}
public void setStartAllWalks() {
jbRun(new Runnable() {public void run() {
setJbRunTitle("Walking All Walks");
setJbRunCanBeCanceled(true);
takeAMultiStageRandomWalk(distances);
}});
}
// Take a sequence of random walks, each of a specified distance from the origin
public void takeAMultiStageRandomWalk(int[] distance) {
for (int i = 0;
i < distance.length && !getJbRunCanceled();
i++) {
beginProgressMonitoredSection(
(i+0.)/distance.length, (i+1.)/distance.length);
setJbRunNote("Randomly walking walk #" + (i+1));
takeARandomWalk(distance[i]);
endProgressMonitoredSection();
}
}
// take our entire sequence of 3 random walks, twice
public void takeAMultiStageRandomWalkTwice(int[] distance) {
final int N_SEQUENCES = 2;
for (int i = 0;
i < N_SEQUENCES && !getJbRunCanceled();
i++) {
beginProgressMonitoredSection(
(i+0.)/N_SEQUENCES, (i+1.)/N_SEQUENCES);
setJbRunNote("Sequence #" + (i+1));
takeAMultiStageRandomWalk(distance);
endProgressMonitoredSection();
}
}
jbRun method's
javadocs
.
HelloWorld and related tutorial-style demos are fine, but ultimately, a form tool needs to facilitate creating forms in real applications if it is to be useful. In this section we will provide JComponentBreadboard-based emulations of several forms taken directly from the Firefox web browser, along with several other forms of a similar, real-world, complexity.
These applications illustrate how, even in the face of the complexities of real-world forms, there's still a very easy to understand corrrespondence between the form's layout/behaviour, and the code of the JComponentBreadboard-derived class that specifies them.
Because these examples are much longer than those of the last section, in the interests of finishing this tutorial in a reasonable time frame, I'll be relying a lot more on JComponentBreadboard's intrinsic intelligibility (and on your own code reading and analysis skills) in this section.
The Firefox browser's Tools, Clear Private Data command pops up a form that I've emulated using the JComponentBreadboard-based application shown below:
This application illustrates a number of strategies that you are likely to find helpful in your own forms:
// FirefoxClearPrivateDataForm.java import java.awt.event.KeyEvent; import javax.swing.*; import net.sourceforge.jcomponentbreadboard.JComponentBreadboard; // This class is our stand-in for that part of Firefox responsible for // managing/maintaining private user data (that the form provides a GUI for clearing). class FirefoxPrivateData { // Internal state representation flags: define if there is anything that needs clearing public boolean isBrowsingHistoryCleared = false; public boolean isDownloadHistoryCleared = false; public boolean isSearchHistoryCleared = false; public boolean isCacheCleared = false; public boolean areCookiesCleared = false; public boolean areSavedPasswordsCleared = false; public boolean areAuthenticatedSessionsCleared = false; // User preference flags: define what data gets cleared when user clicks the // "Clear Private Data Now" button public boolean clearBrowsingHistory = true; public boolean clearDownloadHistory = true; public boolean clearSearchHistory = true; public boolean clearCache = true; public boolean clearCookies = false; public boolean clearSavedPasswords = false; public boolean clearAuthenticatedSessions = true; // If this were Firefox, this would actually clear out the data (not just zap flags) public void clearPrivateData() { if (clearBrowsingHistory) isBrowsingHistoryCleared = true; if (clearDownloadHistory) isDownloadHistoryCleared = true; if (clearSearchHistory) isSearchHistoryCleared = true; if (clearCache) isCacheCleared = true; if (clearCookies) areCookiesCleared = true; if (clearSavedPasswords) areSavedPasswordsCleared = true; if (clearAuthenticatedSessions) areAuthenticatedSessionsCleared = true; } } public class FirefoxClearPrivateDataForm extends JComponentBreadboard { private FirefoxPrivateData privateData; public FirefoxClearPrivateDataForm(FirefoxPrivateData privateData) { this.privateData = privateData; JCheckBox browsingHistory = new JCheckBox("Browsing History"); browsingHistory.setMnemonic(KeyEvent.VK_B); JCheckBox downloadHistory = new JCheckBox("Download History"); downloadHistory.setMnemonic(KeyEvent.VK_D); JCheckBox searchHistory = new JCheckBox("Saved Form and Search History"); searchHistory.setMnemonic(KeyEvent.VK_F); JCheckBox cache = new JCheckBox("Cache"); cache.setMnemonic(KeyEvent.VK_A); JCheckBox cookies = new JCheckBox("Cookies"); cookies.setMnemonic(KeyEvent.VK_C); JCheckBox savedPasswords = new JCheckBox("Saved Passwords"); savedPasswords.setMnemonic(KeyEvent.VK_P); JCheckBox authenticatedSessions = new JCheckBox("Authenticated Sessions"); authenticatedSessions.setMnemonic(KeyEvent.VK_S); JButton clearPrivateDataNow = new JButton("Clear Private Data Now"); JButton cancel = new JButton("Cancel"); JComponent buttonBar = new JComponentBreadboard( new Object[][] {{xSpace(200),clearPrivateDataNow, xSpace(10), cancel}}); JLabel instructions = new JLabel("Clear the following items now:"); final int SPACE = 5; // adds extra vertical space between checkboxes setBreadboard(new Object[][] { {instructions}, {ySpace(SPACE)}, {browsingHistory}, {ySpace(SPACE)}, {downloadHistory}, {ySpace(SPACE)}, {searchHistory}, {ySpace(SPACE)}, {cache}, {ySpace(SPACE)}, {cookies}, {ySpace(SPACE)}, {savedPasswords}, {ySpace(SPACE)}, {authenticatedSessions}, {ySpace(SPACE)}, {buttonBar}, {ySpace(SPACE)}, }); setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); jbConnect(browsingHistory, "browsingHistory"); jbConnect(downloadHistory, "downloadHistory"); jbConnect(searchHistory, "searchHistory"); jbConnect(cache, "cache"); jbConnect(cookies, "cookies"); jbConnect(savedPasswords, "savedPasswords"); jbConnect(authenticatedSessions, "authenticatedSessions"); jbConnect(clearPrivateDataNow, "clearPrivateDataNow"); jbConnect(cancel, "cancel"); jbConnect(CLOSE_BUTTON, "cancel"); } // our screen shot grabber needs a no-arg constructor public FirefoxClearPrivateDataForm() { this(new FirefoxPrivateData()); } public boolean getBrowsingHistory() { return privateData.clearBrowsingHistory; } public void setBrowsingHistory(boolean value) { privateData.clearBrowsingHistory = value; } public boolean getBrowsingHistoryEnabled() { return !privateData.isBrowsingHistoryCleared; } public boolean getDownloadHistory() { return privateData.clearDownloadHistory; } public void setDownloadHistory(boolean value) { privateData.clearDownloadHistory = value; } public boolean getDownloadHistoryEnabled() { return !privateData.isDownloadHistoryCleared; } public boolean getSearchHistory() { return privateData.clearSearchHistory; } public void setSearchHistory(boolean value) { privateData.clearSearchHistory = value; } public boolean getSearchHistoryEnabled() { return !privateData.isSearchHistoryCleared; } public boolean getCache() { return privateData.clearCache; } public void setCache(boolean value) { privateData.clearCache = value; } public boolean getCacheEnabled() { return !privateData.isCacheCleared; } public boolean getCookies() { return privateData.clearCookies; } public void setCookies(boolean value) { privateData.clearCookies = value; } public boolean getCookiesEnabled() { return !privateData.areCookiesCleared; } public boolean getSavedPasswords() { return privateData.clearSavedPasswords; } public void setSavedPasswords(boolean value) { privateData.clearSavedPasswords = value; } public boolean getSavedPasswordsEnabled() { return !privateData.areSavedPasswordsCleared; } public boolean getAuthenticatedSessions() { return privateData.clearAuthenticatedSessions; } public void setAuthenticatedSessions(boolean value) { privateData.clearAuthenticatedSessions = value; } public boolean getAuthenticatedSessionsEnabled() { return !privateData.areAuthenticatedSessionsCleared; } public void setClearPrivateDataNow() { privateData.clearPrivateData(); jbReturn(new Boolean(true)); } public boolean getClearPrivateDataNowEnabled() { return (getBrowsingHistory() && getBrowsingHistoryEnabled()) || (getDownloadHistory() && getDownloadHistoryEnabled()) || (getSearchHistory() && getSearchHistoryEnabled()) || (getCache() && getCacheEnabled()) || (getCookies() && getCookiesEnabled() )|| (getSavedPasswords() && getSavedPasswordsEnabled()) || (getAuthenticatedSessions() && getAuthenticatedSessionsEnabled()); } public void setCancel() { //Cancel should undo changes, but since our app ends anyway, it doesn't matter. jbReturn(new Boolean(false)); } // This main program essentially emulates someone repeatedly clicking on // the "Clear Now..." button within Firefox until they click cancel, which // ends the application. public static void main(String[] args) { FirefoxPrivateData privateData = new FirefoxPrivateData(); JComponentBreadboard clearPrivateDataForm = new FirefoxClearPrivateDataForm(privateData); while ((Boolean) showInputBreadboard(null, clearPrivateDataForm, "Clear Private Data")) { continue; } } }
As you review the application's code below, keep an eye out for the following useful techniques:
Beyond these specific techniques, overall, this application provides an excellent example of how the various features of JComponentBreadboard (array-indexed connections, succinct grid-based layout, pluggable keystroke handlers, etc.) allow you to write exceptionally easy to understand code that captures the relationship between an underlying Java "model" (in this case, a JDK Calendar object) and the form that "views and controls" it.
// DateChooser.java import java.awt.Color; import java.awt.Container; import java.util.Calendar; import java.util.Date; import java.util.Hashtable; import javax.swing.*; import net.sourceforge.jcomponentbreadboard.JComponentBreadboard; public class DateChooser extends JComponentBreadboard { private final int MIN_YEAR = 1900; private final int MAX_YEAR = 2099; private Calendar calendar = Calendar.getInstance(); private String[] weekDayLabels = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; private final int DAYS_PER_WEEK = weekDayLabels.length; private String[] listOfMonths = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; // each month remembers the last selected day; initialize with day 1 int[] selectedDay = {1,1,1,1,1,1,1,1,1,1,1,1}; int selectedMonth; int selectedYear; Date initialDate; JComponent[][] dayOfMonth; public DateChooser(Date initialDate) { this.initialDate = initialDate; calendar.setTime((null == initialDate) ? new Date() : initialDate); selectedYear = calendar.get(Calendar.YEAR); selectedMonth = calendar.get(Calendar.MONTH); selectedDay[selectedMonth] = calendar.get(Calendar.DAY_OF_MONTH); // raise exception for dates beyond range we can handle if (selectedYear < MIN_YEAR || selectedYear > MAX_YEAR) throw new IllegalArgumentException("Initial date's year, " + selectedYear + " is not in the required range: " + MIN_YEAR + "..." + MAX_YEAR); JSpinner yearSpinner = new JSpinner(new SpinnerListModel(intList(MIN_YEAR, MAX_YEAR))); JComboBox monthComboBox = new JComboBox(listOfMonths); monthComboBox.setMaximumRowCount(listOfMonths.length); JSpinner daySpinner = new JSpinner(new SpinnerListModel(getDaySpinnerItems())); JComponent monthSlider = yUpsize(10, new JSlider()); JLabel dateLabel = new JLabel(); // dropdown lists, spinners, labels for selecting/displaying year, month, and day JComponent yearMonthDayBar = xFill(yUpsize(10, new JComponentBreadboard( new Object[][] { {null, NOSCALE, NOSCALE, NOSCALE, NOSCALE, NOSCALE, BISCALE, NOSCALE}, {NOSCALE, xAlign(0,monthComboBox), xSpace(10), yFill(xAlign(0.5,daySpinner)), xSpace(10), yFill(xAlign(1, yearSpinner)), xSpace(10), dateLabel} } ))); JButton ok = new JButton("Choose Date"); JButton cancel = new JButton("Cancel"); JComponent commandButtonBar = xAlign(1, new JComponentBreadboard(new Object[][] { {null, BISCALE, NOSCALE, NOSCALE, NOSCALE }, {NOSCALE, ySpace(10), null, null, null }, {NOSCALE, xSpace(10), ok, xSpace(10), cancel} })); JComponent[] dayOfWeekHeader = new JLabel[DAYS_PER_WEEK]; // why it's 6: partial week on top + 4 full weeks in middle + partial on bottom final int MAX_DISPLAYED_WEEKS = 6; dayOfMonth = new JToggleButton[MAX_DISPLAYED_WEEKS][DAYS_PER_WEEK]; for (int i = 0; i < DAYS_PER_WEEK; i++) { dayOfWeekHeader[i] = xAlign(0.5, new JLabel(weekDayLabels[i])); for (int j = 0; j < MAX_DISPLAYED_WEEKS; j++) { dayOfMonth[j][i] = xyFill(new JToggleButton()); } } setBreadboard(new Object[][] { {null, EXPANDS, __, __, __, __, __, __}, {NOSCALE, yearMonthDayBar, __, __, __, __, __, __}, {"", monthSlider, __, __, __, __, __, __}, {"", dayOfWeekHeader, __, __, __, __, __, __}, {EXPANDS, dayOfMonth, __, __, __, __, __, __}, {"", "", __, __, __, __, __, __}, {"", "", __, __, __, __, __, __}, {"", "", __, __, __, __, __, __}, {"", "", __, __, __, __, __, __}, {"", "", __, __, __, __, __, __}, {NOSCALE, commandButtonBar, __, __, __, __, __, __} }); setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); jbConnect(daySpinner, "daySpinner"); jbConnect(monthComboBox, "month"); jbConnect(monthSlider, "monthSlider"); jbConnect(yearSpinner, "year"); jbConnect(dayOfMonth, "dayOfMonth"); jbConnect(ok, "ok"); jbConnect(CLOSE_BUTTON, "ok"); jbConnect(cancel, "cancel"); jbConnect(KeyStroke.getKeyStroke("ESCAPE"), "cancel"); jbConnect(dateLabel, "dateLabel"); jbConnect(KeyStroke.getKeyStroke("LEFT"), "left"); jbConnect(KeyStroke.getKeyStroke("RIGHT"), "right"); jbConnect(KeyStroke.getKeyStroke("UP"), "up"); jbConnect(KeyStroke.getKeyStroke("DOWN"), "down"); } // returns integer sequence from iMin to iMax as an array of Strings private String[] intList(int iMin, int iMax) { String[] result = new String[iMax - iMin + 1]; for (int i = 0; i < iMax-iMin+1; i++) result[i] = Integer.toString(iMin + i); return result; } // move the calendar to the date associated with the specified row and column private void gotoDate(int iRow, int iCol) { calendar.set(selectedYear, selectedMonth, 1); // value returned for day-of-week is 1-Sun, 2-Mon, but we want 0-Sun, 1-Mon int firstDay = calendar.get(Calendar.DAY_OF_WEEK)-1; calendar.add(Calendar.DAY_OF_MONTH, iRow * DAYS_PER_WEEK + iCol - firstDay); } // togglebutton is only selected (in "pressed state") if it // represents the currently selected date public boolean getDayOfMonth(int iRow, int iCol) { gotoDate(iRow, iCol); boolean result = calendar.get(Calendar.YEAR) == selectedYear && calendar.get(Calendar.MONTH) == selectedMonth && calendar.get(Calendar.DAY_OF_MONTH) == selectedDay[selectedMonth]; return result; } public void setDayOfMonth(int iRow, int iCol, boolean value) { gotoDate(iRow, iCol); selectedYear = calendar.get(Calendar.YEAR); selectedMonth = calendar.get(Calendar.MONTH); selectedDay[selectedMonth] = calendar.get(Calendar.DAY_OF_MONTH); } public String getDayOfMonthText(int iRow, int iCol) { gotoDate(iRow, iCol); String result = calendar.get(Calendar.DAY_OF_MONTH) + ""; return result; } public boolean getDayOfMonthContentAreaFilled(int iRow, int iCol) { boolean result = getDayOfMonth(iRow, iCol); return result; } public Color getDayOfMonthForeground(int iRow, int iCol) { Color result = getDayOfMonth(iRow, iCol) ? Color.RED : Color.BLACK; return result; } // Disables "before month" cells at top and "after month" cells at bottom public boolean getDayOfMonthEnabled(int iRow, int iCol) { gotoDate(iRow, iCol); boolean result = (calendar.get(Calendar.MONTH) == selectedMonth); return result; } // these next 4 methods are connected to same-named keystrokes to allow // navigation of the monthly calendar region via the arrow keys public void setLeft() { selectedDay[selectedMonth] = Math.max(selectedDay[selectedMonth]-1, 1); } public void setRight() { selectedDay[selectedMonth] = Math.min(selectedDay[selectedMonth]+1, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); } public void setUp() { if (selectedDay[selectedMonth] - DAYS_PER_WEEK >= 1) { selectedDay[selectedMonth] = selectedDay[selectedMonth] - DAYS_PER_WEEK; } } public void setDown() { if (selectedDay[selectedMonth] + DAYS_PER_WEEK <= calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) { selectedDay[selectedMonth] = selectedDay[selectedMonth] + DAYS_PER_WEEK; } } public void setOk() { calendar.set(selectedYear, selectedMonth, selectedDay[selectedMonth]); jbReturn(calendar.getTime()); } public boolean getOkIsDefaultButton() { return true; } public void setCancel() { jbReturn(initialDate); } public String getMonth() { return listOfMonths[selectedMonth]; } // assures we don't exceed max day setting during leap years (or similar oddities) private void patchForLeapYears() { calendar.set(selectedYear, selectedMonth, 1); selectedDay[selectedMonth] = Math.min(selectedDay[selectedMonth], calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); } public void setMonth(String value) { for (int i=0; i < listOfMonths.length; i++) { if (value.equals(listOfMonths[i])) { selectedMonth = i; break; } } patchForLeapYears(); } public String getYear() { return selectedYear + ""; } public void setYear(String value) { selectedYear = Integer.parseInt(value); patchForLeapYears(); } public String getDateLabelText() { return listOfMonths[selectedMonth] + " " + selectedDay[selectedMonth] + ", " + selectedYear; } public String getDaySpinner() { return selectedDay[selectedMonth] + ""; } public void setDaySpinner(String value) { selectedDay[selectedMonth] = Integer.parseInt(value); } public String[] getDaySpinnerItems() { calendar.set(selectedYear, selectedMonth, selectedDay[selectedMonth]); String[] result = intList(1, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); return result; } public int getMonthSlider() { return selectedMonth; } public void setMonthSlider(int value) { selectedMonth = value; } public Hashtable<Integer, JLabel> getMonthSliderLabelTable() { Hashtable<Integer, JLabel> result = new Hashtable<Integer, JLabel>(); for (int i = 1; i <= listOfMonths.length; i++) result.put(i-1, new JLabel(i + "")); return result; } public int getMonthSliderMajorTickSpacing() {return 1;} public int getMonthSliderMaximum() {return listOfMonths.length-1;} public int getMonthSliderMinimum() {return 0;} public boolean getMonthSliderPaintLabels() {return true;} public boolean getMonthSliderPaintTicks() {return false;} public boolean getMonthSliderPaintTrack() {return true;} // The static method clients will use to invoke the modal DateChooser // to get a date from the user. Everything else exists to implement this. public static Date showDateChooser(Container parent, Date initialDate, String title) { Date result = (Date) showInputBreadboard(parent, new DateChooser(initialDate), title); return result; } // tests the date-chooser popup modal dialog public static void main(String[] args) { Date result = showDateChooser(null, new Date(), "Choose a Date"); JOptionPane.showMessageDialog(null, "The chooser returned: " + result); } }
Legislating that Pi exactly equals 3.1 was ridiculous. A law that declares that 1+1 must always equal 2--now that's something I could support. |
--Name withheld upon request. |
This application implements a calculator that looks like an interconnected series of the kind of simple computations I did in first grade: two numbers stacked vertically with a line drawn under them, an operator to the left of the second number, and the result (= number1 operator number2) written underneath.
These computations chain together such that the result (bottom) number of each such grouping becomes the first (top) number of the next. Though only five such groups are visible at a time, the scrollbar lets you access up to 1000. Shown below is the calculator after having been used to calculate 2+2=4:
Several techniques of interest are used in the code that implements this application:
// ScrollingCalculator.java import java.awt.AWTKeyStroke; import java.awt.Font; import java.awt.KeyboardFocusManager; import java.awt.event.KeyEvent; import java.util.HashSet; import javax.swing.*; import net.sourceforge.jcomponentbreadboard.JComponentBreadboard; public class ScrollingCalculator extends JComponentBreadboard { final String[] operatorList = {"+", "-", "*", "/", "^", "="}; final int MAX_ENTRIES = 1000; final int VISIBLE_ENTRIES = 5; final String NAN_STRING = ""; double[] entered = new double[MAX_ENTRIES]; double[] computed = new double[MAX_ENTRIES]; String[] operator = new String[MAX_ENTRIES]; int firstVisibleEntry = 0; int maxEnteredEntry = -1; int recentlyEnteredEntry = -1; JComponent[] enteredJC = new JTextField[VISIBLE_ENTRIES]; JComponent[] computedJC = new JTextField[VISIBLE_ENTRIES]; JComponent[] operatorJC = new JComboBox[VISIBLE_ENTRIES]; JComponent scrollBar = xAlign(0,xUpsize(5, new JScrollBar())); // so user can press return as well as tab to move to the next field private void makeEnterActLikeTab() { HashSet<AWTKeyStroke> forwardKeys = new HashSet<AWTKeyStroke>(); forwardKeys.add(AWTKeyStroke.getAWTKeyStroke("ENTER")); forwardKeys.add(AWTKeyStroke.getAWTKeyStroke("TAB")); this.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys); } public ScrollingCalculator() { makeEnterActLikeTab(); Font f = new Font("Monospaced", Font.PLAIN, 16); for (int i = 0; i < VISIBLE_ENTRIES; i++) { computedJC[i] = xyFill(xAlign(0, new JTextField())); computedJC[i].setFont(f); operatorJC[i] = new JComboBox(operatorList); operatorJC[i].setFont(f); enteredJC[i] = xyFill(new JTextField()); enteredJC[i].setFont(f); } JButton resetButton = new JButton("Reset"); resetButton.setMnemonic(KeyEvent.VK_R); JButton closeButton = new JButton("Close"); closeButton.setMnemonic(KeyEvent.VK_C); JComponent buttonBar = yUpsize(10, xFill(new JComponentBreadboard(new Object[][] { {null, BISCALE, NOSCALE, NOSCALE, NOSCALE}, {NOSCALE, xSpace(10),resetButton, xSpace(10), closeButton} }))); resetCalculator(); setBreadboard(new Object[][] { {null, NOSCALE, BISCALE, NOSCALE }, {NOSCALE, ySpace(20), computedJC[0], scrollBar}, {"", operatorJC[0], enteredJC[0], "" }, {"", ySpace(3), null, "" }, {"", xSep(), __, "" }, {"", xSep(), __, "" }, {"", ySpace(20), computedJC[1], "" }, {"", operatorJC[1], enteredJC[1], "" }, {"", ySpace(3), null, "" }, {"", xSep(), __, "" }, {"", xSep(), __, "" }, {"", ySpace(20), computedJC[2], "" }, {"", operatorJC[2], enteredJC[2], "" }, {"", ySpace(3), null, "" }, {"", xSep(), __, "" }, {"", xSep(), __, "" }, {"", ySpace(20), computedJC[3], "" }, {"", operatorJC[3], enteredJC[3], "" }, {"", ySpace(3), null, "" }, {"", xSep(), __, "" }, {"", xSep(), __, "" }, {"", ySpace(20), computedJC[4], "" }, {"", operatorJC[4], enteredJC[4], "" }, {"", ySpace(3), null, "" }, {"", xSep(), __, "" }, {"", xSep(), __, "" }, {"", ySpace(10), null, null}, {"", buttonBar, buttonBar, null} }); setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); jbConnect(computedJC, "computed"); jbConnect(operatorJC, "operator"); jbConnect(enteredJC, "entered"); jbConnect(scrollBar, "scrollBar"); jbConnect(resetButton, "resetButton"); jbConnect(closeButton, "closeButton"); } private void resetCalculator() { for (int i = 0; i < MAX_ENTRIES; i++) { entered[i] = Double.NaN; computed[i] = Double.NaN; operator[i] = "+"; } computed[0] = 0; firstVisibleEntry = 0; maxEnteredEntry = -1; recentlyEnteredEntry = -1; } private double calculateExpression(double x1, String op, double x2) { double result = Double.NaN; if (op.equals("=")) // this operator is a sort of local reset result = x2; else if (Double.isNaN(x1) || Double.isNaN(x2)) result = Double.NaN; else if (op.equals("+")) result = x1 + x2; else if (op.equals("-")) result = x1 - x2; else if (op.equals("*")) result = x1 * x2; else if (op.equals("/")) result = (x2 == 0) ? Double.NaN : (x1 / x2); else if (op.equals("^")) // non-integer powers of negative numbers produce a Not-a-number result result = (x1 < 0 && Math.rint(x2) != x2) ? Double.NaN : Math.pow(x1, x2); return result; } private void recalculateEverything() { for (int i = 0; i <= maxEnteredEntry; i++) computed[i+1] = calculateExpression(computed[i], operator[i], entered[i]); } private String stringify(double x) { String result = NAN_STRING; if (!Double.isNaN(x)) result = Double.toString(x); return result; } public String getEntered(int iRow) { String result = stringify(entered[firstVisibleEntry + iRow]); return result; } public boolean getEnteredEnabled(int iRow) { return (firstVisibleEntry + iRow <= maxEnteredEntry+1); } // very last entry is disabled--last entry only there to show calculated result public boolean getEnteredEditable(int iRow) { return firstVisibleEntry + iRow < MAX_ENTRIES-1; } public void setEntered(int iRow, String value) { entered[firstVisibleEntry+iRow] = value.equals(NAN_STRING)? Double.NaN : Double.parseDouble(value); recentlyEnteredEntry = firstVisibleEntry + iRow; maxEnteredEntry = Math.max(maxEnteredEntry, firstVisibleEntry + iRow); // implicit scrolling feature; assumes user wants to scroll down // after entering last visible row. Moves to operator on last visible row if (iRow + 1 == VISIBLE_ENTRIES && firstVisibleEntry < MAX_ENTRIES-VISIBLE_ENTRIES) { firstVisibleEntry = firstVisibleEntry+1; ((JComboBox) operatorJC[iRow]).requestFocus(); } recalculateEverything(); } public String getEnteredInvalidDataMessage(int iRow, String value) { String result = JComponentBreadboard.DATA_IS_VALID; try { double dummy = Double.parseDouble(value); } catch (NumberFormatException e) { result = "Your entry, \"" + value + "\" is not a valid number, as required.\n\n" + "You may correct your entry below, or click cancel to revert to the previous value."; } return result; } public String getComputed(int iRow) { String result = stringify(computed[firstVisibleEntry + iRow]); return result; } // does nothing because field isn't editable (must exist or exception raised) public void setComputed(int iRow, String value) { } public boolean getComputedEnabled(int iRow) { return firstVisibleEntry + iRow <= maxEnteredEntry+1; } public boolean getComputedEditable(int iRow) { return false; } public String getOperator(int iRow) { return operator[firstVisibleEntry+iRow]; } public void setOperator(int iRow, String value) { operator[firstVisibleEntry+iRow] = value; if (firstVisibleEntry + iRow >= maxEnteredEntry) { // operator selected at the end of the list becomes the new default for (int i=firstVisibleEntry + iRow+1; i < MAX_ENTRIES; i++) operator[i] = value; } recalculateEverything(); } public boolean getOperatorEnabled(int iRow) { return getEnteredEditable(iRow); } public int getScrollBar() {return firstVisibleEntry;} public void setScrollBar(int value) { firstVisibleEntry = value; } public int getScrollBarMinimum() { return 0; } public int getScrollBarMaximum() {return Math.min(MAX_ENTRIES, maxEnteredEntry+2);} public int getScrollBarUnitIncrement() {return 1;} public int getScrollBarBlockIncrement() {return VISIBLE_ENTRIES-1;} public int getScrollBarVisibleAmount() {return VISIBLE_ENTRIES;} public void setResetButton() { resetCalculator(); } public void setCloseButton() { jbReturn(); } public static void main(String[] args) { showMessageBreadboard(null, new ScrollingCalculator(), "Scrolling Calculator"); } }
This example implements a mock-up of the Firefox browser's Print Preview and Page Setup dialogs:
The code from this example illustrates the following useful JComponentBreadboard idioms:
This is a large, complex, form, and it isn't particularly easy to understand its code in an absolute sense. However, both the screen layout and the behaviours are composed, in a straightforward manner, from a series of simple, easily understood, parts. For this reason, though it will require a significant amount of your time to fully understand this form, it will not require very much of your intelligence.
import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.KeyEvent; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; import net.sourceforge.jcomponentbreadboard.JComponentBreadboard; public class FirefoxPrintPreview extends JComponentBreadboard { // Simulates that part of Firefox that holds the user's printer page setup preferences. static class FirefoxPageSetupData implements Cloneable { final static int PORTRAIT = 0; final static int LANDSCAPE = 1; public int orientation = PORTRAIT; public int scale = 100; public boolean shrinkToFitPageWidth = false; public boolean printBackground = false; public double topMargin = 0.0; public double leftMargin = 0.5; public double bottomMargin = 0.0; public double rightMargin = 0.5; public String leftHeader = "--blank--"; public String centerHeader = "--blank--"; public String rightHeader = "--blank--"; public String leftFooter = "--blank--"; public String centerFooter = "--blank--"; public String rightFooter = "--blank--"; public String customLeftHeader = ""; public String customCenterHeader = ""; public String customRightHeader = ""; public String customLeftFooter = ""; public String customCenterFooter = ""; public String customRightFooter = ""; public static String[] headerChoices = { "--blank--", "Title", "URL", "Date/Time", "Page #", "Page # of #", "Custom..."}; public Object clone() { try { Object result = super.clone(); return result; } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } } } private FirefoxPageSetupData pageSetupData; final String SHRINK_TO_FIT_SCALE = "Shrink To Fit"; final String CUSTOM_SCALE = "Custom..."; final int N_PORTRAIT_PAGES = 9; final int N_PORTRAIT_LINES_PER_PAGE = 60; final int N_LANDSCAPE_LINES_PER_PAGE = 40; final int N_PORTRAIT_LINES = N_PORTRAIT_LINES_PER_PAGE*N_PORTRAIT_PAGES; final int N_VISIBLE_LINES = 30; final int N_VISIBLE_COLS = 80; private int firstVisibleLine = 0; private JScrollPane scrollPane; private String[] scaleDropdownList = { "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%", "125%", "150%", "175%", "200%", SHRINK_TO_FIT_SCALE, CUSTOM_SCALE}; private boolean isOnScaleDropdownList(String value) { boolean result = false; for (int i = 0; i < scaleDropdownList.length; i++) if (scaleDropdownList[i].equals(value)) { result = true; break; } return result; } public FirefoxPrintPreview(FirefoxPageSetupData pageSetupData) { this.pageSetupData = pageSetupData; JButton print = (JButton) xFill(new JButton("Print...")); print.setMnemonic(KeyEvent.VK_P); JButton pageSetup = (JButton) xFill(new JButton("Page Setup...")); pageSetup.setMnemonic(KeyEvent.VK_U); JLabel pageLbl = new JLabel("Page:"); JButton firstPage = (JButton) yFill(new JButton("|<")); firstPage.setMargin(new Insets(0,0,0,0)); JButton previousPage = (JButton) yFill(new JButton(" <")); previousPage.setMargin(new Insets(0,0,0,0)); JComponent page = new JTextField(6); JButton nextPage = (JButton) yFill(new JButton("> ")); nextPage.setMargin(new Insets(0,0,0,0)); JButton lastPage = (JButton) yFill(new JButton(">|")); lastPage.setMargin(new Insets(0,0,0,0)); JLabel scaleLbl = new JLabel("Scale:"); JComboBox scale = new JComboBox(scaleDropdownList); scale.setMaximumRowCount(scaleDropdownList.length); JRadioButton[] orientation = { new JRadioButton("Portrait"), new JRadioButton("Landscape") }; orientation[0].setMnemonic(KeyEvent.VK_O); orientation[1].setMnemonic(KeyEvent.VK_L); JComponent close = xFill(new JButton("Close")); JTextArea simulatedPages = new JTextArea(N_VISIBLE_LINES,N_VISIBLE_COLS); simulatedPages.setEditable(false); scrollPane = (JScrollPane) xyFill(new JScrollPane(simulatedPages, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS)); JComponent buttonBar = xFill(new JComponentBreadboard(new Object[][] { {null, NOSCALE, __, __, __, BISCALE, NOSCALE, __, __, __, __, __, BISCALE, NOSCALE, __, BISCALE, NOSCALE, __, BISCALE, NOSCALE}, {NOSCALE, null, xSpace(100), null, xSpace(100), null, null, null, null, null,null, null, null, null, null, null, null, null, null, xSpace(100)}, {"", xSpace(5), print, xSpace(5), pageSetup,xSpace(5),pageLbl,firstPage, previousPage,page,nextPage,lastPage, xSpace(5), scaleLbl, scale, xSpace(5), orientation, orientation,xSpace(5), close} })); setBreadboard(new Object[][] { {null, EXPANDS}, {NOSCALE, ySpace(5)}, {NOSCALE, buttonBar}, {NOSCALE, ySpace(5)}, {EXPANDS, scrollPane} }); jbConnect(print, "print"); jbConnect(pageSetup, "pageSetup"); jbConnect(firstPage, "firstPage"); jbConnect(previousPage, "previousPage"); jbConnect(page, "page"); jbConnect(nextPage, "nextPage"); jbConnect(lastPage, "lastPage"); jbConnect(scale, "scale"); jbConnect(orientation, "orientation"); jbConnect(close, "close"); jbConnect(CLOSE_BUTTON, "close"); jbConnect(simulatedPages, "simulatedPages"); jbConnect(scrollPane.getVerticalScrollBar(), "VScrollPosition"); } // our screen shot grabbing script requires this no-arg constructor: public FirefoxPrintPreview() { this(new FirefoxPageSetupData()); } // this and the next method just generate phony data so we have something to scroll, // and so that something changes when they switch from portrait to landscape. private String simPortraitLine(int portraitLine) { final int N_PORTRAIT_CHUNKS = 25; String chunk = ((portraitLine/100) % 10) + "" + ((portraitLine/10) % 10) + "" + (portraitLine % 10); String result = ""; for (int i = 0; i < N_PORTRAIT_CHUNKS; i++) result += chunk; return result; } static String portraitString = null; static String landscapeString = null; private String getSimulatedPages(int orientation) { String result = ""; if (FirefoxPageSetupData.PORTRAIT == orientation) { if (null == portraitString) { for (int i = 0; i < N_PORTRAIT_LINES; i++) result += simPortraitLine(i) + "\n"; portraitString = result; } result = portraitString; } else { if (null == landscapeString) { for (int i = 0; i < N_PORTRAIT_LINES; i+=2) result += simPortraitLine(i) + simPortraitLine(i+1) + "\n"; landscapeString = result; } result = landscapeString; } return result; } public void setPrint() { JOptionPane.showMessageDialog(this, "I didn't implement a Print dialog, so you will have to imagine one popping up at this point"); } public void setPageSetup() { pageSetupData = (FirefoxPageSetupData) showInputBreadboard(this, new FirefoxPageSetup(pageSetupData), "Page Setup"); } public void setFirstPage() { firstVisibleLine = 0; } private int getLinesPerPage() { int result; if (FirefoxPageSetupData.PORTRAIT == pageSetupData.orientation) result = N_PORTRAIT_LINES_PER_PAGE; else result = N_LANDSCAPE_LINES_PER_PAGE; return result; } public void setPreviousPage() { firstVisibleLine = Math.max(0, firstVisibleLine - getLinesPerPage()); } public void setNextPage() { firstVisibleLine = Math.min(N_PORTRAIT_LINES-getLinesPerPage(), firstVisibleLine + getLinesPerPage()); } public String getPage() { int pageNum = 1 + firstVisibleLine/getLinesPerPage(); return Integer.toString(pageNum); } public void setPage(String value) { int pageNum = Integer.parseInt(value); firstVisibleLine = Math.max(0, Math.min((pageNum-1) * getLinesPerPage(), N_PORTRAIT_LINES-getLinesPerPage())); } public String getPageInvalidDataMessage(String value) { String result = value.matches("[0-9]+") ? JComponentBreadboard.DATA_IS_VALID : JComponentBreadboard.REVERT_AND_BEEP; return result; } public void setLastPage() { firstVisibleLine = Math.max(0, N_PORTRAIT_LINES-getLinesPerPage()); } public String getScale() { String result = pageSetupData.scale + "%"; if (pageSetupData.shrinkToFitPageWidth) result = SHRINK_TO_FIT_SCALE; else if (!isOnScaleDropdownList(result)) result = CUSTOM_SCALE; return result; } public void setScale(String value) { if (value.equals(SHRINK_TO_FIT_SCALE)) pageSetupData.shrinkToFitPageWidth = true; else if (value.equals(CUSTOM_SCALE)) { Object customScale = JOptionPane.showInputDialog(this, "<html><b>Enter a percentage scale factor of at least 10 and at most 999:", "Custom...", JOptionPane.INFORMATION_MESSAGE, null, null, Integer.toString(pageSetupData.scale)); if (customScale instanceof String) { if (((String) customScale).matches(" *[1-9][0-9] *") || ((String) customScale).matches(" *[1-9][0-9][0-9] *")) { pageSetupData.scale = Integer.parseInt((String) customScale); pageSetupData.shrinkToFitPageWidth = false; } else { Toolkit.getDefaultToolkit().beep(); } } } else { pageSetupData.scale = Integer.parseInt(value.substring(0,value.length()-"%".length())); pageSetupData.shrinkToFitPageWidth = false; } } // An array of radio buttons, connected and using the idiom below, is the easiest way // to handle radio button groups (works better than a ButtonGroup). This approach // can also handle groups that have unusual selection requirements (like being able // to deselect everything, or being able to select more than one button) without // much additional work. public boolean getOrientation(int iRow) { return pageSetupData.orientation == iRow; } public void setOrientation(int iRow, boolean value) { firstVisibleLine = 0; pageSetupData.orientation = iRow; } public void setClose() { jbReturn(); } public String getSimulatedPages() { String result = getSimulatedPages(pageSetupData.orientation); return result; } public void setSimulatedPages(String value) { // scrollable page field isn't editable, so set does nothing } public int getVScrollPosition() { return (firstVisibleLine * scrollPane.getVerticalScrollBar().getMaximum())/ N_PORTRAIT_LINES; } public void setVScrollPosition(int value) { firstVisibleLine = ((N_PORTRAIT_LINES+1) * value)/ scrollPane.getVerticalScrollBar().getMaximum(); } // Implements the printer page setup popup dialog (portrait/landscape, margins, etc.) public class FirefoxPageSetup extends JComponentBreadboard { final double PAGE_WIDTH = 8.5; // in inches final double PAGE_HEIGHT = 11.0; final double PAGE_XPIXELS_PER_INCH = 12; // determined via trial and error final double PAGE_YPIXELS_PER_INCH = 10; // until it looked OK on my screen. private FirefoxPageSetupData modifiedData; private FirefoxPageSetupData originalData; // applies the font used throughout this form to various objects private Font f = new Font("SansSerif", Font.PLAIN, 18); private JComponent f(JComponent jc) { jc.setFont(f); return jc; } private TitledBorder f(TitledBorder tb) { tb.setTitleFont(f); return tb; } public FirefoxPageSetup(FirefoxPageSetupData pageSetupData) { originalData = pageSetupData; this.modifiedData = (FirefoxPageSetupData) pageSetupData.clone(); // beginning of panel added to the first tab on the tabbed pane: JComponent[] orientation = { f(new JRadioButton("Portrait")), f(new JRadioButton("Landscape")), }; JComponent orientationGrp = new JComponentBreadboard(new Object[][] { {f(new JLabel("Orientation: ")), orientation, __}, }); JComponent scale = f(new JSpinner(new SpinnerNumberModel(100, 1, 200, 1))); JComponent shrinkToFit = f(new JCheckBox("Shrink To Fit Page Width")); JComponentBreadboard scaleGrp = new JComponentBreadboard(new Object[][] { {f(new JLabel("Scale: ")), scale, f(new JLabel(" % ")), shrinkToFit}, }); JComponent formatGrp = new JComponentBreadboard(new Object[][] { {xySpace(10,10),null, null}, {null, orientationGrp, null}, {ySpace(10), null, null}, {null, scaleGrp, null}, {null, null, xySpace(10,10)} }); formatGrp.setBorder(f(BorderFactory.createTitledBorder("Format"))); JComponent printBackground = f(new JCheckBox("Print Background (color and images)")); JComponent optionsGrp = new JComponentBreadboard(new Object[][] { {xySpace(10,10), null, null}, {null, printBackground, null}, {null, null, xySpace(10,10)} }); optionsGrp.setBorder(f(BorderFactory.createTitledBorder("Options"))); JComponent formatAndOptionsForm = xFill(new JComponentBreadboard(new Object[][] { {xySpace(10,10), null, null}, {null, xFill(formatGrp), null}, {null, xFill(optionsGrp), null}, {null, null, xySpace(10,10)} })); // beginning of panel added to the second tab on the tabbed pane JComponent marginViewer = new JPanel(); // margin viewer JComponent left = xUpsize(10,f(new JSpinner(new SpinnerNumberModel(0, 0, 4, 0.1)))); JComponent top = xUpsize(10,f(new JSpinner(new SpinnerNumberModel(0, 0, 4, 0.1)))); JComponent right = xUpsize(10,f(new JSpinner(new SpinnerNumberModel(0, 0, 4, 0.1)))); JComponent bottom = xUpsize(10, f(new JSpinner(new SpinnerNumberModel(0, 0, 4, 0.1)))); JComponent leftLbl = f(new JLabel("Left: ")); JComponent rightLbl = f(new JLabel("Right: ")); JComponent topLbl = f(new JLabel("Top: ")); JComponent bottomLbl = f(new JLabel("Bottom: ")); // put them in groups so the group as a whole can be centered JComponent leftGrp = yAlign(0.5, new JComponentBreadboard(new Object[][] { {leftLbl}, {left} })); JComponent topGrp = xAlign(0.5, new JComponentBreadboard(new Object[][] { {topLbl, top}, })); JComponent rightGrp = yAlign(0.5, new JComponentBreadboard(new Object[][] { {rightLbl}, {right} })); JComponent bottomGrp = xAlign(0.5, new JComponentBreadboard(new Object[][] { {bottomLbl, bottom}, })); JComponent marginsGrp = new JComponentBreadboard(new Object[][] { {null, BISCALE, NOSCALE, __, __, __, BISCALE}, {BISCALE, ySpace(1), __, __, __, __, __}, {NOSCALE, xSpace(1), null, null, null, null, xSpace(1)}, {"", null, null, topGrp, __, null, null}, {"", "", leftGrp, marginViewer, __, rightGrp, null}, {"", "", "", "", "", "", null}, {"", "", null, bottomGrp, __, null, null}, {BISCALE, ySpace(1), __, __, __, __, __}, }); TitledBorder titledBorder = f(BorderFactory.createTitledBorder("Margins (inches)")); marginsGrp.setBorder(titledBorder); JComponent leftHeader = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent centerHeader = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent rightHeader = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent leftFooter = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent centerFooter = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent rightFooter = f(new JComboBox(FirefoxPageSetupData.headerChoices)); JComponent leftHFLbl = f(xAlign(0.5,new JLabel("Left:"))); JComponent centerHFLbl = f(xAlign(0.5, new JLabel("Center:"))); JComponent rightHFLbl = f(xAlign(0.5, new JLabel("Right:"))); JComponent headerFooterGrp = new JComponentBreadboard(new Object[][] { {null, EXPANDS, __, __ }, {NOSCALE, leftHeader, centerHeader, rightHeader}, {"", leftHFLbl, centerHFLbl, rightHFLbl}, {"", leftFooter, centerFooter, rightFooter}, }); headerFooterGrp.setBorder(f(BorderFactory.createTitledBorder("Headers & Footers"))); // Compute height large enough to contain marginViewer assemblage at either // orientation (to keep overall height rigid as orientation changes) final int H = (int) (Math.max(PAGE_HEIGHT, PAGE_WIDTH) * PAGE_YPIXELS_PER_INCH) + topGrp.getPreferredSize().height + bottomGrp.getPreferredSize().height + titledBorder.getBorderInsets(marginsGrp).top + titledBorder.getBorderInsets(marginsGrp).bottom + 10; JComponent marginsAndHeaderFooterForm = new JComponentBreadboard(new Object[][] { {null, NOSCALE, EXPANDS, NOSCALE}, {NOSCALE, xySpace(10,10), null, null}, {"", ySpace(H), xyFill(marginsGrp), null}, {"", ySpace(10), null, null}, {"", null, xFill(headerFooterGrp), null}, {"", null, null, xySpace(10,10)} }); // "shared" button bar beneath the tabbed pane: JComponent ok = f(xFill(new JButton("OK"))); JComponent cancel = f(xFill(new JButton("Cancel"))); JComponent buttonBar = new JComponentBreadboard(new Object[][] { {null, BISCALE, NOSCALE, __, __ }, {NOSCALE, ySpace(20), xSpace(100), null, xSpace(100)}, {"", xSpace(1), ok, xSpace(10), cancel}, }); // assemble the two panels as first,second tabs of the tabbed pane JComponent tabbedPane = f(new JTabbedPane()); tabbedPane.add("Format & Options", formatAndOptionsForm); tabbedPane.add("Margins & Header/Footer", marginsAndHeaderFooterForm); setBreadboard(new Object[][] { {tabbedPane}, {xFill(buttonBar)} }); setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); jbConnect(orientation, "orientation"); jbConnect(scale, "scale"); jbConnect(shrinkToFit, "shrinkToFit"); jbConnect(printBackground, "printBackground"); jbConnect(marginViewer, "marginViewer"); jbConnect(left, "left"); jbConnect(top, "top"); jbConnect(right, "right"); jbConnect(bottom, "bottom"); jbConnect(leftHeader, "leftHeader"); jbConnect(centerHeader, "centerHeader"); jbConnect(rightHeader, "rightHeader"); jbConnect(leftFooter, "leftFooter"); jbConnect(centerFooter, "centerFooter"); jbConnect(rightFooter, "rightFooter"); jbConnect(cancel, "cancel"); jbConnect(KeyStroke.getKeyStroke("ESCAPE"), "cancel"); jbConnect(ok, "ok"); jbConnect(CLOSE_BUTTON, "ok"); } public boolean getOrientation(int iRow) { return (modifiedData.orientation == iRow); } public void setOrientation(int iRow, boolean value) { firstVisibleLine = 0; modifiedData.orientation = iRow; } public int getScale() { return modifiedData.scale; } public void setScale(int value) { modifiedData.scale = value; } public boolean getShrinkToFit() { return modifiedData.shrinkToFitPageWidth; } public void setShrinkToFit(boolean value) { modifiedData.shrinkToFitPageWidth = value; } public boolean getPrintBackground() {return modifiedData.printBackground;} public void setPrintBackground(boolean value) {modifiedData.printBackground = value;} private double pageWidth() { if (FirefoxPageSetupData.LANDSCAPE == modifiedData.orientation) return PAGE_HEIGHT; else return PAGE_WIDTH; } private double pageHeight() { if (FirefoxPageSetupData.LANDSCAPE == modifiedData.orientation) return PAGE_WIDTH; else return PAGE_HEIGHT; } public Dimension getMarginViewerPreferredSize() { Dimension result = new Dimension((int) (PAGE_XPIXELS_PER_INCH*pageWidth()), (int) (PAGE_YPIXELS_PER_INCH*pageHeight())); return result; } public Border getMarginViewerBorder() { Border line = LineBorder.createBlackLineBorder(); Border margin = BorderFactory.createEmptyBorder( (int) (PAGE_YPIXELS_PER_INCH*modifiedData.topMargin), (int) (PAGE_XPIXELS_PER_INCH*modifiedData.leftMargin), (int) (PAGE_YPIXELS_PER_INCH * modifiedData.bottomMargin), (int) (PAGE_XPIXELS_PER_INCH*modifiedData.rightMargin)); Border result = BorderFactory.createCompoundBorder(margin, line); return result; } public Color getMarginViewerBackground() {return Color.WHITE;} public double getLeft() {return modifiedData.leftMargin;} public void setLeft(double value) {modifiedData.leftMargin=value;} public double getRight() {return modifiedData.rightMargin;} public void setRight(double value) {modifiedData.rightMargin=value;} public double getTop() {return modifiedData.topMargin;} public void setTop(double value) {modifiedData.topMargin=value;} public double getBottom() {return modifiedData.bottomMargin;} public void setBottom(double value) {modifiedData.bottomMargin=value;} // if the hfSelection is "Custom..." prompts user to enter new custom header/footer private String maybeGetCustomHeaderFooter(String hfSelection, String prevCustomHeaderFooter) { String result = prevCustomHeaderFooter; if (hfSelection.equals("Custom...")) { Object obj = JOptionPane.showInputDialog(this, "<html><h1>Enter your custom header/footer text", "Custom...", JOptionPane.INFORMATION_MESSAGE, null, null, prevCustomHeaderFooter); if (obj instanceof String) result = (String) obj; // else user clicked cancel, leave unchanged. } return result; } public String getLeftHeader() {return modifiedData.leftHeader;} public void setLeftHeader(String value) { modifiedData.leftHeader=value; modifiedData.customLeftHeader = maybeGetCustomHeaderFooter( value, modifiedData.customLeftHeader); } public String getLeftHeaderToolTipText() {return "Left header";} public String getCenterHeader() {return modifiedData.centerHeader;} public void setCenterHeader(String value) { modifiedData.centerHeader=value; modifiedData.customCenterHeader = maybeGetCustomHeaderFooter( value, modifiedData.customCenterHeader); } public String getCenterHeaderToolTipText() {return "Center header";} public String getRightHeader() {return modifiedData.rightHeader;} public void setRightHeader(String value) { modifiedData.rightHeader=value; modifiedData.customRightHeader = maybeGetCustomHeaderFooter( value, modifiedData.customRightHeader); } public String getRightHeaderToolTipText() {return "Right header";} public String getLeftFooter() {return modifiedData.leftFooter;} public void setLeftFooter(String value) { modifiedData.leftFooter=value; modifiedData.customLeftFooter = maybeGetCustomHeaderFooter( value, modifiedData.customLeftFooter); } public String getLeftFooterToolTipText() {return "Left footer";} public String getCenterFooter() {return modifiedData.centerFooter;} public void setCenterFooter(String value) { modifiedData.centerFooter=value; modifiedData.customCenterFooter = maybeGetCustomHeaderFooter( value, modifiedData.customCenterFooter); } public String getCenterFooterToolTipText() {return "Center footer";} public String getRightFooter() {return modifiedData.rightFooter;} public void setRightFooter(String value) { modifiedData.rightFooter=value; modifiedData.customRightFooter = maybeGetCustomHeaderFooter( value, modifiedData.customRightFooter); } public String getRightFooterToolTipText() {return "Right footer";} public void setCancel() { jbReturn(originalData); } public void setOk() { jbReturn(modifiedData); } public boolean getOkIsDefaultButton() {return true;} } public static void main(String[] args) { FirefoxPageSetupData pageSetupData = new FirefoxPageSetupData(); showMessageBreadboard(null,new FirefoxPrintPreview(pageSetupData), "Print Preview"); } }
|
Download JComponentBreadboard from its Sourceforge Project Page |
||||||
PREV PACKAGE NEXT PACKAGE | FRAMES NO FRAMES |