A scalable architecture for building object-oriented user interfaces Before we turn our attention to the main subject of this month’s Java Toolbox — menu negotiation — it seems, judging by numerous reader inquiries, that I have a bit more explaining to do about the visual-proxy architecture that I discussed in the September column.Build user interfaces for object-oriented systems: Read the whole series!Part 1. What is an object? The theory behind building object-oriented user interfacesPart 2. The visual-proxy architecturePart 3. The incredible transmogrifying widgetPart 4. Menu negotiationPart 5. Useful stuffPart 6. An RPN-calculator application demonstrates object-oriented UI principlesOperations on attributesThe first recurring question had to do with the notions of Employees and their salary attribute: How can you sum all the salaries of all the employees in some department without violating encapsulation? Or, to put it another way, why don’t you have to get a number out of the Employee class to perform this operation?My answer is that you don’t get the value of the salary from the Salary object. You might have a salary() method that returns a Salary object that represents the salary, but the implementation of the salary is still hidden inside an object. The main purpose of the auxiliary Salary object is to remove the salary-manipulation functions from the main Employee interface and put them into a class of their own. That is, rather than give the Employee a print_your_salary() method, you’d give the Salary a print_yourself() method. (The salary would also support methods to do other salary-related operations, of course.) You could then print an employee’s salary with either: some_employee.print_your_salary(); orsome_employee.salary().print_yourself(); Adding its salary to a sum is also a reasonable request to ask of Employee in some situations; both of the following are workable object-oriented solutions, since neither exposes the way in which salaries are implemented:some_employee.add_your_salary_to( Salary the_sum ) { the_sum.add( my_salary ); } Or, if you had resorted to a get method: the_sum.add( some_employee.salary() ); Interactions between proxiesThe second issue readers brought to my attention was: How do you handle interconnected controls on a form? For example, what if the status of some checkbox determines which values are valid in another proxy entirely? The quick answer is that the interaction happens at the model (abstraction) level, and the proxies take care of themselves.The figure below demonstrates two common situations. On the left, a model-level object has displayed proxies for two attributes, one as a checkbox and the other as a field of some sort. When the user clicks on the checkbox, a model-level listener (manufactured when the proxies are created) notices that the checkbox has changed state and enables the associated field.The more interesting example is on the right side of the figure. Here, there is a complex interaction between proxies for three objects of different classes: an instance of Company called employer, an instance of Group called division, and an instance of Person called employee. The proxy for the Company displays a list of divisions. It can do this without any communication with the rest of the system because of the qualified association. (Think of the qualifier as specifying a hashtable of Group objects, keyed by division Name. The Company can create a UI displaying all the division names simply by calling getKeys() on the hashtable.) Now look at the sequence diagram shown in the comment on the figure. When the user selects a division’s name, the model-level employer object looks up that division in the hashtable, then asks the division to display its employees. The division turns around and iterates across its list of employees, asking each one for a proxy, which division displays on some UI (a list, for example) that it’s creating. When it’s done with the traversal, it displays the list. The UI seems to be unified in that selecting a division causes a list of employees to change, but it’s really model-level communication that’s going on, not communication between proxies. Of course, the Bag wrapper I discussed last month would simplify the implementation considerably, since it would take care of creating the lists.Tabbing order and focus shift would be handled in much the same way. Either the model-level entity that creates the proxy would establish these behaviors when it created the proxies, or the associated control object would do so. For example, in the Form class that we looked at two months ago, the Form object could easily establish a tab ordering that would follow the order in which Element objects appeared in the form description. Invariant fields would not be put into the tab ordering at all. Data validation and cancel buttons (transaction-based proxies)The final question that kept coming up in your letters was also one of interaction: What about data validation upon close, rather than on the fly? What about a cancel button undoing any changes requested by the user after the form displayed?This one is more complicated. In order to make the architecture more clear, my earlier article presented a simplified version of the system that I actually use, and though I don’t want to rewrite the article, I can at least explain the more sophisticated architecture. Rather than returning a simple JComponent, you could rewrite the User_interface to support transaction-based messaging, as follows:<a id="Transaction" name="Transaction">interface Transaction</a> <a id="Transaction.begin()" name="Transaction.begin()">{ public void begin();</a> <a id="Transaction.commit()" name="Transaction.commit()"> public void commit();</a> <a id="Transaction.rollback()" name="Transaction.rollback()"> public void rollback();</a> <a id="Transaction.supported_rollback_levels()" name="Transaction.supported_rollback_levels()"> public int supported_rollback_levels();</a> } abstract class Visual_proxy extends JComponent implements Transaction {} <a id="User_interface" name="User_interface">interface User_interface</a> <a id="User_interface.proxy" name="User_interface.proxy">{ public Visual_proxy proxy( String attribute_name,</a> <a id="User_interface." name="User_interface."> boolean is_read_only );</a> } The Transaction interface supports begin() and commit() operations. (The former effectively activates the proxy; the user can then interact with it, but the results of that interaction are not sent to the abstraction-level object until a commit() is issued.) The rollback() method causes the proxy to interact with the model, and should put the model back into the state it was in before a previous commit() was issued. You can use this mechanism for undo operations and the like. The supported_rollback_levels() tells the control object how many levels of rollback are supported. It could return 0 if rollback isn’t supported at all. Given this structure, the control object has choices that it didn’t have before, and can easily support the requested features. For data validation on close, the validation is done in the commit() operation, which throws up a dialog box or equivalent if everything’s not OK. Cancel is easily implemented, either by not committing until an OK button is pressed, or by rolling back all committed transactions when the Cancel button is pressed.Menu sitesLet’s move on to the main topic of this month’s column: menu negotiation. Another missing feature in the architecture that I’ve been discussing over the past several months is support for menus. Heaven forfend that I would ever say something good about a Microsoft technology, but the OLE in-place activation model is a not bad example of menu control in object-oriented systems. (The implementation of OLE is miserable, but the ideas are good.)In-place activation describes what happens when you imbed something like an Excel spreadsheet as a table in something like a Word document. When you double click on the embedded table, Excel pops up to edit it. Excel creates a window that exactly overlays the table, and any communication through that window is communication directly to Excel, not Word. Excel also creates its own UI — toolbars, pop-up scroll bars, and so forth. Most importantly — at least from the perspective of this month’s column — Excel also negotiates with Word for space on its menu bar. The menu items inserted by Excel are not connected to Word in any significant way. When you choose a menu item, Excel is notified, and Word doesn’t (or a least shouldn’t) know that any user interaction has occurred. When you finish editing, Excel creates an image of the table for Word to display, and then shuts down. The process I’ve just described is very object oriented. Word knows absolutely nothing about what Excel is doing or how it’s doing it. All it knows is that the Excel-generated table takes up a certain amount of space in the underlying file, and that the visual proxy (to use my term) returned by Excel when it shuts down should be displayed at a certain location in the document. As far as Word is concerned, the table is a black box with no implementation or UI details exposed. In the same way that the Form that I discussed in Part 2 of this series is just a passive location onto which visual proxies are placed, Word just provides a place for an Excel-generated image to be displayed.In the visual-proxy architecture that I’ve been discussing, it’s commonplace for some proxy to need space on the menu bar. Consider the rare case in which a particular attribute or object might need to display itself in one of several ways: it could put a menu on the menu bar that would specify the desired look, and users of the program could then select the look they wanted from the menu. Such a menu is owned by the proxy that created it, however. Communication goes directly from the menu to the model-level object or to the proxy itself.For a menu-negotiation system to be flexible enough to be useful, proxies must be able to do three things: put entire menus onto the menu bar, add line items to any existing menu on the menu bar, and easily remove any items that it has added. The proxy (or model-level object) creates the JMenuItem objects that are added to the menu, so the proxy (or model-level object) listens for the item to be selected. The menu site — the frame that owns the menu bar — doesn’t know that anything’s happened, however. With a menu-site interface added to the mix, the UI that the user experiences is really a composite of user interfaces created by model-level objects. Even the menu bar is such a composite.Using a menu siteBefore looking at the implementation, let’s look at how you might use the classes I’ll describe in a moment. The Menu_site interface specifies how a visual proxy interacts with the menu site. The interface specifies the following methods:void add_menu(Object requester, Menu menu);Adds a menu to the Menu_site‘s main menu bar. The requester argument (typically this) identifies the proxy that is making the request.void add_line_item(Object requester, MenuItem item, String to_this_menu);Adds a line item to a menu already on the menu_site‘s main menu bar. As before, the requester argument (typically this) identifies the proxy that’s making the request. The to_this_menu string identifies the menu to which the item is to be added. The string is first compared against the (invisible) menu-item name (set with MenuComponent.setName()). If no match is found, then the string is compared to the (visible) labels (usually passed into the Menu or MenuItem constructor). This behavior is useful in internationalized programs, where you don’t want to identify anything by the visible label, which might change when the program moves to a new locale. The menu item name, which serves as a unique identifier for the menu item, should never change.Normally, the menus are added the menu bar in left-to-right order. The menu named Help is treated specially in that it always appears at the far right of the menu bar. (The string Help can be either the visible label or the invisible menu name.)void remove_my_menus(Object requester);Removes all Menus and MenuItems that were added by this requester. The menu bar itself is destroyed when there are no more menus on it.All three methods take a requester argument that identifies the proxy that adds the menu items. A proxy doesn’t have to keep track of the menus and line items that it added to the menu site; it just says remove_my_menus() and the Menu_site implementation removes all items added by a given proxy. Typically, the requester argument will be this, but, if a proxy that wanted to remove only some of its menu items were to create an object solely to identify this subset, this need not be the case.For a proxy to use the Menu_site interface, it must first find the parent window that implements MenuSite. In raw AWT this is a possible, but annoyingly verbose, operation. You have to use a loop that looks something like this:Container parent = this; while( (parent = parent.getParent()) != null ) if( parent instanceof MenuSite ) break; Swing provides a handy convenience method that does the same thing:parent = SwingUtilites.getAncestorOfClass(MenuSite.class, this); Once the Menu_site is located, the proxy can add menus or menu items as desired. When the proxy shuts down, it must send a remove_my_menus(this) message to the Menu_site, which will remove any menus or menu items that the proxy added. (Implementation note: The default Menu_site implementation could register itself as an AncestorListener on all proxies that added menu items, and then automatically remove the menu items associated with a proxy when that proxy shuts down. I choose not to use this strategy because proxies are sometimes very long lived; they may exist for the life of the program, but be invisible part of the time. Similarly, a proxy may want to modify the menus dynamically based on the state that it’s in at any given moment. Automatic removal wouldn’t work in these situations.) Note that a proxy can create an object solely to be able to pass a reference to the object as a requester to one of the Menu_site methods. This way, the proxy could remove a subset of its menu items (the ones associated with a particular requester) if it necessary.Implementing menu sitesThe listing below contains both the Menu_site interface definition (which runs from line 31 to line 71) and also an inner-class implementation of that interface (the remainder of the listing).Multiple inheritance in JavaI use this inner-class interface strategy when I expect multiple inheritance to be required; if the Menu_site were a class, I’d have problems in situations where I wanted a container of some sort to be both a JContainer and a Menu_site — you can’t extend two classes. By making Menu_site an interface and providing a default implementation of that interface, I can effectively implement multiple inheritance as follows:<a id="Main_frame" name="Main_frame">class Main_frame extends JFrame implements Menu_site</a> { final Menu_site.Implementation support; Main_frame() { support = new Menu_site.Implementation(this); //... } <a id="Main_frame.add_menu(Object,JMenu)" name="Main_frame.add_menu(Object,JMenu)"> public void add_menu( Object requester, JMenu item )</a> { support.add_menu( requester, item ); } <a id="Main_frame.void" name="Main_frame.void"> public void</a> add_line_item(Object requester,JMenuItem item, String here) { support.add_line_item(requester, item, here ); } <a id="Main_frame.remove_my_menus(Object)" name="Main_frame.remove_my_menus(Object)"> public void remove_my_menus( Object requester )</a> { support.remove_my_menus( requester ); } //... } For all intents and purposes, a Main_frame object is both a JFrame and a Menu_site. Who says you can’t have multiple inheritance in Java?I usually implement default implementations of interfaces as static inner classes of the interface itself, because you would never use the implementation class unless you were also using the interface. This way, I don’t clutter up the package-level name space with extra symbols. I consistently call this implementation class Implementation.Implementing the interface methodsThe Implementation class contains the actual menu bar, referenced by menu_bar (line 120) and created by add_menu() when the first menu item is added. The Implementation also defines a hash table that connects the requesters — proxies that have requested space on the menu bar with the menus or line items that they’ve added (line 121). The hashtable is keyed by references to given proxies and contains Vectors of Item objects (discussed momentarily), one for each submenu or line item added by a given requester.The only other data member, menu_frame (line 118), references the frame that contains the current menu site. It’s initialized from a constructor argument.The Menu_site interface overrides come next. add_menu(...) (line 128) creates the menu bar, if necessary, and adds a new Item. I’ll talk about the Item class in depth in a moment (it’s an inner class of Menu_site.Implementation), but for now, think of it as an object that associates a particular menu item with the proxy that requested that said menu item be added to the menu site. The requester_menus() call on line 135 returns a vector of Items that have been added by the current requester. (If this is the first item to be added for a requester, the returned Vector is empty). Finally, add_menu() asks the item to attach itself to the menu bar.The add_line_item override (line 139) comes next. Most of the code is just searching the existing menu bar for a menu with a name or label that matches the to_this_menu argument. All names are examined before the labels are, in case the same string appears in both lists. In other words, item names are given precedence over labels. Once the required menu is identified, a new Item is created, added to the Vector of menu items associated with the current requester, and then asked to add itself to the menu bar.The final interface override is the remove_my_menus(...) method (line 183), which gets the Vector of menu items added by a given proxy out of the requesters hash table, and then traverses the Vector, asking each Item to remove itself from whatever container (the menu bar itself or some submenu) holds it.Convenience methodsThe next part of the Implementation class defines a few useful (static) convenience methods. I’m not much of a fan of the default Swing menu look and feel — the text used for line items is much too large for my taste. These convenience methods let you set up a font to use for line items and help you create both menus and line items whose text is displayed in the given font. The default font is 10 point Sans Serif bold, but you can change that by calling setFont(...) (line 200). Any line items created by calling line_item (line 212) (rather than new JMenuItem()) will use that font for their label. Similarly, menus created with menu(...) (line 224) (rather than new JMenu()) will use the same font for their label.The Item classMost of the messy work is done in the Menu_site.Implementation.Item class (line 256). The three fields identify a menu item (line_item), the parent of the line item (container), which is declared as an Object, but which will always actually be either a JMenuBar or a JMenu object, depending on whether the current item is on the menu bar itself or in some submenu. (An assertion in the constructor checks this fact.) The is_help_menu flag indicates whether the current Item object represents the Help menu.The attach_menu_to_container() message (line 281) is sent to an Item to ask it to attach itself to its container. The main problem here is Swing. The JMenuBar class defines methods that purport to modify the Help menu, but these methods don’t work (or at least didn’t work the last time I looked — in any event, they don’t work across all Swing implementations). Consequently, I need to deal with the Help menu explicitly. The other problem is that Swing doesn’t correctly update the menu bar as items are added to and removed from it in all situations. I’ve solved both problems by building a new menu bar from scratch every time an item is added or removed. I then replace the existing menu bar with the new one. The menu_bar_contents LinkedList (line 246) lists the contents of the menu bar in left-to-right order. I add new Item objects to the right end of the list unless a help menu is found at the far-right position. In this case, new items are added immediately to the left of the help menu. It’s ugly, but it works.The detach_menu_from_container() method (line 316) removes the current Item from both the menu bar and menu that contains it and the menu_bar_contents list. The regenerate() method (line 334) rebuilds the menu bar from the current menu_bar_contents list and replaces the current menu bar with the new one.The remainder of the listing implements the requester_menus(...) (line 361) is_help_menu(...) (line 377) methods mentioned earlier. I also provide a unit-test class, Test (line 402).Listing: /src/com/holub/ui/Menu_site.java 1: package com.holub.ui; 2: 3: import java.util.*; 4: import java.io.*; 5: import java.awt.*; 6: import java.awt.event.*; 7: import javax.swing.*; 8: 9: import com.holub.tools.debug.Assert; 10: 11: /** A Menu_site is a frame that holds a menu bar. Other objects in 12: * the system (which do not have to be visual objects) can negotiate 13: * with the Menu_site to have menu's placed on the site's menu 14: * bar (or within submenus already found on the menu bar). 15: */ 16: 17: // Possible extensions: 18: // * Stack existing menus and menu items if you add one with the same 19: // name. Restore old item from the stack when menu reverts to previous 20: // state. 21: 22: <a id="Menu_site" name="Menu_site">public interface Menu_site</a> 23: { 24: /***************************************************************** 25: * Adds a menu to the menu_site's main menu bar. 26: * @param requester The object that creates the menu. This 27: * does not have to be a visual object. 28: * @param menu The menu to be added 29: */ 30: 31: <a id="Menu_site.add_menu(Object,JMenu)" name="Menu_site.add_menu(Object,JMenu)"> void add_menu(Object requester, JMenu menu );</a> 32: 33: /***************************************************************** 34: * Adds a line item to a menu already on the menu_site's main 35: * menu bar. 36: * 37: * @param requester The object that creates the menu. This 38: * does not have to be a visual object. 39: * @param item The line-item to be added. This could 40: * be a single JMenuItem or an entire JMenu. 41: * @param to_this_menu The label that identifies the menu to 42: * which the item is to be added. The 43: * to_this_menu string is first compared 44: * against all the (invisible) menu names 45: * (set with JMenuItem.setName()) 46: * If no match is found, then the string 47: * is compared to the (visible) labels 48: * (usually passed into the JMenu or JMenuItem 49: * constructor). Note that all the menu names 50: * are examined before any of the labels are 51: * examined (by design). 52: * 53: * The "Help" menu (identified by a label or 54: * name having the value "Help"). No special 55: * methods are required to manipulate it. 56: * 57: * @throws IllegalArgumentException (a RuntimeException) if the 58: * menu specified in <code>to_this_menu</code> 59: * can't be found. 60: */ 61: 62: void add_line_item( Object requester, JMenuItem item, 63: <a id="Menu_site." name="Menu_site."> String to_this_menu);</a> 64: 65: /***************************************************************** 66: * Removes all JMenus and JMenuItem's that were added by this 67: * requester. Note that the AWT JMenuBar is, itself, destroyed 68: * when it has no menus on it. 69: */ 70: 71: <a id="Menu_site.remove_my_menus(Object)" name="Menu_site.remove_my_menus(Object)"> void remove_my_menus( Object requester );</a> 72: 73: /***************************************************************** 74: * This class provides support for implementing a Menu_site. 75: * It has default versions of all menu-site methods, and also 76: * maintains the menu bar itself (which is installed 77: * on the frame, a reference to which is passed to the 78: * Implementation(JFrame) constructor). Use it like this: 79: * 80: * <PRE> 81: * class Main_frame extends JFrame implements Menu_site 82: * { 83: <a id="Menu_site.support" name="Menu_site.support"> * final Menu_site.Implementation support;</a> 84: * 85: * Main_frame() 86: <a id="Menu_site." name="Menu_site."> * { support = new Menu_site.Implementation(this);</a> 87: * //... 88: * } 89: * 90: * public void add_menu(Object requester, JMenu item ) 91: <a id="Menu_site." name="Menu_site."> * { support.add_menu( requester, item );</a> 92: * } 93: * 94: * public void 95: * add_line_item(Object requester,JMenuItem item,String here) 96: <a id="Menu_site." name="Menu_site."> * { support.add_line_item(requester, item, here );</a> 97: * } 98: * 99: * public void remove_my_menus(Object requester) 100: <a id="Menu_site." name="Menu_site."> * { support.remove_my_menus( requester );</a> 101: * } 102: * } 103: * </PRE> 104: */ 105: 106: <a id="Menu_site.Implementation" name="Menu_site.Implementation"> public static class Implementation</a> 107: implements Menu_site, Serializable 108: { 109: // The "requesters" table keeps track of who requested which 110: // menu items. It is indexed by requester and contains a 111: // Vector of Implementation.Item objects that identify all 112: // items added by that requester. 113: // 114: // The Implementation also encapsulates the menu bar itself, 115: // which it creates and installs on its menu_frame (passed 116: // into the constructor) when the first item is added. 117: 118: <a id="Menu_site.Implementation.menu_frame" name="Menu_site.Implementation.menu_frame"> private /*final*/ JFrame menu_frame;</a> 119: 120: <a id="Menu_site.Implementation.menu_bar" name="Menu_site.Implementation.menu_bar"> private JMenuBar menu_bar = null;</a> 121: <a id="Menu_site.Implementation.requesters" name="Menu_site.Implementation.requesters"> private Hashtable requesters = new Hashtable();</a> 122: 123: //------------------------------------------------------------ 124: <a id="Menu_site.Implementation.Implementation(JFrame)" name="Menu_site.Implementation.Implementation(JFrame)"> public Implementation( JFrame menu_frame )</a> 125: { this.menu_frame = menu_frame; 126: } 127: //------------------------------------------------------------ 128: <a id="Menu_site.Implementation.add_menu(Object,JMenu)" name="Menu_site.Implementation.add_menu(Object,JMenu)"> public void add_menu( Object requester, JMenu menu )</a> 129: { 130: if( menu_bar == null ) 131: menu_frame.setJMenuBar( menu_bar = new JMenuBar() ); 132: 133: Item item = new Item(menu, menu_bar, is_help_menu(menu)); 134: 135: <a id="Menu_site.java.getmenu" name="Menu_site.java.getmenu"> requester_menus( requester ).addElement(item);</a> 136: item.attach_menu_to_container(); 137: } 138: //------------------------------------------------------------ 139: <a id="Menu_site.Implementation.add_line_item" name="Menu_site.Implementation.add_line_item"> public void add_line_item( Object requester,</a> 140: JMenuItem line_item, String to_this_menu ) 141: { 142: Assert.is_true(requester != null, "null requester" ); 143: Assert.is_true(line_item != null, "null line_item" ); 144: Assert.is_true(to_this_menu != null, "null to_this_menu"); 145: 146: JMenu found = null; 147: int menu_count = menu_bar.getMenuCount(); 148: 149: // Find the menu into which the line item should be 150: // inserted. First check for a match of the (invisible) 151: // menu names. 152: 153: int i = 0; 154: for(; i < menu_count; ++i ) 155: { found = menu_bar.getMenu(i); 156: if( to_this_menu.equals( found.getName() ) ) 157: break; 158: } 159: 160: // If that didn't work, check for a match of 161: // the (visible) menu labels. 162: 163: if( i >= menu_count ) 164: for( i = 0 ; i < menu_count; ++i ) 165: { found = menu_bar.getMenu(i); 166: if( to_this_menu.equals( found.getText() ) ) 167: break; 168: } 169: 170: // If you can't find it either place, throw an exception. 171: 172: if( i >= menu_count ) 173: throw new IllegalArgumentException( 174: "add_line_item() can't find menu (" 175: + to_this_menu + ")" ); 176: 177: Item item = new Item( line_item, found, false ); 178: 179: requester_menus( requester ).addElement( item ); 180: item.attach_menu_to_container(); 181: } 182: //------------------------------------------------------------ 183: <a id="Menu_site.Implementation.remove_my_menus(Object)" name="Menu_site.Implementation.remove_my_menus(Object)"> public void remove_my_menus( Object requester )</a> 184: { 185: Vector menus = (Vector)( requesters.remove(requester) ); 186: 187: if( menus != null ) 188: { Enumeration e = menus.elements(); 189: while( e.hasMoreElements() ) 190: ((Item)(e.nextElement())) 191: .detach_menu_from_container(); 192: } 193: } 194: 195: /************************************************************* 196: * Set the default font used by the {@link #line_item} and 197: * {@link #menu} convenience methods. If you never call this 198: * method, 10-point, SanSerif, bold is used. 199: */ 200: <a id="Menu_site.Implementation.setFont(Font)" name="Menu_site.Implementation.setFont(Font)"> static public void setFont( Font menu_font )</a> 201: { Implementation.menu_font = menu_font; 202: } 203: 204: <a id="Menu_site.Implementation.menu_font" name="Menu_site.Implementation.menu_font"> static private Font menu_font =</a> 205: new Font("SansSerif", Font.BOLD, 10); 206: 207: /************************************************************* 208: * This is a convenience method that manufactures menu items 209: * with text labels. The font used is a bit more tractable 210: * than the default Java look-and-feel font. 211: */ 212: <a id="Menu_site.Implementation.line_item" name="Menu_site.Implementation.line_item"> static public JMenuItem line_item(String text,</a> 213: ActionListener action) 214: { JMenuItem item = new JMenuItem( text ); 215: item.setFont( menu_font ); 216: item.addActionListener( action ); 217: return item; 218: } 219: /************************************************************* 220: * This is a convenience method that manufactures JMenu's 221: * that use the same font as the line items manufactured by 222: * {@link #line_item}. 223: */ 224: <a id="Menu_site.Implementation.menu(String)" name="Menu_site.Implementation.menu(String)"> static public JMenu menu(String text)</a> 225: { JMenu item = new JMenu( text ); 226: item.setFont( menu_font ); 227: return item; 228: } 229: 230: //============================================================ 231: // Private support methods and classes 232: 233: /************************************************************* 234: * The menu_bar_contents vector contains references to the 235: * various menus that comprise the menu bar. This kluge is 236: * necessary because Swing does not yet support the notion 237: * of a Help menu, and it won't let you insert menus anywhere 238: * other than the far right of the menu bar, where the Help 239: * menu should be. Consequently, when a new menu is added to 240: * a menu bar, you need to clear out the existing menu bar 241: * and rebuild it from scratch. The menu_bar_has_help 242: * indicates that a help menu has been installed (It's 243: * assumed to be at the far right of the menu bar.) 244: */ 245: 246: <a id="Menu_site.Implementation.menu_bar_contents" name="Menu_site.Implementation.menu_bar_contents"> private final LinkedList menu_bar_contents = new LinkedList();</a> 247: 248: /************************************************************* 249: * An Item makes the association between a line item or 250: * submenu and the MenuBar or Menu that contains it. You can 251: * ask an Item to add or remove itself from its container. 252: * All the weirdness associated with help menus is handled 253: * here. 254: */ 255: 256: <a id="Menu_site.Implementation.Item" name="Menu_site.Implementation.Item"> private final class Item implements Serializable</a> 257: { 258: <a id="Menu_site.Implementation.Item.line_item" name="Menu_site.Implementation.Item.line_item"> private final JMenuItem line_item;</a> 259: <a id="Menu_site.Implementation.Item.container" name="Menu_site.Implementation.Item.container"> private Object container;</a> 260: <a id="Menu_site.Implementation.Item.is_help_menu" name="Menu_site.Implementation.Item.is_help_menu"> private final boolean is_help_menu;</a> 261: 262: <a id="Menu_site.Implementation.Item.Item" name="Menu_site.Implementation.Item.Item"> public Item( JMenuItem line_item, Object container,</a> 263: boolean is_help_menu) 264: { 265: Assert.is_true( container instanceof JMenu || 266: container instanceof JMenuBar ); 267: 268: this.line_item = line_item; 269: this.container = container; 270: this.is_help_menu = is_help_menu; 271: } 272: 273: /******************************************************** 274: * Attach a menu item to it's container (either a menu 275: * bar or a menu). Items are added at the end of the 276: * <code>menu_bar_contents</code> list unless a help 277: * menu exists, in which case items are added at 278: * the penultimate position. 279: */ 280: 281: <a id="Menu_site.Implementation.Item.attach_menu_to_container()" name="Menu_site.Implementation.Item.attach_menu_to_container()"> public final void attach_menu_to_container()</a> 282: { 283: if( container instanceof JMenu ) 284: ((JMenu)container).add( line_item ); 285: else 286: { 287: if( menu_bar_contents.size() <= 0 ) 288: { 289: menu_bar_contents.add( this ); 290: ((JMenuBar)container).add( line_item ); 291: } 292: else 293: { Item last = 294: (Item)( menu_bar_contents.getLast() ); 295: if( !last.is_help_menu ) 296: { menu_bar_contents.add(this); 297: ((JMenuBar)container).add( line_item ); 298: } 299: else // remove the help menu, add the new 300: { // item, then put the help menu back 301: // (following the new item). 302: 303: menu_bar_contents.removeLast(); 304: menu_bar_contents.add( this ); 305: menu_bar_contents.add( last ); 306: container = regenerate(); 307: } 308: } 309: } 310: } 311: 312: /******************************************************** 313: * Remove the current menu item from its container 314: * (either a menu bar or a menu). 315: */ 316: <a id="Menu_site.Implementation.Item.detach_menu_from_container()" name="Menu_site.Implementation.Item.detach_menu_from_container()"> public final void detach_menu_from_container()</a> 317: { 318: if( container instanceof JMenu ) 319: { ((JMenu)container).remove( line_item ); 320: } 321: else 322: { ((JMenuBar)container).remove( line_item ); 323: menu_bar_contents.remove( this ); 324: container = regenerate(); 325: } 326: } 327: 328: 329: /******************************************************** 330: * Replace the old menu bar with a new one that reflects 331: * the current state of the <code>menu_bar_contents</code> 332: * list. 333: */ 334: <a id="Menu_site.Implementation.Item.regenerate()" name="Menu_site.Implementation.Item.regenerate()"> private JMenuBar regenerate()</a> 335: { 336: // Create the new menu bar and populate it from 337: // the current-contents list. 338: 339: menu_bar = new JMenuBar(); 340: ListIterator i = menu_bar_contents.listIterator(0); 341: while( i.hasNext() ) 342: menu_bar.add( ((Item)(i.next())).line_item ); 343: 344: // Replace the old menu bar with the new one. 345: // Calling setVisible causes the menu bar to be 346: // redrawn with a minimum amount of flicker. Without 347: // it, the redraw doesn't happen at all. 348: 349: menu_frame.setJMenuBar( menu_bar ); 350: menu_frame.setVisible( true ); 351: 352: return menu_bar; 353: } 354: } 355: /************************************************************ 356: * Return a vector of menu items associated with a given 357: * requester. A new (empty) vector is created and returned 358: * if there are no menus associated with the requester at 359: * present. 360: */ 361: <a id="Menu_site.Implementation.requester_menus(Object)" name="Menu_site.Implementation.requester_menus(Object)"> private Vector requester_menus( Object requester )</a> 362: { 363: Assert.is_true( requester != null, "Bad argument" ); 364: Assert.is_true( requesters != null, "No requesters" ); 365: 366: Vector menus = (Vector)( requesters.get(requester) ); 367: if( menus == null ) 368: { menus = new Vector(); 369: requesters.put( requester, menus ); 370: } 371: return menus; 372: } 373: /************************************************************ 374: * Return true if the menu passed as an argument is the 375: * "help" menu. The name "help" is not case sensitive. 376: */ 377: <a id="Menu_site.Implementation.is_help_menu(JMenu)" name="Menu_site.Implementation.is_help_menu(JMenu)"> private boolean is_help_menu( JMenu menu )</a> 378: { 379: String s = menu.getName(); 380: if( s != null && s.toLowerCase().equals("help") ) 381: return true; 382: 383: s = menu.getText(); 384: return (s != null && s.toLowerCase().equals("help")); 385: } 386: } 387: 388: /*************************************************************** 389: * This inner class tests the Menu_site.Implementation. Do not 390: * Ship Menu_site$Test.class with the applications. Test the 391: * code by invoking "java com.holub.tools.Menu_site$Test". 392: * The test code creates three menus: 393: * o A Help menu that contains three line items, each added in 394: * a different way. Nothing happens when these are selected. 395: * o A "Removal" menu that disappears when selected. 396: * o A "Main" menu that initially contains a single line item 397: * called "Add an Item." Selecting this item adds a "Remove 398: * Menus" Item to the "Main" menu. Selecting that removes 399: * both the Main and Help (but not the "Removal") menus. 400: */ 401: 402: <a id="Menu_site.Test" name="Menu_site.Test"> static class Test extends JFrame implements Menu_site</a> 403: { 404: static Test instance; 405: 406: Test() 407: { setSize( 400, 200 ); 408: addWindowListener 409: ( new WindowAdapter() 410: <a id="Menu_site.Test.windowClosing(WindowEvent)" name="Menu_site.Test.windowClosing(WindowEvent)"> { public void windowClosing( WindowEvent e )</a> 411: { System.exit(1); 412: } 413: } 414: ); 415: show(); 416: } 417: //------------------------------------------------------------ 418: final Menu_site.Implementation support 419: = new Menu_site.Implementation(this); 420: 421: <a id="Menu_site.Test.add_menu(Object,JMenu)" name="Menu_site.Test.add_menu(Object,JMenu)"> public void add_menu(Object requester, JMenu item )</a> 422: { support.add_menu( requester, item ); 423: } 424: 425: <a id="Menu_site.Test.void" name="Menu_site.Test.void"> public void</a> 426: add_line_item(Object requester, JMenuItem item, 427: String to_this_menu) 428: { support.add_line_item(requester, item, to_this_menu ); 429: } 430: 431: <a id="Menu_site.Test.remove_my_menus(Object)" name="Menu_site.Test.remove_my_menus(Object)"> public void remove_my_menus(Object requester)</a> 432: { support.remove_my_menus( requester ); 433: } 434: 435: //------------------------------------------------------------ 436: <a id="Menu_site.Test.Remove_listener" name="Menu_site.Test.Remove_listener"> static class Remove_listener implements ActionListener</a> 437: <a id="Menu_site.Test.Remove_listener.actionPerformed(ActionEvent)" name="Menu_site.Test.Remove_listener.actionPerformed(ActionEvent)"> { public void actionPerformed( ActionEvent e )</a> 438: { instance.remove_my_menus( instance ); 439: } 440: } 441: 442: <a id="Menu_site.Test.main(String[])" name="Menu_site.Test.main(String[])"> static public void main( String[] args )</a> 443: { 444: instance = new Test(); 445: 446: JMenu main_menu = new JMenu( "Main" ); 447: instance.add_menu( instance, main_menu ); 448: 449: JMenuItem add_an_item_menu = new JMenuItem( 450: "Add Line Item to Menu" ); 451: add_an_item_menu.addActionListener 452: ( new ActionListener() 453: <a id="Menu_site.Test.actionPerformed(ActionEvent)" name="Menu_site.Test.actionPerformed(ActionEvent)"> { public void actionPerformed( ActionEvent e )</a> 454: { 455: JMenuItem remove_menus = new JMenuItem( 456: "Remove Main and Help menus"); 457: 458: remove_menus.addActionListener 459: ( new ActionListener() 460: <a id="Menu_site.Test.actionPerformed(ActionEvent)" name="Menu_site.Test.actionPerformed(ActionEvent)"> { public void actionPerformed(ActionEvent e)</a> 461: { instance.remove_my_menus(instance); 462: } 463: } 464: ); 465: 466: // Add line item via menu site 467: instance.add_line_item(instance, 468: remove_menus, "Main"); 469: } 470: } 471: ); 472: 473: // Add this line-item directly to the "Main" menu (without 474: // going through the menu site) just to make sure that 475: // we can. 476: 477: main_menu.add( add_an_item_menu ); 478: 479: // Create a help menu. Do it in the middle of things 480: // to make sure that it ends up on the far right. 481: // Use all three mechanisms for adding menu items directly 482: // using the menu's "name," and using the menu's "text"). 483: 484: JMenu help = new JMenu("Help"); // "Help" is the menu text 485: help.setName("HelpMenu"); // "HelpMenu" is the 486: // (invisible) menu name 487: 488: JMenuItem help_by_text= new JMenuItem("Added using text"); 489: JMenuItem help_by_name= new JMenuItem("Added using name"); 490: JMenuItem help_direct = new JMenuItem("Added directly"); 491: 492: ActionListener help_listener = 493: new ActionListener() 494: <a id="Menu_site.Test.actionPerformed(ActionEvent)" name="Menu_site.Test.actionPerformed(ActionEvent)"> { public void actionPerformed(ActionEvent e)</a> 495: { System.out.println("No help available"); } 496: }; 497: 498: help_by_text.addActionListener( help_listener ); 499: help_by_name.addActionListener( help_listener ); 500: help_direct.addActionListener ( help_listener ); 501: 502: instance.add_menu ( instance, help ); 503: 504: help .add (help_direct ); 505: instance.add_line_item(instance, help_by_text, "Help" ); 506: instance.add_line_item(instance, help_by_name, "HelpMenu"); 507: 508: // Create a second "requester" and have it add a Removal 509: // menu with the name Removal_menu. Picking that menu 510: // will remove only the menu for the current requester. 511: 512: final int x[] = new int[1]; 513: 514: JMenu remove_menu = new JMenu("Removal"); 515: remove_menu.setName( "Removal_menu" ); 516: instance.add_menu( x, remove_menu ); 517: 518: // Create and add a line item to the Removal_menu by name 519: // (the earlier example used the Label). 520: 521: JMenuItem remove_me = new JMenuItem("Click_to_Remove_Me"); 522: remove_me.addActionListener 523: ( new ActionListener() 524: <a id="Menu_site.Test.actionPerformed(ActionEvent)" name="Menu_site.Test.actionPerformed(ActionEvent)"> { public void actionPerformed( ActionEvent e )</a> 525: { instance.remove_my_menus(x); 526: } 527: } 528: ); 529: instance.add_line_item( x, remove_me, "Removal_menu" ); 530: } 531: } 532: } Mopping upSo that’s the Menu_site. A frame implements a menu site simply by declaring one. The menu site itself then creates the required menu bar and negotiates with proxies for space on that menu bar. This strategy is helpful when proxies need space for menus, and lets them get that space without knowing anything about the context in which they’re used (except that a Menu_site is implemented by the parent window).Next month, I’ll pull all of the previous discussion together by implementing a small RPN calculator application that uses the principles that I’ve been discussing. This way, you can see the architecture applied to a real (though small) application.Allen Holub runs Holub Associates, a software-design firm based in the San Francisco Bay area. Holub Associates is a software-architecture house. It designs software and guides you through the implementation process in much the same way that traditional architects design buildings and help contractors construct them. Holub Associates also provides training in object-oriented design and Java, provides design-review and mentoring services, and does occasional (Java) implementation work. Allen has been working in the computer industry since 1979. He is widely published in magazines (Dr. Dobb’s Journal, Programmers Journal, Byte, and 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 built two operating systems from scratch, several compilers, and various application programs ranging in scope from robotics controllers to children’s software. He’s been teaching programming (first C, then C++ and MFC, now object-oriented design and Java) both on his own and for the University of California at Berkeley Extension since 1982. Get information, and contact Allen, via his Web site at http://www.holub.com. Java