Download JComponentBreadboard
from its Sourceforge Project Page

Package net.sourceforge.jcomponentbreadboard

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:

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.
 

Package net.sourceforge.jcomponentbreadboard Description

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:

  1. Like snapping an electronic component onto a plastic grid, you define each JComponent's relative position within the form by assigning it to a rectangular block of elements within a 2-D breadboard array. The rows and columns of this grid auto-scale to fit components at their preferred sizes, and it's easy to specify which rows and columns will stretch or contract to incorporate any space surpluses or deficits in the parent window that contains the form.

  2. Like wiring together electronic components on a breadboard, you use the jbConnect method to connect the main, user manipulable, property of each JComponent to associated application properties. Connect single components to individual getter/setter defined properties (JCheckBox <==> boolean), or directly plug arrays of components into row/column indexed properties. Such connected components enjoy pluggable auxiliary properties (enabled, visible, etc.) and simplified data validation and progress/cancel feedback.

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

  1. Preface: GridBag vs. Breadboard, A Code-to-Code Comparison
  2. 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:

    1. How long does it take you to understand the relationship between the JComponentBreadboard-based implementation and the two screen shots below?

    2. How long does it take you to understand the relationship between the original JDK implementation and the same two screen shots?

    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!

  3. HelloWorld and Friends: Simple Instructive JComponentBreadboard-based Forms
    1. HelloWorld
    2. Running HelloWorld: Incorporating JComponentBreadboard into Your Java Desktop Application
      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 HelloWorld
        
        

      However, 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:

      // 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");
         }
      
      }
      

      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:

      // 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);
         }
      
      }
      

      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.

    3. HelloWorldPlus
    4. 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:

        

      // 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");
         }
      
      }
      

      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:

      1. JComponentBreadboard will automatically call the setUseExpectedPhrase method each time the user changes the state of the checkbox, passing in true when the checkbox has been checked, and false when it has been unchecked.

      2. Whenever the user changes the state of any other connected JComponent on the form, JComponentBreadboard will automatically check the checkbox whenever the getUseExpectedPhrase method returns true, and uncheck it when it returns false.

      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:

       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 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.

      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.

      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.

    5. NumericInputDialog
    6. 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.

      For a more advanced application of this same basic strategy, check out our DateChooser example application.

      A screen shot from this application, as well as its complete source code, is shown below:

      // 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);  
         }
         
      }
      

      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:

      public String getXxxInvalidDataMessage(String inputToValidate) {...}
       

      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:

      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".

      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.

      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.

      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.

      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.

      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:

      1. Note how the standard Swing methods, setPreferredSize, setAlignmentX, and setBorder are used, just as an experienced Swing user would expect, to make the OK button the same size as the Cancel button, to center the button bar horizontally, and to create a 10 pixel border around the form as a whole.

        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.

      2. The xSpace and ySpace methods create and return a zero height JPanel of a specified width, and a zero-width JPanel of the specified height. These JComponentBreadboard static convenience methods are handy to introduce horizontal or vertical gaps of an explicit pixel size into a form's layout. In this example, they create a 10 pixel horizontal gap between the OK and Cancel buttons, and a 10 pixel vertical gap between the entry field and the button bar.

      3. Note the use of a layout-only JComponentBreadboard to define the relative positions of the OK and Cancel buttons within the button bar. Thanks to such a sub-breadboard, the OK and Cancel buttons can be centered independently of the parent grid (if placed directly within the parent grid, their horizontal positions would be individually constrained by that grid, so it would be much harder to center them properly as a group).

        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.

      4. Despite being placed in a sub-breadboard, the OK and Cancel buttons are connected to the parent breadboard just as the number input field is. JComponentBreadboard packages layout and connection related aspects of a form together into a single class so that you can quickly access, in one place, the layout and connection specifications for your form. Nonetheless, in accord with Swing's orthogonal design, you can freely and independently modify the layout and connnection related aspects of your form, as this example illustrates.

      5. Note how the buttonBar is placed in both cells of the second row to assure that it gets centered horizontally across the form as a whole. In general (just as with the JDK's GridBagLayout) you define each component's relative location by assigning it to a single, non-overlapping rectangular sub-block of the 2-D "breadboard" array.

    7. CelsiusFahrenheitConverter
    8. It's like a poor man's parenthesized reverse-Polish packer!
                 -- Jay Seage, commenting on the layout attribute functions discussed in this section

      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:

      // 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");
        }
         
      }
      

      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.

      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.

      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:

      DirectiveScales down if parent
      is too small to fit
      all components into the grid
      at their preferred sizes
      Scales 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

      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:

      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).

      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.

      RandomWalks

      A journey of a thousand steps begins with a single step of a million step random walk.
                 -- Albert E. Brown

      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:

      // 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");
         }
      }
      

      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).

      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.

      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 jbRun method's javadocs.

  4. Case Studies: Real-World JComponentBreadboard Forms
  5. 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.

    1. FirefoxClearPrivateDataForm
    2. 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:

      1. Passing a reference to an "application model object" (in this case, the "Private Data management system" of Firefox) into the constructor of the form, and letting the form modify that object's properties directly in its setter/getter methods.

      2. Implementing a getXxxEnabled method (in the optional part of the injected interface of every connectable JComponent) to disable and enable the various checkboxes and buttons so as to appropriately reflect the state of the application model object being manipulated via the form.

      // 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;
             }
         }
      }
      

    3. DateChooser
    4. This application implements a modal dialog that lets the user select a date via what has become (with minor variations) a standard, monthly-calendar-style, interface.

      As you review the application's code below, keep an eye out for the following useful techniques:

      1. How the connected, 2-D array of JToggleButtons allows succinct and clear defining equations between row and column positions on the monthly grid, and things like the day label and selection status of the toggle buttons.
      2. How the use of the row ditto ("") and column ditto (__) makes the breadboard array, which contains over 50, mostly repeated, elements, more readable.
      3. How connecting to the LEFT, RIGHT, UP, and DOWN KeyStrokes supports arrow key navigation within the monthly calendar region.
      4. How the use of getter/setter functions (associated with connected components) that operate on the same underlying JDK Calendar object automatically synchronizes: 1) the day spinner and the selected day-of-month toggle button, and 2) the month slider and the month drop-down combobox list.
      5. The use of the EXPANDS scaling directive to support "blowing up" the size of the monthly calendar part of the interface whenever the user resizes the parent JDialog that contains it.
      6. The use of the BISCALE directive on an empty (xSpace) component within the pentultimate column of the day-month-year selection bar to assure that extra space in the parent gets placed in this middle, "slack" component. This assures that the month selector combobox is always flush against the left edge of the form, and that the date label is always flush right.
      7. How the monthly slider's "numbered month" labels get defined via the getMonthSliderLabelTable method.

      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);
         }
         
      }
      

    5. ScrollingCalculator
    6. 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:

      1. The values displayed in the components are offset by an integer that is bound to the scrollbar. Essentially, this allows the data arrays that hold the user's numeric entries and numerical operation selections to be "scrolled through" the form, a technique that is frequently useful.

      2. Note how three arrays are connected, therebye providing the kind of indexed getters and setters that we require. However, the specific positioning of these arrays on the form itself is not sequential, but rather interleaved. Although in many applications (like the DateChooser of the last section) the connection indexing will correspond directly to the positional indexing on the breadboard, this is not required, and this ability to independently specify the position of each component within the breadboard array, regardless of how it is connected, is frequently useful.

      3. JComponentBreadboard does not provide any simplifying wrapper around the standard Swing focus management system--which, if you stick with the built-in defaults or slight variations thereof, is already very easy to use. This application uses the JDKs setFocusTraversalKeys in the standard Swing way to slightly modify those defaults so as to allow the RETURN key to function like TAB.

      // 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");   
         }  
      }
      

    7. FirefoxPrintPreview
    8. 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:

      1. The Page Setup dialog employs a JTabbedPane with two tabs. The contents of each tab are defined via a JComponentBreadboard. It's quite feasable to build an entire application whose user interface consists mainly of a single JTabbedPane, each tab of which is defined by a JComponentBreadboard.

      2. The scrollbar contained within a JScrollPane can be connected via jbConnect which allows the the JScollPane's scroll position to be synchonized with other ways of viewing the scroll position (specifically, in this example, with the page number displayed within a JTextField).

      3. For the Portrait/Landscape radio buttons, a connected 1-D array of radio buttons, along with JComponentBreadboard's array connection conventions, is used in lieu of the conventional Swing ButtonGroup-based approach. Although the standard ButtonGroup approach still works, the array connection approach is more succinct and (as it turns out) a lot more versatile. For example, it's easy to support the ability to select nothing, or, with a bit more work, the ability to select "up to m out of N" radio buttons (or checkboxes).

      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"); 
         }
      }
      

Version:
1.0.1 (requires JDK 1.5 or higher)
Author:
John C. Gunther

Download JComponentBreadboard
from its Sourceforge Project Page

SourceForge.net Logo