by Allen Holub

User interfaces for object-oriented systems, Part 3

news
Oct 1, 199931 mins

The incredible transmogrifying widget

This month’s Java Toolbox continues the object-oriented design/GUI theme by looking at a Collection wrapper that makes it easy to present a visual proxy, as I described in the September Java Toolbox. The wrapper, called a Bag, works just like a Collection — it implements the java.util.Collection interface. It can, however, create a visual proxy for itself when asked.

Build user interfaces for object-oriented systems: Read the whole series!

  • Part 1. What is an object? The theory behind building object-oriented user interfaces
  • Part 2. The visual-proxy architecture
  • Part 3. The incredible transmogrifying widget
  • Part 4. Menu negotiation
  • Part 5. Useful stuff
  • Part 6. An RPN-calculator application demonstrates object-oriented UI principles

The associated proxy can display the list of elements as a list; a button that, when pressed, pops up a frame containing the list; or a combo box. The display strategy is automatic, controlled by the size of the proxy. The list is used if there’s enough space, the button is used when space is tight, and the combo box is used in an in-between situation. Moreover, several such proxies can simultaneously display the same underlying Collection (presumably, on different screens). When the Collection is changed at the model level — by someone adding or removing a member, for example — all the proxies will redraw themselves to reflect the change. The notion of selection is supported: When a user selects an element from the UI, a model-level object that’s registered as a listener will receive notification. If several proxies are displayed, they can stay in synch with each other. If you select from one, the selections in the others will change as well. (This behavior isn’t required, however.)

You may want to download the code from my Web site (see Resources) and play with this thing a bit to see how it works. Figure 1 illustrates three visual proxies simultaneously displaying the same underlying Collection. The proxy on the left is large enough to have represented itself as a list. The one at the top is a bit smaller, so it has represented itself as a combo box, while the one in the bottom right is smaller still, so it has represented itself as button, which when pressed pops up the frame window that’s shown. (The button is disabled as long as the frame window is visible; it’s re-enabled automatically when you close the frame.) If you resize any of these windows, they’ll transmogrify into one or the other of these three forms as they get larger or smaller. Moreover, if you select a line in any of them, the other two will change to reflect that selection.

This sort of dynamic adaptability is essential when implementing user interfaces using the architecture that I described in the July and September columns (which you should read before proceeding — I assume that you’re familiar with the architecture I presented in those articles). In this architecture, a control layer builds a presentation from visual proxies of attributes that are provided by an abstraction-layer object. The abstraction layer cannot know the context in which the proxy will be asked to display itself, so the proxy must accommodate whatever space is available automatically.

If the proxy wasn’t adaptive in this way, the coupling between the abstraction and presentation layers would be too tight. In an HR application, for example, an employee’s identity attribute might want to display a name, an employee ID, and a photograph. Moreover, it will indeed display all three items when there’s enough space. If space is tight, the proxy might display itself as a button with the employee’s name as the label. If pressed, the button will throw up a frame with the additional information. Adaptability is the key. In any event, all well-written Java applications should make no assumptions about the display environment. The same application that’s runs on the 20-inch monitor on your desktop might also have to run on a Palm Pilot at some juncture.

Unfortunately, most of Java’s built-in widgets don’t work in an adaptable way out of the box. We all wish that the font size on a button’s label would grow as the button grows (if it’s placed in a GridBag or Grid, for example), but that’s not how it works. Instead, you usually end up with minuscule text in on a huge gray button or a button that’s too small for the label to print in its entirety. On the other hand, since most Swing widgets support the notion of a renderer — an object whose job is to draw all or part of the widget — it’s relatively easy to build a set of adaptive widgets on top of Swing. That’s what I’ve done here.

Letting the proxy out of the Bag

Before looking at the implementation, let’s look at how it’s used. Listing 1 shows the test class from the Bag implementation. Notice that a Bag is as easy to use as a Collection. Indeed, it’s just a Collection that can produce its own UI when asked.

The main() method (Listing 1, line 26) creates a Collection by wrapping a LinkedList in a Bag, then adds a few elements to the underlying Collection through the Bag. Note that the Bag is a Collection in a real sense, so you can treat it exactly like one without difficulty (or accessed through a Collection reference). The Bag contains the actual data structure, however. (It’s passed in as a constructor argument.)

The Bag is an example of the Gang-of-Four Decorator pattern (see Resources). A Decorator adds capabilities to the decorated item without using implementation inheritance (extends). The java.io package includes many examples of the Decorator pattern. The BufferedInputStream, for example, decorates another InputStream by adding buffering. You don’t know whether you’re dealing with an actual InputStream or a decorated one when you call read(), however. The BufferedInputStream, therefore, adds the ability to buffer input to a raw InputStream. Similarly, a Bag decorates a Collection in order to add the ability to create a visual proxy on demand.

The Collection object is only wrapped — it’s passed the Bag constructor — and you can access it directly, rather than through the wrapper, if you like. But as is the case with the java.io Decorators, you should use direct access with care. For example, it would be dangerous to directly access an InputStream that was simultaneously accessed via a BufferedInputStream Decorator elsewhere in the program. In the case of a Bag, it’s perfectly safe to examine the wrapped Collection — traverse it with an iterator, look something up in it, and so on — without going through the Bag wrapper. Don’t modify the wrapped Collection directly, however, do it via the Bag wrapper. If you do decide to directly modify the contained Collection (rather than via the Bag object), be aware that none of the exposed proxies will reflect your modifications. On the other hand, you may want such behavior.

As is the case with many Decorators, the Bag supports a few operations over and above the ones defined in the Collection interface. I invoke one of these — addActionListener() — on line 44 to add a listener that reports selected items by printing them onto the console window.

A few lines down, I create (and display) three identical visual proxies by calling create_ui(), which I’ll examine momentarily. Then I enter a while loop that adds whatever lines you type on the console window to the current Collection. (This text will appear in all three of the proxy windows.)

