User interfaces for object-oriented systems, Part 3

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.

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.

Figure 1. The Bag's visual proxy in action

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:   public static class Test
2:      {
3:       public void create_ui( Collection aggregate
)
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:                   {   public void windowClosing(
WindowEvent e )
18:                          {   System.exit(0);
19:                          }
20:                      }
21:                  );
22:              frame.pack();
23:              frame.show();
24:          }
25:  
26:       public static void main( String[] args ) throws Exception
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:           final Bag the_bag = (Bag) aggregate;
45:              the_bag.addActionListener
46:              (   new ActionListener()
47:               {   public void actionPerformed(
ActionEvent e )
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.

 

Figure 2. The Bag's static model

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:   *  <p>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:   *  <p>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'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:   *  <p>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:  public class Bag implements User_interface, Collection
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:   private /*final*/ Collection contents;
62:   private /*final*/ Comparator sort_strategy;
63:   private /*final*/ String     name;
64:   private /*final*/ Vector     proxies = new Vector();
65:   private           Object     top     = null;
66:  
67:      /** Create a bag that uses the indicated Collection to represent
68:       *  the indicated attribute.
69:       */
70:  
71:   public Bag( Collection contents, String name 
)
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:   public Bag( Collection contents,
String name, Comparator sort_strategy )
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:   public boolean add   (Object o)       { boolean result = contents.add(o);
97:                                              changed(1);
98:                                              return result;
99:                                            }
100:   public boolean addAll(Collection c)   { boolean result
= contents.addAll(c);    
101:                                              changed(1);
102:                                              return result;
103:                                            }
104:   public boolean remove(Object o)       { boolean result = contents.remove(o);     
105:                                              changed(-1);
106:                                              return result;
107:                                            }
108:   public void    clear()                { contents.clear();   
109:                                              changed(-1);
110:                                            }
111:   public boolean removeAll(Collection c){ boolean
result = contents.removeAll(c); 
112:                                              changed(-1);
113:                                              return result;
114:                                            }
115:   public boolean retainAll(Collection c){ boolean
result = contents.retainAll(c); 
116:                                              changed(-1);
117:                                              return result;
118:                                            }
119:  
120:   public int      size        ()              {return contents.size();        }
121:   public boolean  isEmpty     ()              {return contents.isEmpty();
}
122:   public boolean  contains    (Object o)      {return contents.contains(o);
}
123:   public Iterator iterator    ()              {return contents.iterator();
}
124:   public Object[] toArray     ()              {return contents.toArray();
}
125:   public Object[] toArray     (Object a[])    {return contents.toArray(a);
}
126:   public boolean  containsAll (Collection c)
{return contents.containsAll(c);}
127:   public boolean  equals      (Object o)      {return contents.equals(o);
}
128:   public int      hashCode    ()              {return contents.hashCode();
}
129:  
130:      /*==================================================================*/
131:      /*                  "Top" item management                           */
132:      /*==================================================================*/
133:  
134:      /** Return the current "top" element.
135:       */
136:  
137:   public Object top()
138:      {   return top;
139:      }
140:  
141:   private ActionListener subscription_list = null;
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:   public final void addActionListener(
ActionListener subscriber )
149:      {   subscription_list =
150:              AWTEventMulticaster.add( subscription_list, subscriber );
151:      }
152:  
153:      /** Remove a listener added by {@link #addActionListener}.
154:       */
155:  
156:   public final void
removeActionListener( ActionListener subscriber )
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:   public void designate_top(final Object top)
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:       for( int i = 0; i < copy.length; ++i )
190:          {   final Proxy proxy = (Proxy)copy[i];
191:              SwingUtilities.invokeLater
192:              (   new Runnable()
193:               {   public void run()
194:                      {   proxy.new_selection(top);
195:                          proxy.repaint();
196:                      }
197:                  }
198:              );
199:          }
200:      }
201:  
202:      /*==================================================================*/
203:      /*                        Visual proxy                              */
204:      /*==================================================================*/
205:  
206:   private void changed( final int direction )
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:               {   public void run()
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>"chooser"<code>) is supported.
227:       *  The <code>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:   public JComponent visual_proxy( String
attribute_name, boolean is_read_only )
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:           {   public void ancestorRemoved(AncestorEvent
event)    
255:                  {   synchronized( proxies )
256:                      {   proxies.remove( proxy );
257:                      }
258:                  }
259:               public void ancestorAdded(AncestorEvent
event)
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:   public class Proxy extends JPanel 
276:      {
277:       private int         selected_size = 10;
278:       private String      attribute_name;
279:       private JComboBox   drop_down;
280:       private JList       list;
281:       private JButton     button;
282:       private JComponent  current_ui;
283:  
284:       private Model    state  = new Model();
285:       private Renderer artist = new Renderer();
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("xxx")</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:       private Proxy( final String attribute_name )
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:               {
public void valueChanged( ListSelectionEvent event )
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:               {   public
void actionPerformed( ActionEvent e )
330:                      {   Window popup = new Popup(attribute_name);
331:                          button.setEnabled(false);
332:                          popup.addWindowListener
333:                          (   new WindowAdapter()
334:                           {
public void windowClosing(WindowEvent e)
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:           this.addComponentListener
349:              (   new ComponentAdapter()
350:               {
public void componentResized(ComponentEvent e)
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:       private final void install_ui()
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:       public void changed(int direction)
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:       public void new_selection( Object
selected )
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:       public void addNotify()
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:       private final class Model   extends     AbstractListModel
462:                                      implements  ComboBoxModel
463:          {
464:           public Object getSelectedItem(
){ return top();           } 
465:           public void
setSelectedItem(Object o ){ designate_top(o);       }
466:  
467:           public int    getSize        (        
){ return contents.size(); }
468:           public Object getElementAt
(int index)
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:           public void changed( int direction
)
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:       private final class Renderer implements ListCellRenderer     
499:          {   
500:           private JPanel selected_wrapper
= new JPanel();
501:  
502:           public Renderer()
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:     
public Component getListCellRendererComponent(
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:               if( value instanceof User_interface )
523:                  {   item = ((User_interface)value).visual_proxy(null,true);
524:                  }
525:                  else
526:                  {   item = new JLabel( value.toString() )
527:                   {   public
Dimension getPreferredSize()
528:                          {   Dimension d = super.getPreferredSize();
529:                              d.height += 8;
530:                              d.width  += Math.min( d.width+8, 100 );
531:                              return d;
532:                          }
533:                      };
534:               }
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:       private final class Popup extends JFrame
555:          {   
556:           public Popup( String attribute_name
)
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:   public static class Test
592:      {
593:       public void create_ui( Collection
aggregate )
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:                   {   public
void windowClosing( WindowEvent e )
608:                          {   System.exit(0);
609:                          }
610:                      }
611:                  );
612:              frame.pack();
613:              frame.show();
614:          }
615:  
616:       public static void main( String[] args ) throws Exception
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:           final Bag the_bag = (Bag) aggregate;
635:              the_bag.addActionListener
636:              (   new ActionListener()
637:               {   public
void actionPerformed( ActionEvent e )
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.

Learn more about this topic

  • A usual, the code to this month's article is available in the "Articles" section of my Web site. An index of all my previous JavaWorld articles (including the first two parts of the current series) can be found there as well. http://www.holub.com
  • Design PatternsElements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides -- the "Gang of Four" (Addison Wesley, 1994) http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0201633612