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: }
|