The create_ui() method at the top of the listing mimics the behavior of the control object: It creates a frame, retrieves a visual proxy from the Bag (which it treats as a generic «displayable» object, as described last month), then sets up a listener to handle the window-closing event on the frame and pops the frame into existence.

 

Listing 1 (/src/com/holub/ui/Bag.java): Testing the Bag
1:  <a id="Test" name="Test"> public static class Test</a>
2:      {
3:  <a id="Test.create_ui(Collection)" name="Test.create_ui(Collection)">     public void create_ui( Collection aggregate
)</a>
4:          {
5:              JFrame frame = new JFrame();
6:  
7:              // Get the visual proxy and shove it into the Frame
8:  
9:              User_interface displayable = (User_interface)aggregate;
10:              frame.getContentPane().add(
11:                          displayable.visual_proxy("Attribute", true) );
12:  
13:              // Set up a window-closing handler and pop the frame up.
14:  
15:              frame.addWindowListener
16:                  (   new WindowAdapter()
17:  <a id="Test.windowClosing(WindowEvent)" name="Test.windowClosing(WindowEvent)">                 {   public void windowClosing(
WindowEvent e )</a>
18:                          {   System.exit(0);
19:                          }
20:                      }
21:                  );
22:              frame.pack();
23:              frame.show();
24:          }
25:  
26:  <a id="excerpt.main" name="excerpt.main">     public static void main( String[] args ) throws Exception</a>
27:          {   
28:              Collection aggregate = new Bag( new LinkedList(), "outer" );
29:              aggregate.add("A");
30:              aggregate.add("B");
31:              aggregate.add("C");
32:              aggregate.add("D");
33:  
34:              // You need to treat it as a Bag (as compared to a generic
35:              // Collection) to install an ActionListener, thus the cast.
36:              // It has to be final because it's referenced by the inner-
37:              // class object. Note that the listener will report all the
38:              // selections associated with displaying the initial UI.
39:              // There will be two such notifications (one for the list
40:              // and one for the drop-down) for each of the three proxies.
41:              // If you don't want this behavior, install the listener
42:              // after the visual proxy has been displayed.
43:  
44:  <a id="excerpt.add_listener" name="excerpt.add_listener">         final Bag the_bag = (Bag) aggregate;</a>
45:              the_bag.addActionListener
46:              (   new ActionListener()
47:  <a id="Test.actionPerformed(ActionEvent)" name="Test.actionPerformed(ActionEvent)">             {   public void actionPerformed(
ActionEvent e )</a>
48:                      {   System.out.println( "Selected " + the_bag.top() );
49:                      }
50:                  }
51:              );
52:  
53:              create_ui( aggregate );
54:              create_ui( aggregate );
55:              create_ui( aggregate );
56:  
57:              // Transfer all lines typed on the console to the collection.
58:  
59:              String s;
60:              while( (s = com.holub.tools.Std.in().readLine()) != null )
61:              {   com.holub.tools.Std.out().println( "->" + s );
62:                  aggregate.add( s );
63:              }
64:          }
65:      }
                                       
                                    

The nitty-gritty

Now let’s look at the rest of the Bag.java file, in Listing 2. I’ve sketched out the Bag‘s static model in Figure 2. Starting with the Bag itself in the upper-left corner, a Bag both is a Collection and contains a Collection. That’s one of the things that makes it a Decorator. Bag implements all of the methods of Collection, though the ones that don’t modify the state of the contained object are just pass-through methods — they do nothing but call the method with the same name on the contained Collection object. The Bag also implements the User_interface interface (described in September’s column). In this sense it’s a Gang-of-Four Adapter — it makes a Collection appear to be a User_interface, so any “control” object can use it to build its presentation. To continue with the vocabulary I introduced last month, the Bag is an abstraction-layer object; a control-layer object can ask it to produce a presentation-layer object (a visual proxy) by calling visual_proxy().

Looking below the Bag box in Figure 2, the proxy is implemented as a JPanel derivative called Proxy. Proxy is a private inner class of Bag that implements a public interface (JPanel), so the outside world can access only those features of a Proxy that you’ve defined in this public interface. The other methods shown in the “operations” compartment are for internal communication between the Bag and its proxies.

The Proxy does its work by creating a single Swing model object that can serve as the model for both a Jlist and a JComboBox — it extends AbstractListModel and implements ComboBoxModel. Since both of these UI Delegates share the same physical model object, they will always stay in synch with each other. The Model class, then, serves as an intermediary between the visual side of things and the actual data, stored in the Bag‘s contents field. (It’s a somewhat degraded case of a Gang-of-Four Mediator.) Note that Model is an inner class of an inner class. The Bag doesn’t know or care that it exists, so its definition properly nests inside the Proxy, which does care about it.

The bottom third of Figure 2 shows the actual display objects, which are contained inside the Proxy‘s panel: a JComboBox called drop_down; a JList called, oddly enough, list; and a JButton called button. All three of these exist at once, but only one is visible. The main job of the Proxy, from the perspective of UI, is to pick the correct one of these three objects to display, based on the size of the containing panel. The button, when clicked, creates and pops up an instance of Popup, which does nothing but provide a free-floating container for the same list object.

 

The Collection pass-throughs

The implementation in Listing 2 is, as usual, a straightforward translation of the design. The methods that pass through to the contained Collection start with add(...) (Listing 2, line 96). The methods that modify the Collection’s state come first. All that they do, other than chain through to the equivalent method of the contained Collection, is call changed(...) (Listing 2, line 206), which notifies any existing proxies that the data they’re displaying has changed. This way they can update themselves dynamically.

