       1 /*
       2  * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved.
       3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       4  *
       5  * This code is free software; you can redistribute it and/or modify it
       6  * under the terms of the GNU General Public License version 2 only, as
       7  * published by the Free Software Foundation.  Oracle designates this
       8  * particular file as subject to the "Classpath" exception as provided
       9  * by Oracle in the LICENSE file that accompanied this code.
      10  *
      11  * This code is distributed in the hope that it will be useful, but WITHOUT
      12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
      13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
      14  * version 2 for more details (a copy is included in the LICENSE file that
      15  * accompanied this code).
      16  *
      17  * You should have received a copy of the GNU General Public License version
      18  * 2 along with this work; if not, write to the Free Software Foundation,
      19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
      20  *
      21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
      22  * or visit www.oracle.com if you need additional information or have any
      23  * questions.
      24  */
      25
      26 package javax.swing.plaf.basic;
      27
      28 import sun.swing.DefaultLookup;
      29 import sun.swing.UIAction;
      30 import java.awt.*;
      31 import java.awt.event.*;
      32 import java.beans.*;
      33 import javax.swing.*;
      34 import javax.swing.event.*;
      35 import javax.swing.plaf.*;
      36 import javax.swing.border.*;
      37 import java.util.Arrays;
      38 import java.util.ArrayList;
      39
      40
      41 /**
      42  * A default L&amp;F implementation of MenuUI.  This implementation
      43  * is a "combined" view/controller.
      44  *
      45  * @author Georges Saab
      46  * @author David Karlton
      47  * @author Arnaud Weber
      48  */
      49 public class BasicMenuUI extends BasicMenuItemUI
      50 {
      51     /**
      52      * The instance of {@code ChangeListener}.
      53      */
      54     protected ChangeListener         changeListener;
      55
      56     /**
      57      * The instance of {@code MenuListener}.
      58      */
      59     protected MenuListener           menuListener;
      60
      61     private int lastMnemonic = 0;
      62
      63     /** Uses as the parent of the windowInputMap when selected. */
      64     private InputMap selectedWindowInputMap;
      65
      66     /* diagnostic aids -- should be false for production builds. */
      67     private static final boolean TRACE =   false; // trace creates and disposes
      68     private static final boolean VERBOSE = false; // show reuse hits/misses
      69     private static final boolean DEBUG =   false;  // show bad params, misc.
      70
      71     private static boolean crossMenuMnemonic = true;
      72
      73     /**
      74      * Constructs a new instance of {@code BasicMenuUI}.
      75      *
      76      * @param x a component
      77      * @return a new instance of {@code BasicMenuUI}
      78      */
      79     public static ComponentUI createUI(JComponent x) {
      80         return new BasicMenuUI();
      81     }
      82
      83     static void loadActionMap(LazyActionMap map) {
      84         BasicMenuItemUI.loadActionMap(map);
      85         map.put(new Actions(Actions.SELECT, null, true));
      86     }
      87
      88
      89     protected void installDefaults() {
      90         super.installDefaults();
      91         updateDefaultBackgroundColor();
      92         ((JMenu)menuItem).setDelay(200);
      93         crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic");
      94     }
      95
      96     protected String getPropertyPrefix() {
      97         return "Menu";
      98     }
      99
     100     protected void installListeners() {
     101         super.installListeners();
     102
     103         if (changeListener == null)
     104             changeListener = createChangeListener(menuItem);
     105
     106         if (changeListener != null)
     107             menuItem.addChangeListener(changeListener);
     108
     109         if (menuListener == null)
     110             menuListener = createMenuListener(menuItem);
     111
     112         if (menuListener != null)
     113             ((JMenu)menuItem).addMenuListener(menuListener);
     114     }
     115
     116     protected void installKeyboardActions() {
     117         super.installKeyboardActions();
     118         updateMnemonicBinding();
     119     }
     120
     121     void installLazyActionMap() {
     122         LazyActionMap.installLazyActionMap(menuItem, BasicMenuUI.class,
     123                                            getPropertyPrefix() + ".actionMap");
     124     }
     125
     126     @SuppressWarnings("deprecation")
     127     void updateMnemonicBinding() {
     128         int mnemonic = menuItem.getModel().getMnemonic();
     129         int[] shortcutKeys = (int[])DefaultLookup.get(menuItem, this,
     130                                                    "Menu.shortcutKeys");
     131         if (shortcutKeys == null) {
     132             shortcutKeys = new int[] {KeyEvent.ALT_MASK,
     133                     KeyEvent.ALT_MASK | KeyEvent.ALT_GRAPH_MASK};
     134         }
     135         if (mnemonic == lastMnemonic) {
     136             return;
     137         }
     138         InputMap windowInputMap = SwingUtilities.getUIInputMap(
     139                        menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW);
     140         if (lastMnemonic != 0 && windowInputMap != null) {
     141             for (int shortcutKey : shortcutKeys) {
     142                 windowInputMap.remove(KeyStroke.getKeyStroke
     143                         (lastMnemonic, shortcutKey, false));
     144             }
     145         }
     146         if (mnemonic != 0) {
     147             if (windowInputMap == null) {
     148                 windowInputMap = createInputMap(JComponent.
     149                                               WHEN_IN_FOCUSED_WINDOW);
     150                 SwingUtilities.replaceUIInputMap(menuItem, JComponent.
     151                                        WHEN_IN_FOCUSED_WINDOW, windowInputMap);
     152             }
     153             for (int shortcutKey : shortcutKeys) {
     154                 windowInputMap.put(KeyStroke.getKeyStroke(mnemonic,
     155                         shortcutKey, false), "selectMenu");
     156             }
     157         }
     158         lastMnemonic = mnemonic;
     159     }
     160
     161     protected void uninstallKeyboardActions() {
     162         super.uninstallKeyboardActions();
     163         lastMnemonic = 0;
     164     }
     165
     166     protected MouseInputListener createMouseInputListener(JComponent c) {
     167         return getHandler();
     168     }
     169
     170     /**
     171      * Returns an instance of {@code MenuListener}.
     172      *
     173      * @param c a component
     174      * @return an instance of {@code MenuListener}
     175      */
     176     protected MenuListener createMenuListener(JComponent c) {
     177         return null;
     178     }
     179
     180     /**
     181      * Returns an instance of {@code ChangeListener}.
     182      *
     183      * @param c a component
     184      * @return an instance of {@code ChangeListener}
     185      */
     186     protected ChangeListener createChangeListener(JComponent c) {
     187         return null;
     188     }
     189
     190     protected PropertyChangeListener createPropertyChangeListener(JComponent c) {
     191         return getHandler();
     192     }
     193
     194     BasicMenuItemUI.Handler getHandler() {
     195         if (handler == null) {
     196             handler = new Handler();
     197         }
     198         return handler;
     199     }
     200
     201     protected void uninstallDefaults() {
     202         menuItem.setArmed(false);
     203         menuItem.setSelected(false);
     204         menuItem.resetKeyboardActions();
     205         super.uninstallDefaults();
     206     }
     207
     208     protected void uninstallListeners() {
     209         super.uninstallListeners();
     210
     211         if (changeListener != null)
     212             menuItem.removeChangeListener(changeListener);
     213
     214         if (menuListener != null)
     215             ((JMenu)menuItem).removeMenuListener(menuListener);
     216
     217         changeListener = null;
     218         menuListener = null;
     219         handler = null;
     220     }
     221
     222     protected MenuDragMouseListener createMenuDragMouseListener(JComponent c) {
     223         return getHandler();
     224     }
     225
     226     protected MenuKeyListener createMenuKeyListener(JComponent c) {
     227         return (MenuKeyListener)getHandler();
     228     }
     229
     230     public Dimension getMinimumSize(JComponent c) {
     231         return (((JMenu)menuItem).isTopLevelMenu()) ?
     232             c.getPreferredSize() : null;
     233     }
     234
     235     public Dimension getMaximumSize(JComponent c) {
     236         if (((JMenu)menuItem).isTopLevelMenu()) {
     237             Dimension d = c.getPreferredSize();
     238             return new Dimension(d.width, Short.MAX_VALUE);
     239         }
     240         return null;
     241     }
     242
     243     /**
     244      * Sets timer to the {@code menu}.
     245      *
     246      * @param menu an instance of {@code JMenu}.
     247      */
     248     protected void setupPostTimer(JMenu menu) {
     249         Timer timer = new Timer(menu.getDelay(), new Actions(
     250                                     Actions.SELECT, menu,false));
     251         timer.setRepeats(false);
     252         timer.start();
     253     }
     254
     255     private static void appendPath(MenuElement[] path, MenuElement elem) {
     256         MenuElement newPath[] = new MenuElement[path.length+1];
     257         System.arraycopy(path, 0, newPath, 0, path.length);
     258         newPath[path.length] = elem;
     259         MenuSelectionManager.defaultManager().setSelectedPath(newPath);
     260     }
     261
     262     private static class Actions extends UIAction {
     263         private static final String SELECT = "selectMenu";
     264
     265         // NOTE: This will be null if the action is registered in the
     266         // ActionMap. For the timer use it will be non-null.
     267         private JMenu menu;
     268         private boolean force=false;
     269
     270         Actions(String key, JMenu menu, boolean shouldForce) {
     271             super(key);
     272             this.menu = menu;
     273             this.force = shouldForce;
     274         }
     275
     276         private JMenu getMenu(ActionEvent e) {
     277             if (e.getSource() instanceof JMenu) {
     278                 return (JMenu)e.getSource();
     279             }
     280             return menu;
     281         }
     282
     283         public void actionPerformed(ActionEvent e) {
     284             JMenu menu = getMenu(e);
     285             if (!crossMenuMnemonic) {
     286                 JPopupMenu pm = BasicPopupMenuUI.getLastPopup();
     287                 if (pm != null && pm != menu.getParent()) {
     288                     return;
     289                 }
     290             }
     291
     292             final MenuSelectionManager defaultManager = MenuSelectionManager.defaultManager();
     293             if(force) {
     294                 Container cnt = menu.getParent();
     295                 if(cnt != null && cnt instanceof JMenuBar) {
     296                     MenuElement me[];
     297                     MenuElement subElements[];
     298
     299                     subElements = menu.getPopupMenu().getSubElements();
     300                     if(subElements.length > 0) {
     301                         me = new MenuElement[4];
     302                         me[0] = (MenuElement) cnt;
     303                         me[1] = menu;
     304                         me[2] = menu.getPopupMenu();
     305                         me[3] = subElements[0];
     306                     } else {
     307                         me = new MenuElement[3];
     308                         me[0] = (MenuElement)cnt;
     309                         me[1] = menu;
     310                         me[2] = menu.getPopupMenu();
     311                     }
     312                     defaultManager.setSelectedPath(me);
     313                 }
     314             } else {
     315                 MenuElement path[] = defaultManager.getSelectedPath();
     316                 if(path.length > 0 && path[path.length-1] == menu) {
     317                     appendPath(path, menu.getPopupMenu());
     318                 }
     319             }
     320         }
     321
     322         @Override
     323         public boolean accept(Object c) {
     324             if (c instanceof JMenu) {
     325                 return ((JMenu)c).isEnabled();
     326             }
     327             return true;
     328         }
     329     }
     330
     331     /*
     332      * Set the background color depending on whether this is a toplevel menu
     333      * in a menubar or a submenu of another menu.
     334      */
     335     private void updateDefaultBackgroundColor() {
     336         if (!UIManager.getBoolean("Menu.useMenuBarBackgroundForTopLevel")) {
     337            return;
     338         }
     339         JMenu menu = (JMenu)menuItem;
     340         if (menu.getBackground() instanceof UIResource) {
     341             if (menu.isTopLevelMenu()) {
     342                 menu.setBackground(UIManager.getColor("MenuBar.background"));
     343             } else {
     344                 menu.setBackground(UIManager.getColor(getPropertyPrefix() + ".background"));
     345             }
     346         }
     347     }
     348
     349     /**
     350      * Instantiated and used by a menu item to handle the current menu selection
     351      * from mouse events. A MouseInputHandler processes and forwards all mouse events
     352      * to a shared instance of the MenuSelectionManager.
     353      * <p>
     354      * This class is protected so that it can be subclassed by other look and
     355      * feels to implement their own mouse handling behavior. All overridden
     356      * methods should call the parent methods so that the menu selection
     357      * is correct.
     358      *
     359      * @see javax.swing.MenuSelectionManager
     360      * @since 1.4
     361      */
     362     protected class MouseInputHandler implements MouseInputListener {
     363         // NOTE: This class exists only for backward compatibility. All
     364         // its functionality has been moved into Handler. If you need to add
     365         // new functionality add it to the Handler, but make sure this
     366         // class calls into the Handler.
     367
     368         public void mouseClicked(MouseEvent e) {
     369             getHandler().mouseClicked(e);
     370         }
     371
     372         /**
     373          * Invoked when the mouse has been clicked on the menu. This
     374          * method clears or sets the selection path of the
     375          * MenuSelectionManager.
     376          *
     377          * @param e the mouse event
     378          */
     379         public void mousePressed(MouseEvent e) {
     380             getHandler().mousePressed(e);
     381         }
     382
     383         /**
     384          * Invoked when the mouse has been released on the menu. Delegates the
     385          * mouse event to the MenuSelectionManager.
     386          *
     387          * @param e the mouse event
     388          */
     389         public void mouseReleased(MouseEvent e) {
     390             getHandler().mouseReleased(e);
     391         }
     392
     393         /**
     394          * Invoked when the cursor enters the menu. This method sets the selected
     395          * path for the MenuSelectionManager and handles the case
     396          * in which a menu item is used to pop up an additional menu, as in a
     397          * hierarchical menu system.
     398          *
     399          * @param e the mouse event; not used
     400          */
     401         public void mouseEntered(MouseEvent e) {
     402             getHandler().mouseEntered(e);
     403         }
     404         public void mouseExited(MouseEvent e) {
     405             getHandler().mouseExited(e);
     406         }
     407
     408         /**
     409          * Invoked when a mouse button is pressed on the menu and then dragged.
     410          * Delegates the mouse event to the MenuSelectionManager.
     411          *
     412          * @param e the mouse event
     413          * @see java.awt.event.MouseMotionListener#mouseDragged
     414          */
     415         public void mouseDragged(MouseEvent e) {
     416             getHandler().mouseDragged(e);
     417         }
     418
     419         public void mouseMoved(MouseEvent e) {
     420             getHandler().mouseMoved(e);
     421         }
     422     }
     423
     424     /**
     425      * As of Java 2 platform 1.4, this previously undocumented class
     426      * is now obsolete. KeyBindings are now managed by the popup menu.
     427      */
     428     public class ChangeHandler implements ChangeListener {
     429         /**
     430          * The instance of {@code JMenu}.
     431          */
     432         public JMenu    menu;
     433
     434         /**
     435          * The instance of {@code BasicMenuUI}.
     436          */
     437         public BasicMenuUI ui;
     438
     439         /**
     440          * {@code true} if an item of popup menu is selected.
     441          */
     442         public boolean  isSelected = false;
     443
     444         /**
     445          * The component that was focused.
     446          */
     447         public Component wasFocused;
     448
     449         /**
     450          * Constructs a new instance of {@code ChangeHandler}.
     451          *
     452          * @param m an instance of {@code JMenu}
     453          * @param ui an instance of {@code BasicMenuUI}
     454          */
     455         public ChangeHandler(JMenu m, BasicMenuUI ui) {
     456             menu = m;
     457             this.ui = ui;
     458         }
     459
     460         public void stateChanged(ChangeEvent e) { }
     461     }
     462
     463     private class Handler extends BasicMenuItemUI.Handler implements MenuKeyListener {
     464         //
     465         // PropertyChangeListener
     466         //
     467         public void propertyChange(PropertyChangeEvent e) {
     468             if (e.getPropertyName() == AbstractButton.
     469                              MNEMONIC_CHANGED_PROPERTY) {
     470                 updateMnemonicBinding();
     471             }
     472             else {
     473                 if (e.getPropertyName().equals("ancestor")) {
     474                     updateDefaultBackgroundColor();
     475                 }
     476                 super.propertyChange(e);
     477             }
     478         }
     479
     480         //
     481         // MouseInputListener
     482         //
     483         public void mouseClicked(MouseEvent e) {
     484         }
     485
     486         /**
     487          * Invoked when the mouse has been clicked on the menu. This
     488          * method clears or sets the selection path of the
     489          * MenuSelectionManager.
     490          *
     491          * @param e the mouse event
     492          */
     493         public void mousePressed(MouseEvent e) {
     494             JMenu menu = (JMenu)menuItem;
     495             if (!menu.isEnabled())
     496                 return;
     497
     498             MenuSelectionManager manager =
     499                 MenuSelectionManager.defaultManager();
     500             if(menu.isTopLevelMenu()) {
     501                 if(menu.isSelected() && menu.getPopupMenu().isShowing()) {
     502                     manager.clearSelectedPath();
     503                 } else {
     504                     Container cnt = menu.getParent();
     505                     if(cnt != null && cnt instanceof JMenuBar) {
     506                         MenuElement me[] = new MenuElement[2];
     507                         me[0]=(MenuElement)cnt;
     508                         me[1]=menu;
     509                         manager.setSelectedPath(me);
     510                     }
     511                 }
     512             }
     513
     514             MenuElement selectedPath[] = manager.getSelectedPath();
     515             if (selectedPath.length > 0 &&
     516                 selectedPath[selectedPath.length-1] != menu.getPopupMenu()) {
     517
     518                 if(menu.isTopLevelMenu() ||
     519                    menu.getDelay() == 0) {
     520                     appendPath(selectedPath, menu.getPopupMenu());
     521                 } else {
     522                     setupPostTimer(menu);
     523                 }
     524             }
     525         }
     526
     527         /**
     528          * Invoked when the mouse has been released on the menu. Delegates the
     529          * mouse event to the MenuSelectionManager.
     530          *
     531          * @param e the mouse event
     532          */
     533         public void mouseReleased(MouseEvent e) {
     534             JMenu menu = (JMenu)menuItem;
     535             if (!menu.isEnabled())
     536                 return;
     537             MenuSelectionManager manager =
     538                 MenuSelectionManager.defaultManager();
     539             manager.processMouseEvent(e);
     540             if (!e.isConsumed())
     541                 manager.clearSelectedPath();
     542         }
     543
     544         /**
     545          * Invoked when the cursor enters the menu. This method sets the selected
     546          * path for the MenuSelectionManager and handles the case
     547          * in which a menu item is used to pop up an additional menu, as in a
     548          * hierarchical menu system.
     549          *
     550          * @param e the mouse event; not used
     551          */
     552         public void mouseEntered(MouseEvent e) {
     553             JMenu menu = (JMenu)menuItem;
     554             // only disable the menu highlighting if it's disabled and the property isn't
     555             // true. This allows disabled rollovers to work in WinL&F
     556             if (!menu.isEnabled() && !UIManager.getBoolean("MenuItem.disabledAreNavigable")) {
     557                 return;
     558             }
     559
     560             MenuSelectionManager manager =
     561                 MenuSelectionManager.defaultManager();
     562             MenuElement selectedPath[] = manager.getSelectedPath();
     563             if (!menu.isTopLevelMenu()) {
     564                 if(!(selectedPath.length > 0 &&
     565                      selectedPath[selectedPath.length-1] ==
     566                      menu.getPopupMenu())) {
     567                     if(menu.getDelay() == 0) {
     568                         appendPath(getPath(), menu.getPopupMenu());
     569                     } else {
     570                         manager.setSelectedPath(getPath());
     571                         setupPostTimer(menu);
     572                     }
     573                 }
     574             } else {
     575                 if(selectedPath.length > 0 &&
     576                    selectedPath[0] == menu.getParent()) {
     577                     MenuElement newPath[] = new MenuElement[3];
     578                     // A top level menu's parent is by definition
     579                     // a JMenuBar
     580                     newPath[0] = (MenuElement)menu.getParent();
     581                     newPath[1] = menu;
     582                     if (BasicPopupMenuUI.getLastPopup() != null) {
     583                         newPath[2] = menu.getPopupMenu();
     584                     }
     585                     manager.setSelectedPath(newPath);
     586                 }
     587             }
     588         }
     589         public void mouseExited(MouseEvent e) {
     590         }
     591
     592         /**
     593          * Invoked when a mouse button is pressed on the menu and then dragged.
     594          * Delegates the mouse event to the MenuSelectionManager.
     595          *
     596          * @param e the mouse event
     597          * @see java.awt.event.MouseMotionListener#mouseDragged
     598          */
     599         public void mouseDragged(MouseEvent e) {
     600             JMenu menu = (JMenu)menuItem;
     601             if (!menu.isEnabled())
     602                 return;
     603             MenuSelectionManager.defaultManager().processMouseEvent(e);
     604         }
     605         public void mouseMoved(MouseEvent e) {
     606         }
     607
     608
     609         //
     610         // MenuDragHandler
     611         //
     612         public void menuDragMouseEntered(MenuDragMouseEvent e) {}
     613         public void menuDragMouseDragged(MenuDragMouseEvent e) {
     614             if (menuItem.isEnabled() == false)
     615                 return;
     616
     617             MenuSelectionManager manager = e.getMenuSelectionManager();
     618             MenuElement path[] = e.getPath();
     619
     620             Point p = e.getPoint();
     621             if(p.x >= 0 && p.x < menuItem.getWidth() &&
     622                p.y >= 0 && p.y < menuItem.getHeight()) {
     623                 JMenu menu = (JMenu)menuItem;
     624                 MenuElement selectedPath[] = manager.getSelectedPath();
     625                 if(!(selectedPath.length > 0 &&
     626                      selectedPath[selectedPath.length-1] ==
     627                      menu.getPopupMenu())) {
     628                     if(menu.isTopLevelMenu() ||
     629                        menu.getDelay() == 0  ||
     630                        e.getID() == MouseEvent.MOUSE_DRAGGED) {
     631                         appendPath(path, menu.getPopupMenu());
     632                     } else {
     633                         manager.setSelectedPath(path);
     634                         setupPostTimer(menu);
     635                     }
     636                 }
     637             } else if(e.getID() == MouseEvent.MOUSE_RELEASED) {
     638                 Component comp = manager.componentForPoint(e.getComponent(), e.getPoint());
     639                 if (comp == null)
     640                     manager.clearSelectedPath();
     641             }
     642
     643         }
     644         public void menuDragMouseExited(MenuDragMouseEvent e) {}
     645         public void menuDragMouseReleased(MenuDragMouseEvent e) {}
     646
     647         //
     648         // MenuKeyListener
     649         //
     650         /**
     651          * Open the Menu
     652          */
     653         public void menuKeyTyped(MenuKeyEvent e) {
     654             if (!crossMenuMnemonic && BasicPopupMenuUI.getLastPopup() != null) {
     655                 // when crossMenuMnemonic is not set, we don't open a toplevel
     656                 // menu if another toplevel menu is already open
     657                 return;
     658             }
     659
     660             if (BasicPopupMenuUI.getPopups().size() != 0) {
     661                 //Fix 6939261: to return in case not on the main menu
     662                 //and has a pop-up.
     663                 //after return code will be handled in BasicPopupMenuUI.java
     664                 return;
     665             }
     666
     667             char key = Character.toLowerCase((char)menuItem.getMnemonic());
     668             MenuElement path[] = e.getPath();
     669             if (key == Character.toLowerCase(e.getKeyChar())) {
     670                 JPopupMenu popupMenu = ((JMenu)menuItem).getPopupMenu();
     671                 ArrayList<MenuElement> newList = new ArrayList<>(Arrays.asList(path));
     672                 newList.add(popupMenu);
     673                 MenuElement subs[] = popupMenu.getSubElements();
     674                 MenuElement sub =
     675                         BasicPopupMenuUI.findEnabledChild(subs, -1, true);
     676                 if(sub != null) {
     677                     newList.add(sub);
     678                 }
     679                 MenuSelectionManager manager = e.getMenuSelectionManager();
     680                 MenuElement newPath[] = new MenuElement[0];;
     681                 newPath = newList.toArray(newPath);
     682                 manager.setSelectedPath(newPath);
     683                 e.consume();
     684             }
     685         }
     686
     687         public void menuKeyPressed(MenuKeyEvent e) {}
     688         public void menuKeyReleased(MenuKeyEvent e) {}
     689     }
     690 }
    --- EOF ---