The Collection methods that don’t modify the state of the underlying Collection object start at Listing 2, line 120. These methods literally do nothing but call their counterparts in the contained object. The designate_top(...) method (Listing 2, line 173) lets you (or the proxy) designate a new “top” item. It takes care of notifying all the action listeners, and it also updates the proxies — in the for loop on line 189. This loop modifies the state of a visible Swing object. But, as I discussed back in February, Swing is not thread safe. Since the Bag might be modified by a random thread, it’s important to play by the rules.

With this in mind, the proper way to instruct a Swing object to modify itself is to post a request — in the form of a Runnable Command object — on the Swing event thread by calling SwingUtilites.invokeLater. Swing will eventually process this request synchronously. (This aspect of Swing is an example of the Active-Object design pattern that I discussed in June.)

You should note, by the way, that the Bag is thread safe with respect to internal communications with Swing, but it’s not externally thread safe for the same reason that the Java.util Collection classes aren’t thread safe: I didn’t want to incur the overhead of synchronization unnecessarily. It’s not safe for two threads to simultaneously call add(), for example. If you want thread safety, you should write a thread-safe wrapper along the lines of the one that’s returned from Collections.synchronizedCollection(). (This method returns a Decorator object that implements synchronized versions of the Collection methods that do nothing but call the similarly named methods in the decorated object.)

The changed(...) method (Listing 2, line 206) notifies the proxies of changes in size of the underlying Collection. It also uses invokeLater() to get Swing to process requests synchronously.

The top() item

Bag extends Collection conceptually in only one way: by introducing the notion of an item at the top of the bag — the one you’ll reach first when you stick your hand in. The top() item is the item the user selected via the list or combo box displayed by the Proxy. You can access the item via the top (Listing 2, line 65) method. (Yea I know, it reeks of being a get function, but it’s the nature of a Bag to be a container of data. That is, accessing the top element is part of the problem-domain description of what a Bag does. At least you can’t modify the element through top().) The Bag also supports the registration of ActionListeners, which are notified whenever a user, working through a proxy, selects a new top item.

The visual proxy

visual_proxy(...) (Listing 2, line 240) creates the visual proxy. Besides instantiating the proxy, it sets up an AncestorListener that adds or removes the proxy object from the proxy list maintained by the Bag. The Proxy object is added to the list when it’s added to its container, and removed from the list when it’s removed from the container (or the container shuts down).

This list manipulation is essential because the reference on the list of proxies will prevent the Proxy object from being garbage collected, even when it’s not displayed. Note that, in the case of a container window that’s simply hidden, rather than destroyed, the container window itself will keep a reference to the Proxy object, thereby preventing it from being destroyed.

The Proxy class itself comes next (Listing 2, line 275). As you can see, it creates the JComboBox, JList, and JButton. And it installs a common renderer and model into them. It also sets up an ActionListener on the button to get the popup to appear on cue, and a ComponentListener on itself (on line 348) to perhaps swap user interfaces when the component changes size.

The UI is actually installed by install_ui() (Listing 2, line 365), which checks the panel size against the preferred size of the combo box, and installs the correct UI accordingly. The combo box is used if there’s enough width and if the height of the panel is less than three times the preferred height of the combo box.

If the panel is smaller, the button is used, if it’s larger, the list is used. Nothing at all happens if the correct UI delegate has already been installed.

The changed(...) method (Listing 2, line 413) makes sure that the indexes on the various controls are set correctly when a new top item is selected. The rest of the proxy methods are self explanatory.

The Model and Renderer

The Model (Listing 2, line 461) and Renderer (Listing 2, line 498) classes come next. They are straightforward implementations, much like the examples you’ll find in any decent Swing book. Notice that the changed(...) method (Listing 2, line 483) must fire off an event to the UI delegate to tell it that the model’s state has changed. This changed() method is called indirectly by the changed() method we looked at earlier (in the Bag’s add() method, for example), which is called when the Collection changes state.

The only interesting part of the Renderer class is the code on lines 522 to 534, where the component whose paint() method is called to render the cell is actually created. If the Collection member implements User_interface, then the visual proxy for that object renders the cell; otherwise, it creates a JLabel to hold the string returned by the Collection element’s toString() method.

 

Listing 2 (/src/com/holub/ui/Bag.java): The Bag Wrapper
1:  package com.holub.ui;
2:  
3:  import java.awt.*;
4:  import java.awt.event.*;
5:  import java.util.*;
6:  
7:  import javax.swing.*;
8:  import javax.swing.event.*;
9:  import javax.swing.border.*;
10:  
11:  import com.holub.ui.AncestorAdapter;
12:  import com.holub.tools.Multicaster;
13:  
14:  /**
15:   *  The Bag is a Collection that supports a notion of a user interface.
16:   *  The Bag can produce a "visual proxy" that will take the form
17:   *  of a JList, JComboBox, or JButton (which displays a dialog when
18:   *  pushed), depending on the amount of screen real estate that's
19:   *  available to it. The proxy decides how to display itself without
20:   *  any outside intervention. Moreover, the "visual proxy" automatically
21:   *  reflects model-level changes such as adding or removing items from the list.
22:   *  For example, when you add or remove an item from a Bag, any proxies
23:   *  that have exposed UIs will update their UIs to indicate the change.
24:   *  
25:   *  

Unlike a Collection, a Bag also has a notion of the "top" element. 26: * (the one that you encounter first when you reach into the bag). 27: * The top element is typically modified by the user selecting 28: * an element in a UI created by a visual proxy. You can find out 29: * when that element changes by registering an ActionListener. You 30: * can also designate an existing bag element as the "top" element 31: * manually by calling {@link Bag#designate_top}. 32: * 33: *

As with standard collections, the Bag is not particularly thread 34: * safe. It does take some trouble to make sure that the list of 35: * <code>ActionListener&#039;s</code> is handled in a thread-safe way, but 36: * that's it. If you want thread safety, you should wrap a <code>Bag</code> 37: * in a thread-safety wrapper of your own devising. You can use the 38: * methods of the <code>Collection</code> class for this purpose if 39: * you like, but only if you do not call any methods of <code>Bag</code> 40: * that are not defined in the <code>Collection</code> interface. 41: * 42: * Though the <code>Bag</code> is not thread safe with respect to 43: * the outside world, it is thread safe with respect to Swing. That is, 44: * it's okay for the <code>Bag</code> to be modified, even if a proxy 45: * is exposing a UI. 46: * 47: *

Modifying the wrapped collection directly, rather than by calling a 48: * Bag method, is dangerous. If you need to modify an element, remove it, 49: * change it, then put it back. 50: * 51: **/ 52: 53: <a id="Bag" name="Bag">public class Bag implements User_interface, Collection</a> 54: { 55: // A bug in the compiler (version 1.2.2 and all prior versions) 56: // erroneously prints the error message "xxx may not have been 57: // initialized," when the following are made final. This bug 58: // is somehow related to the presence of inner classes, but I'd 59: // rather have the inner classes than the immutable fields. 60: 61: <a id="Bag.contents" name="Bag.contents"> private /*final*/ Collection contents;</a> 62: <a id="Bag.sort_strategy" name="Bag.sort_strategy"> private /*final*/ Comparator sort_strategy;</a> 63: <a id="Bag.name" name="Bag.name"> private /*final*/ String name;</a> 64: <a id="Bag.proxies" name="Bag.proxies"> private /*final*/ Vector proxies = new Vector();</a> 65: <a id="Bag.top" name="Bag.top"> private Object top = null;</a> 66: 67: /** Create a bag that uses the indicated Collection to represent 68: * the indicated attribute. 69: */ 70: 71: <a id="Bag.Bag(Collection,String)" name="Bag.Bag(Collection,String)"> public Bag( Collection contents, String name )</a> 72: { this.contents = contents; 73: this.name = name; 74: this.sort_strategy = null; 75: } 76: 77: /** Create a <code>Bag</code> that wraps the <code>Collection</code> 78: * passed in as an argument. When the items in the collection are 79: * displayed, they are first extracted to an array, which is then 80: * sorted using the supplied <code>Comparator</code>. (There's 81: * no point in doing this if the underlying Collection is 82: * already sorted, but it's handy for <code>HashSet</code> objects 83: * and <code>LinkedList</code>.) 84: */ 85: 86: <a id="Bag.Bag(Collection,String,Comparator)" name="Bag.Bag(Collection,String,Comparator)"> public Bag( Collection contents, String name, Comparator sort_strategy )</a> 87: { this.contents = contents; 88: this.name = name; 89: this.sort_strategy = sort_strategy; 90: } 91: 92: /*==================================================================*/ 93: /* Pass-through (to Collection) methods. */ 94: /*==================================================================*/ 95: 96: <a id="Bag.add(Object)" name="Bag.add(Object)"> public boolean add (Object o) { boolean result = contents.add(o);</a> 97: changed(1); 98: return result; 99: } 100: <a id="Bag.addAll(Collection)" name="Bag.addAll(Collection)"> public boolean addAll(Collection c) { boolean result = contents.addAll(c);</a> 101: changed(1); 102: return result; 103: } 104: <a id="Bag.remove(Object)" name="Bag.remove(Object)"> public boolean remove(Object o) { boolean result = contents.remove(o);</a> 105: changed(-1); 106: return result; 107: } 108: <a id="Bag.clear()" name="Bag.clear()"> public void clear() { contents.clear();</a> 109: changed(-1); 110: } 111: <a id="Bag.removeAll(Collection)" name="Bag.removeAll(Collection)"> public boolean removeAll(Collection c){ boolean result = contents.removeAll(c);</a> 112: changed(-1); 113: return result; 114: } 115: <a id="Bag.retainAll(Collection)" name="Bag.retainAll(Collection)"> public boolean retainAll(Collection c){ boolean result = contents.retainAll(c);</a> 116: changed(-1); 117: return result; 118: } 119: 120: <a id="Bag.size()" name="Bag.size()"> public int size () {return contents.size(); }</a> 121: <a id="Bag.isEmpty()" name="Bag.isEmpty()"> public boolean isEmpty () {return contents.isEmpty(); }</a> 122: <a id="Bag.contains(Object)" name="Bag.contains(Object)"> public boolean contains (Object o) {return contents.contains(o); }</a> 123: <a id="Bag.iterator()" name="Bag.iterator()"> public Iterator iterator () {return contents.iterator(); }</a> 124: <a id="Bag.toArray()" name="Bag.toArray()"> public Object[] toArray () {return contents.toArray(); }</a> 125: <a id="Bag.toArray(Object[])" name="Bag.toArray(Object[])"> public Object[] toArray (Object a[]) {return contents.toArray(a); }</a> 126: <a id="Bag.containsAll(Collection)" name="Bag.containsAll(Collection)"> public boolean containsAll (Collection c) {return contents.containsAll(c);}</a> 127: <a id="Bag.equals(Object)" name="Bag.equals(Object)"> public boolean equals (Object o) {return contents.equals(o); }</a> 128: <a id="Bag.hashCode()" name="Bag.hashCode()"> public int hashCode () {return contents.hashCode(); }</a> 129: 130: /*==================================================================*/ 131: /* "Top" item management */ 132: /*==================================================================*/ 133: 134: /** Return the current "top" element. 135: */ 136: 137: <a id="Bag.top()" name="Bag.top()"> public Object top()</a> 138: { return top; 139: } 140: 141: <a id="Bag.subscription_list" name="Bag.subscription_list"> private ActionListener subscription_list = null;</a> 142: 143: /** Add a listener that's notified when the designated "top" element 144: * changes, either through a call to {@link #designate_top} 145: * or by a user-mandated change made via a visual proxy. 146: */ 147: 148: <a id="Bag.addActionListener(ActionListener)" name="Bag.addActionListener(ActionListener)"> public final void addActionListener( ActionListener subscriber )</a> 149: { subscription_list = 150: AWTEventMulticaster.add( subscription_list, subscriber ); 151: } 152: 153: /** Remove a listener added by {@link #addActionListener}. 154: */ 155: 156: <a id="Bag.removeActionListener(ActionListener)" name="Bag.removeActionListener(ActionListener)"> public final void removeActionListener( ActionListener subscriber )</a> 157: { subscription_list = 158: AWTEventMulticaster.remove( subscription_list, subscriber ); 159: } 160: 161: /** Designate a new "top" element and notify any listeners that 162: * the top item has changed.. Typically, this will be done 163: * by the user picking an element in a proxy, but you can 164: * do it manually by calling this method. 165: * @param top the new "top" element. This element must be 166: * an element of the encapsulated Collection, either added 167: * directly or added via the <code>Bag</code> version 168: * of {@link add()}. 169: * @throws IllegalArgumentException if the argument doesn't 170: * identify an item already in the bag. 171: */ 172: 173: <a id="Bag.designate_top(Object)" name="Bag.designate_top(Object)"> public void designate_top(final Object top)</a> 174: { 175: if( !contents.contains( top ) ) 176: throw new IllegalArgumentException( 177: "Designated top item not in Bag (" + top + ")" ); 178: this.top = top; 179: 180: if( subscription_list != null ) 181: subscription_list.actionPerformed 182: ( new ActionEvent(this, ActionEvent.ACTION_PERFORMED, 183: top.toString()) 184: ); 185: 186: Object[] copy; 187: synchronized(proxies){ copy = proxies.toArray(); } 188: 189: <a id="Bag.java.proxy_update" name="Bag.java.proxy_update"> for( int i = 0; i < copy.length; ++i )</a> 190: { final Proxy proxy = (Proxy)copy[i]; 191: SwingUtilities.invokeLater 192: ( new Runnable() 193: <a id="Bag.run()" name="Bag.run()"> { public void run()</a> 194: { proxy.new_selection(top); 195: proxy.repaint(); 196: } 197: } 198: ); 199: } 200: } 201: 202: /*==================================================================*/ 203: /* Visual proxy */ 204: /*==================================================================*/ 205: 206: <a id="Bag.changed(int)" name="Bag.changed(int)"> private void changed( final int direction )</a> 207: { 208: Object[] copy; 209: synchronized(proxies){ copy = proxies.toArray(); } 210: 211: for( int i = 0; i < copy.length; ++i ) 212: { final Proxy proxy = (Proxy)copy[i]; 213: SwingUtilities.invokeLater 214: ( new Runnable() 215: <a id="Bag.run()" name="Bag.run()"> { public void run()</a> 216: { proxy.changed( direction ); 217: proxy.repaint(); 218: } 219: } 220: ); 221: } 222: } 223: 224: /** Manufacture a "visual proxy" for the current <code>Bag</code>, 225: * suitable for inclusion in a "form". Currently, only one 226: * proxy type (<code>&quot;chooser&quot;&lt;code&gt;) is supported. 227: * The &lt;code&gt;Bag</code> 228: * displays itself as a JList, JComboBox, or JButton, depending 229: * on the amount of screen real estate available to it. In the 230: * case of a button, the button label is the <code>attribute</code> 231: * argument. Note that you can add an Ancestor listener to the 232: * returned proxy to find out when the window that contains 233: * the proxy shuts down. The most-recently selected item 234: * can be fetched, at that point, by calling {@link #selected_item()} 235: * 236: * @see Form 237: * @see User_interface 238: */ 239: 240: <a id="Bag.visual_proxy(String,boolean)" name="Bag.visual_proxy(String,boolean)"> public JComponent visual_proxy( String attribute_name, boolean is_read_only )</a> 241: { 242: if( !is_read_only ) 243: return null; 244: 245: // Set things up so that the proxy will be in the "proxies" 246: // list whenever it's visible. It's essential that it be removed 247: // from the list when invisible; otherwise, the reference in 248: // the list will keep the <code>Proxy</code> object from being garbage 249: // collected after the containing window shuts down. 250: 251: final Proxy proxy = new Proxy( name ); 252: proxy.addAncestorListener 253: ( new AncestorAdapter() 254: <a id="Bag.ancestorRemoved(AncestorEvent)" name="Bag.ancestorRemoved(AncestorEvent)"> { public void ancestorRemoved(AncestorEvent event)</a> 255: { synchronized( proxies ) 256: { proxies.remove( proxy ); 257: } 258: } 259: <a id="Bag.ancestorAdded(AncestorEvent)" name="Bag.ancestorAdded(AncestorEvent)"> public void ancestorAdded(AncestorEvent event)</a> 260: { synchronized( proxies ) 261: { proxies.add( proxy ); 262: } 263: } 264: } 265: ); 266: return proxy; 267: } 268: 269: /** The actual visual-proxy class is a JPanel that contains either 270: * a button, combo box, or list box, depending on its size. An 271: * instance of this class is returned by the <code>visual_proxy()</code> 272: * request. 273: */ 274: 275: <a id="Bag.Proxy" name="Bag.Proxy"> public class Proxy extends JPanel</a> 276: { 277: <a id="Bag.Proxy.selected_size" name="Bag.Proxy.selected_size"> private int selected_size = 10;</a> 278: <a id="Bag.Proxy.attribute_name" name="Bag.Proxy.attribute_name"> private String attribute_name;</a> 279: <a id="Bag.Proxy.drop_down" name="Bag.Proxy.drop_down"> private JComboBox drop_down;</a> 280: <a id="Bag.Proxy.list" name="Bag.Proxy.list"> private JList list;</a> 281: <a id="Bag.Proxy.button" name="Bag.Proxy.button"> private JButton button;</a> 282: <a id="Bag.Proxy.current_ui" name="Bag.Proxy.current_ui"> private JComponent current_ui;</a> 283: 284: <a id="Bag.Proxy.state" name="Bag.Proxy.state"> private Model state = new Model();</a> 285: <a id="Bag.Proxy.artist" name="Bag.Proxy.artist"> private Renderer artist = new Renderer();</a> 286: 287: /** A private constructor, creates a proxy for the current 288: * Bag. Since this constructor is private, you may not issue 289: * a <code>new Bag(&quot;xxx&quot;)</code> request. Get a proxy by 290: * calling the Bag's {@link Bag#visual_proxy} method. 291: * 292: * @see Bag#visual_proxy. 293: */ 294: 295: <a id="Bag.Proxy.Proxy(String)" name="Bag.Proxy.Proxy(String)"> private Proxy( final String attribute_name )</a> 296: { 297: drop_down = new JComboBox ( state ); 298: list = new JList ( state ); 299: button = new JButton ( attribute_name ); 300: 301: drop_down.setRenderer(artist); 302: drop_down.setSelectedIndex( 0 ); 303: 304: // Set up the list. It turns out that the model, though 305: // sufficient for controlling the appearance of the drop- 306: // down, is not sufficient to control the list, so set 307: // up a listener that sets modifies the state of the 308: // current Bag in response to a selection. 309: 310: list.setCellRenderer (artist); 311: list.addListSelectionListener 312: ( new ListSelectionListener() 313: <a id="Bag.Proxy.valueChanged(ListSelectionEvent)" name="Bag.Proxy.valueChanged(ListSelectionEvent)"> { public void valueChanged( ListSelectionEvent event )</a> 314: { if( !event.getValueIsAdjusting() ) 315: designate_top(list.getSelectedValue()); 316: } 317: } 318: ); 319: 320: // In the case of the button, set up a listener to throw up 321: // the selection box (a small frame containing the JList) 322: // when the button is pressed. Also arrange for the box 323: // to pop up over the button, rather than in the upper-left 324: // corner of the screen. The button is disabled as long as 325: // the selection box is displayed. 326: 327: button.addActionListener 328: ( new ActionListener() 329: <a id="Bag.Proxy.actionPerformed(ActionEvent)" name="Bag.Proxy.actionPerformed(ActionEvent)"> { public void actionPerformed( ActionEvent e )</a> 330: { Window popup = new Popup(attribute_name); 331: button.setEnabled(false); 332: popup.addWindowListener 333: ( new WindowAdapter() 334: <a id="Bag.Proxy.windowClosing(WindowEvent)" name="Bag.Proxy.windowClosing(WindowEvent)"> { public void windowClosing(WindowEvent e)</a> 335: { button.setEnabled(true); 336: } 337: } 338: ); 339: popup.setLocation( button.getLocationOnScreen() ); 340: popup.show(); 341: } 342: } 343: ); 344: 345: // Arrange for the UI to change it's appearance, if necessary, 346: // when the size changes. 347: 348: <a id="Bag.java.installcomponentlistener" name="Bag.java.installcomponentlistener"> this.addComponentListener</a> 349: ( new ComponentAdapter() 350: <a id="Bag.Proxy.componentResized(ComponentEvent)" name="Bag.Proxy.componentResized(ComponentEvent)"> { public void componentResized(ComponentEvent e)</a> 351: { install_ui(); 352: } 353: } 354: ); 355: 356: this.attribute_name = attribute_name; 357: setLayout( new BorderLayout() ); 358: } 359: 360: /** Examines the size of the <code>Proxy</code> object and 361: * installs the button, drop-down, or list as appropriate. 362: * does nothing if the correct widget is already displayed. 363: */ 364: 365: <a id="Bag.Proxy.install_ui()" name="Bag.Proxy.install_ui()"> private final void install_ui()</a> 366: { 367: Rectangle bounds = getBounds(); 368: Dimension drop_down_size = drop_down.getPreferredSize(); 369: 370: boolean use_list = bounds.height > (drop_down_size.height *3 ) 371: && bounds.width > (drop_down_size.width +10); 372: boolean use_button = bounds.width < drop_down_size.width 373: || bounds.height < drop_down_size.height; 374: boolean use_drop = !use_list && !use_button; 375: 376: if( use_list && current_ui == list ) return; 377: if( use_button && current_ui == button ) return; 378: if( use_drop && current_ui == drop_down ) return; 379: 380: if( current_ui != null ) 381: current_ui.setVisible( false ); 382: this.removeAll(); 383: 384: if( use_button ) 385: this.add( current_ui = button, BorderLayout.CENTER ); 386: else if( use_list ) 387: this.add( new JScrollPane(current_ui = list), BorderLayout.CENTER ); 388: else 389: { if( current_ui == null ) 390: drop_down.setSelectedIndex(0); 391: this.add( current_ui = drop_down, BorderLayout.NORTH ); 392: } 393: 394: current_ui.setVisible( true ); 395: 396: // Make the newly added component visible, note that 397: // repaint(), invalidate(), and doLayout() do not work 398: // for this purpose (probably a bug). 399: 400: this.setVisible( false ); 401: this.setVisible( true ); 402: } 403: 404: /** Called when the state of the underlying <code>Collection</code> 405: * is changed by calling <code>add()</code>, <code>remove()</code>, 406: * etc. 407: * @param direction <o if the collection has gotten smaller,<br> 408: * >0 if it's gotten larger.<br> 409: * =0 if it hasn't changed size, but needs 410: * to be refreshed. 411: */ 412: 413: <a id="Bag.Proxy.changed(int)" name="Bag.Proxy.changed(int)"> public void changed(int direction)</a> 414: { int selected_index = list.getSelectedIndex(); 415: 416: if( selected_index < 0 ) 417: selected_index = 0; 418: 419: if( selected_index >= contents.size() ) 420: selected_index = contents.size()-1; 421: 422: state.changed(direction); 423: 424: list.setSelectedIndex (selected_index); 425: list.ensureIndexIsVisible (selected_index); 426: 427: drop_down.setSelectedIndex (selected_index); 428: } 429: 430: /** Called when the user picks an item from the current UI. 431: * Makes sure that the drop-down and the list stay in synch 432: * with each other. 433: */ 434: 435: <a id="Bag.Proxy.new_selection(Object)" name="Bag.Proxy.new_selection(Object)"> public void new_selection( Object selected )</a> 436: { if( list.getSelectedValue() != selected ) 437: { list.setSelectedValue ( selected, true ); 438: list.ensureIndexIsVisible( list.getSelectedIndex() ); 439: } 440: if( drop_down.getSelectedItem() != selected ) 441: { drop_down.setSelectedItem( selected ); 442: } 443: } 444: 445: /** Overrides the base-class method, so must be public. Do not 446: * call this method. Installs the ui when the proxy is 447: * displayed the first time. 448: */ 449: 450: <a id="Bag.Proxy.addNotify()" name="Bag.Proxy.addNotify()"> public void addNotify()</a> 451: { super.addNotify(); 452: install_ui(); 453: } 454: 455: /******************************************************************* 456: * The "model" class, holds the state of the font-name combo box. 457: * The list of possible fonts is stored here, as is the currently-selected 458: * font. 459: */ 460: 461: <a id="Bag.Proxy.Model" name="Bag.Proxy.Model"> private final class Model extends AbstractListModel</a> 462: implements ComboBoxModel 463: { 464: <a id="Bag.Proxy.Model.getSelectedItem()" name="Bag.Proxy.Model.getSelectedItem()"> public Object getSelectedItem( ){ return top(); }</a> 465: <a id="Bag.Proxy.Model.setSelectedItem(Object)" name="Bag.Proxy.Model.setSelectedItem(Object)"> public void setSelectedItem(Object o ){ designate_top(o); }</a> 466: 467: <a id="Bag.Proxy.Model.getSize()" name="Bag.Proxy.Model.getSize()"> public int getSize ( ){ return contents.size(); }</a> 468: <a id="Bag.Proxy.Model.getElementAt(int)" name="Bag.Proxy.Model.getElementAt(int)"> public Object getElementAt (int index)</a> 469: { Object[] items = contents.toArray(); 470: if( sort_strategy != null ) 471: { Arrays.sort( items, sort_strategy ); 472: } 473: return (0 <= index && index < items.length) ? items[index]: null; 474: } 475: 476: /** Call this method if the collection changes in some way. 477: * @param direction should be a negative number if the 478: * collection got smaller. A positive number 479: * if it got larger, 0 if it didn't change 480: * size, but the UI needs redrawing anyway. 481: */ 482: 483: <a id="Bag.Proxy.Model.changed(int)" name="Bag.Proxy.Model.changed(int)"> public void changed( int direction )</a> 484: { 485: if( direction < 0 ) 486: fireIntervalAdded (this, 0, contents.size()-1); 487: else if( direction > 0 ) 488: fireIntervalRemoved(this, 0, contents.size()-1); 489: else 490: fireContentsChanged(this, 0, contents.size()-1); 491: } 492: } 493: 494: /******************************************************************* 495: * The JList and JComboBox use the Renderer to draw its cells. 496: */ 497: 498: <a id="Bag.Proxy.Renderer" name="Bag.Proxy.Renderer"> private final class Renderer implements ListCellRenderer</a> 499: { 500: <a id="Bag.Proxy.Renderer.selected_wrapper" name="Bag.Proxy.Renderer.selected_wrapper"> private JPanel selected_wrapper = new JPanel();</a> 501: 502: <a id="Bag.Proxy.Renderer.Renderer()" name="Bag.Proxy.Renderer.Renderer()"> public Renderer()</a> 503: { selected_wrapper.setLayout( new BorderLayout() ); 504: selected_wrapper.setBorder( 505: BorderFactory.createLineBorder(Color.red) ); 506: } 507: 508: // Return a Component whose paint method is used to draw 509: // the cell. Note that that's the only thing that the 510: // Component is used for. The component itself it not put 511: // into the cell. 512: 513: <a id="Bag.Proxy.Renderer.getListCellRendererComponent" name="Bag.Proxy.Renderer.getListCellRendererComponent"> public Component getListCellRendererComponent(</a> 514: JList list, Object value, int index, 515: boolean isSelected, boolean cellHasFocus) 516: { 517: JComponent item; 518: 519: if(value == null ) 520: value = "????"; 521: 522: <a id="Bag.java.proxy_choice" name="Bag.java.proxy_choice"> if( value instanceof User_interface )</a> 523: { item = ((User_interface)value).visual_proxy(null,true); 524: } 525: else 526: { item = new JLabel( value.toString() ) 527: <a id="Bag.Proxy.Renderer.getPreferredSize()" name="Bag.Proxy.Renderer.getPreferredSize()"> { public Dimension getPreferredSize()</a> 528: { Dimension d = super.getPreferredSize(); 529: d.height += 8; 530: d.width += Math.min( d.width+8, 100 ); 531: return d; 532: } 533: }; 534: <a id="Bag.java.proxy_choice_end" name="Bag.java.proxy_choice_end"> }</a> 535: 536: if( isSelected ) 537: { 538: selected_wrapper.removeAll(); 539: selected_wrapper.add( item, BorderLayout.CENTER ); 540: item = selected_wrapper; 541: } 542: 543: return item; 544: } 545: } 546: 547: /*************************************************************** 548: * The "selection frame" that pop's up when the button UI is 549: * pressed. The (non-modal) frame contains a panel with an 550: * etched border, which in turn, holds the same JList object 551: * that appears when the Proxy has enough room to display it. 552: */ 553: 554: <a id="Bag.Proxy.Popup" name="Bag.Proxy.Popup"> private final class Popup extends JFrame</a> 555: { 556: <a id="Bag.Proxy.Popup.Popup(String)" name="Bag.Proxy.Popup.Popup(String)"> public Popup( String attribute_name )</a> 557: { JPanel interior = new JPanel(); 558: interior.setBorder 559: ( BorderFactory.createTitledBorder 560: ( new EtchedBorder(Color.white,Color.gray), 561: attribute_name 562: ) 563: ); 564: 565: interior.setLayout( new BorderLayout() ); 566: interior.add( new JScrollPane(list), BorderLayout.CENTER ); 567: 568: list.setVisible( true ); 569: setContentPane( interior ); 570: pack(); 571: } 572: } 573: } 574: 575: /*==================================================================*/ 576: /* TEST */ 577: /*==================================================================*/ 578: 579: /** This small test class demonstrates how to use a Bag. It creates a 580: * <code>Bag</code>, puts a few items in it, displays two UIs, then 581: * allows you to add elements to the collection. You can resize the 582: * windows to watch the display change from on appearance to another, 583: * and when you type lines into the console window, the new lines 584: * appear in both of the UI windows. Also notice that the proxies 585: * stay in synch with each other with respect to selection as 586: * well (If you don't want this last behavior, wrap the same 587: * <code>Collection</code> object into two distinct <code>Bag</code> 588: * objects. 589: */ 590: 591: <a id="Bag.Test" name="Bag.Test"> public static class Test</a> 592: { 593: <a id="Bag.Test.create_ui(Collection)" name="Bag.Test.create_ui(Collection)"> public void create_ui( Collection aggregate )</a> 594: { 595: JFrame frame = new JFrame(); 596: 597: // Get the visual proxy and shove it into the Frame 598: 599: User_interface displayable = (User_interface)aggregate; 600: frame.getContentPane().add( 601: displayable.visual_proxy("Attribute", true) ); 602: 603: // Set up a window-closing handler and pop the frame up. 604: 605: frame.addWindowListener 606: ( new WindowAdapter() 607: <a id="Bag.Test.windowClosing(WindowEvent)" name="Bag.Test.windowClosing(WindowEvent)"> { public void windowClosing( WindowEvent e )</a> 608: { System.exit(0); 609: } 610: } 611: ); 612: frame.pack(); 613: frame.show(); 614: } 615: 616: <a id="Bag.java.main" name="Bag.java.main"> public static void main( String[] args ) throws Exception</a> 617: { 618: Collection aggregate = new Bag( new LinkedList(), "outer" ); 619: aggregate.add("A"); 620: aggregate.add("B"); 621: aggregate.add("C"); 622: aggregate.add("D"); 623: 624: // You need to treat it as a Bag (as compared to a generic 625: // Collection) to install an ActionListener, thus the cast. 626: // It has to be final because it's referenced by the inner- 627: // class object. Note that the listener will report all the 628: // selections associated with displaying the initial UI. 629: // There will be two such notifications (one for the list 630: // and one for the drop-down) for each of the three proxies. 631: // If you don't want this behavior, install the listener 632: // after the visual proxy has been displayed. 633: 634: <a id="Bag.java.add_listener" name="Bag.java.add_listener"> final Bag the_bag = (Bag) aggregate;</a> 635: the_bag.addActionListener 636: ( new ActionListener() 637: <a id="Bag.Test.actionPerformed(ActionEvent)" name="Bag.Test.actionPerformed(ActionEvent)"> { public void actionPerformed( ActionEvent e )</a> 638: { System.out.println( "Selected " + the_bag.top() ); 639: } 640: } 641: ); 642: 643: create_ui( aggregate ); 644: create_ui( aggregate ); 645: create_ui( aggregate ); 646: 647: // Transfer all lines typed on the console to the collection. 648: 649: String s; 650: while( (s = com.holub.tools.Std.in().readLine()) != null ) 651: { com.holub.tools.Std.out().println( "->" + s ); 652: aggregate.add( s ); 653: } 654: } 655: } 656: //END_TEST 657: }

Whew!

Though all this code seems complicated (because it is complicated), you don’t write code like this very often. You use it a lot, however, and this class is pretty simple to use. Put a Collection into it, then use it like a Collection. Call visual_proxy() when you need to display a UI for the collection. That’s it. With only a few wrappers like Bag, you can easily create visual proxies for many of the attributes of most abstraction-level classes.

Hopefully, I’ve also demonstrated the strengths of this architecture vis-a-vis eliminating the tight-coupling relationships inherent in many of the architectures that I’ve discussed in previous articles. A Bag provides a general, very loosely coupled mechanism for displaying object aggregations without knowing anything about what classes those objects instantiate.

Next month, I’ll continue on the UI theme by discussing how a proxy can interact with the user via the application’s main menu. In particular, I’ll provide an implementation of a menu site class that defines the negotiation necessary for a proxy to get a seat at the menu bar. The proxy can then use this facility to let a user pick a particular look and feel by communicating directly to the proxy via a menu, rather than through the surrounding frame.

Allen Holub has been working in the computer industry since 1979. He is widely published in magazines (Dr. Dobb’s Journal, Programmers Journal, Byte, MSJ, among others). He has seven books to his credit, and is currently working on an eighth that will present the complete sources for a Java compiler written in Java. After eight years as a C++ programmer, Allen abandoned C++ for Java in early 1996. He now looks at C++ as a bad dream, the memory of which is mercifully fading. He’s been teaching programming (first C, then C++ and MFC, now OO-Design and Java) both on his own and for the University of California Berkeley Extension since 1982. Allen offers both public classes and in-house training in Java and object-oriented design topics. He also does object-oriented design consulting and contract Java programming. Get information, and contact Allen, via his Web site http://www.holub.com.