001/*
002 * Copyright (c) 1998-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2015-08-24 22:44:14 +0000 (Mon, 24 Aug 2015) $' 
007 * '$Revision: 33630 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.kepler.gui;
031
032import java.awt.Component;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.lang.reflect.Constructor;
036import java.util.Collections;
037import java.util.HashSet;
038import java.util.Iterator;
039import java.util.LinkedHashMap;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043
044import javax.swing.AbstractAction;
045import javax.swing.Action;
046import javax.swing.JCheckBoxMenuItem;
047import javax.swing.JComponent;
048import javax.swing.JMenu;
049import javax.swing.JMenuBar;
050import javax.swing.JMenuItem;
051
052import org.apache.commons.logging.Log;
053import org.apache.commons.logging.LogFactory;
054import org.kepler.build.modules.ModuleTree;
055import org.kepler.configuration.ConfigurationManager;
056import org.kepler.configuration.ConfigurationNamespace;
057import org.kepler.configuration.ConfigurationProperty;
058
059import diva.gui.GUIUtilities;
060import diva.gui.toolbox.JContextMenu;
061import ptolemy.actor.gui.TableauFrame;
062import ptolemy.gui.MemoryCleaner;
063
064/**
065 * MenuMapper
066 * 
067 * A menu creation Runnable that creates and adds a new JMenuBar that provides a
068 * 'view' (in a loose sense) of the ptii menus. It enumerates all the existing
069 * ptii menu items at runtime, and re-uses them, rearranged (and optionally
070 * renamed) - all set via mappings in a localizable resourcebundle properties
071 * (text) file.
072 * 
073 * If/when new menu items get added to ptii, they are immediately available for
074 * use here, just by adding the relevant text mapping to the properties file.
075 * 
076 * 
077 * @author Matthew Brooke
078 * @version $Id: MenuMapper.java 33630 2015-08-24 22:44:14Z crawl $
079 * @since
080 * @Pt.ProposedRating
081 * @Pt.AcceptedRating
082 */
083public class MenuMapper {
084
085        // /////////////////////////////////////////////////////////////////
086        // // public variables ////
087
088        public final static String MENUITEM_TYPE = "MENUITEM_TYPE";
089
090        public final static String CHECKBOX_MENUITEM_TYPE = "CHECKBOX_MENUITEM_TYPE";
091
092        public final static String NEW_JMENUITEM_KEY = "NEW_JMENUITEM";
093
094        // /////////////////////////////////////////////////////////////////
095        // // public methods ////
096
097        /**
098         * Debugging method
099         */
100        public void printDebugInfo() {
101                System.out.println("_ptiiMenuActionsMap: " + _ptiiMenuActionsMap.size());
102                for (String s : _ptiiMenuActionsMap.keySet()) {
103                        Action a = _ptiiMenuActionsMap.get(s);
104                        System.out.println(s + " : " + a.toString());
105                }
106        }
107        
108        /**
109         * constructor
110         * 
111         * @param ptiiMenubar
112         *            JMenuBar the existing ptii menubar containing the original
113         *            menus
114         * @param tableauFrameInstance
115         *            TableauFrame the frame from which the ptii menu bar will be
116         *            hidden, and to which the new menu bar will be added
117         */
118        public MenuMapper(final TableauFrame tableauFrameInstance) {
119                this._tableauFrameInstance = tableauFrameInstance;
120                _mappers.add(this);
121                init();
122        }
123
124        public void init() {
125
126                if (_tableauFrameInstance == null) {
127                        log.error("MenuMapper cannot proceed, due to one or more NULL values:"
128                                        + "\nptiiMenubar = "
129                                        + _ptiiMenubar
130                                        + "\ntableauFrameInstance = "
131                                        + _tableauFrameInstance
132                                        + "\ndefaulting to PTII menus");
133
134                        return;
135                }
136
137                // First, we need to make sure PTII has finished constructing its menus.
138                // Those menus are created and assembled in a thread that is started in
139                // the
140                // pack() method of ptolemy.gui.Top. As that thread finishes, it adds
141                // the
142                // new ptii JMenuBar to the frame - so we test for that here, and don't
143                // proceed until the frame has the new JMenuBar added
144                // For safety, so we don't have an infinite loop, we also set a safety
145                // counter:
146                // maxWaitMS is the longest the app waits for the ptii
147                // menus to be added, before it continues anyway.
148                final int maxWaitMS = 500;
149                // sleepTimeMS is the amount of time it sleeps per loop:
150                final int sleepTimeMS = 5;
151                // ////
152                final int maxLoops = maxWaitMS / sleepTimeMS;
153                int safetyCounter = 0;
154
155                while (safetyCounter++ < maxLoops
156                                && !_tableauFrameInstance.isMenuPopulated()) {
157
158                        if (isDebugging) {
159                                log.debug("Waiting for PTII menus to be created... "
160                                                + safetyCounter);
161                        }
162                        try {
163                                Thread.sleep(sleepTimeMS);
164                        } catch (Exception e) {
165                                // ignore
166                        }
167                }
168                
169                JMenuBar _keplerMenubar = null;
170                if(_ptiiMenubar == null) {
171                        _ptiiMenubar = _tableauFrameInstance.getJMenuBar();
172                }
173                
174                if (_ptiiMenubar != null) {
175                        
176                        // gets here if a PTII menubar has been added to frame...
177
178                        // 1) Now PTII has finished constructing its menus, get all
179                        // menu items and put them in a Map for easier access later...
180                        _ptiiMenuActionsMap = getPTIIMenuActionsMap();
181                        Map<String, Action> ptiiMenuActionsMap = _ptiiMenuActionsMap;
182
183                        // 2) Now we have all the PTII menu items, get the
184                        // Kepler-specific menu mappings from the preferences file,
185                        // then go thru the Kepler menu mappings and
186                        // populate the new menubar with Kepler menus,
187                        // creating any new menu items that don't exist yet
188
189                        // this is a Map that will be used to keep track of
190                        // what we have added to the menus, and in what order
191                        _keplerMenubar = createKeplerMenuBar(ptiiMenuActionsMap);
192
193                        if (_keplerMenubar != null) {
194
195                                // First, look to see if any menus are empty. If
196                                // they are, remove the top-level menu from the menubar...
197                                // ** NOTE - do these by counting *down* to zero, otherwise the
198                                // menus'
199                                // indices change dynamically as we remove menus, causing
200                                // errors!
201                                for (int m = _keplerMenubar.getMenuCount() - 1; m >= 0; m--) {
202                                        JMenu nextMenu = _keplerMenubar.getMenu(m);
203                                        if (nextMenu.getMenuComponentCount() < 1) {
204                                                if (isDebugging) {
205                                                        log.debug("deleting empty menu: "
206                                                                        + nextMenu.getText());
207                                                }
208                                                _keplerMenubar.remove(nextMenu);
209                                        }
210                                }
211                                // hide the ptii menubar
212                                _tableauFrameInstance.hideMenuBar();
213
214                                // add the new menu bar
215                                _tableauFrameInstance.setJMenuBar(_keplerMenubar);
216                        } else {
217                                log.error("Problem creating Kepler menus - defaulting to PTII menus");
218                        }
219
220                } else {
221                        // gets here if a PTII menubar has *NOT* been added to frame...
222                        // Therefore, this frame doesn't have a menubar by default,
223                        // so we probably shouldn't add one, for now, at least
224
225                        // hide the ptii menubar (may not be necessary)
226                        _tableauFrameInstance.hideMenuBar();
227
228                        // add the new menu bar (may not be necessary)
229                        _tableauFrameInstance.setJMenuBar(null);
230                }
231        }
232
233        public static Action getActionFor(String key,
234                        Map<String, Action> menuActionsMap,
235                        TableauFrame tableauFrameInstance) {
236                if (isDebugging) {
237                        log.debug("getActionFor(" + key + "," + menuActionsMap.toString()
238                                        + ")");
239                }
240
241                Action action = null;
242
243                // it's a mapping...
244                // NOTE that all keys in ptiiMenuActionsMap are
245                // uppercase, to make the ptii value entries in the
246                // menu mappings props file case-insensitive
247                String uKey = key.toUpperCase();
248
249                if (key.indexOf(MENU_PATH_DELIMITER) > -1) {
250                        Object val = menuActionsMap.get(uKey);
251                        if (val instanceof Action) {
252                                action = (Action) val;
253                        }
254                } else {
255                        // it's a class or a separator
256
257                        if (uKey.equals(MENU_SEPARATOR_KEY.toUpperCase())) {
258
259                                // it's a separator
260                                return null;
261
262                        } else {
263
264                                // it's a class - try to instantiate...
265                                Object actionObj = null;
266                                boolean actionInitialized = true;
267                                try {
268                                        // create the class
269                                        String classSearchName = key.trim();
270                                        ClassLoader thisClassLoader = MenuMapper.class.getClassLoader();
271                                        Class<?> classDefinition = thisClassLoader.loadClass(classSearchName);
272
273                                        // add the arg types
274                                        Class<?>[] args = new Class[] { TableauFrame.class };
275
276                                        // create a constructor
277                                        Constructor<?> constructor;
278                                        try {
279                                                constructor = classDefinition.getConstructor(args);
280                                        } catch (java.lang.NoSuchMethodException nsme) {
281                                                args = new Class[] { ptolemy.vergil.basic.BasicGraphFrame.class };
282                                                constructor = classDefinition.getConstructor(args);
283                                        }
284
285                                        // set the args
286                                        Object[] argImp = new Object[] { tableauFrameInstance };
287
288                                        // create the object
289                                        //if (isDebugging) log.debug("BEFORE");
290                                        actionObj = constructor.newInstance(argImp);
291                                        //if (isDebugging) log.debug("AFTER");
292
293                                        // if the action object is an InitializableAction, try to
294                                        // initialize it. 
295                                        if(actionObj instanceof InitializableAction) {
296                                            actionInitialized = ((InitializableAction)actionObj).initialize(menuActionsMap);
297                                        }
298                                        
299                                } catch (Exception e) {
300                                        log.warn("Exception trying to create an Action for classname: <"
301                                                        + key + ">:\n" + e.getCause() + " (" + e + ")");
302                                        actionObj = null;
303                                }
304
305                                if (actionObj == null) {
306                                        // System.out.println("2Error creating action for class: " +
307                                        // key);
308                                        if (isDebugging) {
309                                                log.error("Problem trying to create an Action for classname: <"
310                                                                + key
311                                                                + ">\nPossible reasons:\n"
312                                                                + "1) Should be a fully-qualified classname, including "
313                                                                + "the package - Check carefully for mistakes.\n"
314                                                                + "2) class must implement javax.swing.Action, and must "
315                                                                + "have a constructor of the form: \n"
316                                                                + "  MyConstructor(ptolemy.actor.gui.TableauFrame)\n"
317                                                                + "Returning NULL Action for classname: " + key);
318                                        }
319                                        return null;
320                                // see if the initialization failed. if so, set the actionObj
321                                // to null so this action is not put on the menu bar.
322                                } else if(!actionInitialized) {
323                                    actionObj = null;
324                }
325                                action = (Action) actionObj;
326                        }
327                }
328                return action;
329        }
330
331        /**
332         * Recurse through all the submenu heirarchy beneath the passed JMenu
333         * parameter, and for each "leaf node" (ie a menu item that is not a
334         * container for other menu items), add the Action and its menu path to the
335         * passed Map
336         * 
337         * @param nextMenuItem
338         *            the JMenu to recurse into
339         * @param menuPathBuff
340         *            a delimited String representation of the hierarchical "path"
341         *            to this menu item. This will be used as the key in the
342         *            actionsMap. For example, the "Graph Editor" menu item beneath
343         *            the "New" item on the "File" menu would have a menuPath of
344         *            File->New->Graph Editor. Delimeter is "->" (no quotes), and
345         *            spaces are allowed within menu text strings, but not around
346         *            the delimiters; i.e: New->Graph Editor is OK, but File ->New
347         *            is not.
348         * @param MENU_PATH_DELIMITER
349         *            String
350         * @param actionsMap
351         *            the Map containing key => value pairs of the form: menuPath
352         *            (as described above) => Action (the javax.swing.Action
353         *            assigned to this menu item)
354         */
355        public static void storePTIIMenuItems(JMenuItem nextMenuItem,
356                        StringBuffer menuPathBuff, final String MENU_PATH_DELIMITER,
357                        Map<String, Action> actionsMap) {
358
359                menuPathBuff.append(MENU_PATH_DELIMITER);
360                if (nextMenuItem != null && nextMenuItem.getText() != null) {
361                        String str = nextMenuItem.getText();
362                        // do not make the recent files menu item upper case since
363                        // it contains paths in the file system.
364                        if (menuPathBuff.toString().startsWith("FILE->RECENT FILES->")) {
365                                menuPathBuff = new StringBuffer("File->Recent Files->");
366                        } else {
367                                str = str.toUpperCase();
368                        }
369                        menuPathBuff.append(str);                       
370                }
371
372                if(isDebugging) {
373                        log.debug(menuPathBuff.toString());
374                }
375                // System.out.println(menuPathBuff.toString());
376
377                if (nextMenuItem instanceof JMenu) {
378                        storePTIITopLevelMenus((JMenu) nextMenuItem,
379                                        menuPathBuff.toString(), MENU_PATH_DELIMITER, actionsMap);
380                } else {
381                        Action nextAction = nextMenuItem.getAction();
382                        // if there is no Action, look for an ActionListener
383                        // System.out.println("Processing menu " + nextMenuItem.getText());
384                        if (nextAction == null) {
385                                final ActionListener[] actionListeners = nextMenuItem
386                                                .getActionListeners();
387                                // System.out.println("No Action for " + nextMenuItem.getText()
388                                // + "; found " + actionListeners.length
389                                // + " ActionListeners");
390                                if (actionListeners.length > 0) {
391                                        if (isDebugging) {
392                                                log.debug(actionListeners[0].getClass().getName());
393                                        }
394                                        // ASSUMPTION: there is only one ActionListener
395                                        nextAction = new AbstractAction() {
396                                                public void actionPerformed(ActionEvent a) {
397                                                        actionListeners[0].actionPerformed(a);
398                                                }
399                                        };
400                                        // add all these values - @see diva.gui.GUIUtilities
401                                        nextAction.putValue(Action.NAME, nextMenuItem.getText());
402                                        // System.out.println("storing ptII action for menu " +
403                                        // nextMenuItem.getText());
404                                        nextAction.putValue(GUIUtilities.LARGE_ICON,
405                                                        nextMenuItem.getIcon());
406                                        nextAction.putValue(GUIUtilities.MNEMONIC_KEY, new Integer(
407                                                        nextMenuItem.getMnemonic()));
408                                        nextAction.putValue("tooltip",
409                                                        nextMenuItem.getToolTipText());
410                                        nextAction.putValue(GUIUtilities.ACCELERATOR_KEY,
411                                                        nextMenuItem.getAccelerator());
412                                        nextAction.putValue("menuItem", nextMenuItem);
413                                } else {
414                                        if (isDebugging) {
415                                                log.warn("No Action or ActionListener found for "
416                                                                + nextMenuItem.getText());
417                                        }
418                                }
419                        }
420                        if (!actionsMap.containsValue(nextAction)) {
421                                actionsMap.put(menuPathBuff.toString(), nextAction);
422                                if (isDebugging) {
423                                        log.debug(menuPathBuff.toString() + " :: ACTION: "
424                                                        + nextAction);
425                                }
426                        }
427                }
428        }
429
430        // /////////////////////////////////////////////////////////////////
431        // // private methods ////
432
433        private JMenuBar createKeplerMenuBar(Map<String, Action> ptiiMenuActionsMap) {
434
435                final LinkedHashMap<String, JMenuItem> keplerMenuMap = new LinkedHashMap<String, JMenuItem>();
436                final JMenuBar _keplerMenubar = new JMenuBar();
437
438                if(isDebugging) {
439                        log.debug("***************\nKEPLER MENUS:\n***************\n");
440                }
441
442                Iterator<ConfigurationProperty> it = null;
443                try {
444
445                        ConfigurationProperty prop = ConfigurationManager.getInstance()
446                                        .getProperty(ConfigurationManager.getModule("gui"),
447                                                        new ConfigurationNamespace("uiMenuMappings"));
448
449                        if (prop == null) {
450                                log.warn("Could not load uiMenuMappings configuration file.");
451                                return null;
452                        }
453
454                        List<ConfigurationProperty> reposList = prop.getProperties("name",
455                                        true);
456                        it = reposList.iterator();
457
458                        if (it == null) {
459                                log.warn("Menu mappings do not contain any valid assignments");
460                                return null;
461                        }
462
463                        separatorJustAdded = false;
464
465                        while (it.hasNext()) {
466
467                                ConfigurationProperty cp = it.next();
468                                String nextKey = cp.getValue();
469                                String nextVal = cp.getParent().getProperty("value").getValue();
470                                ConfigurationProperty moduleProperty = cp.getParent()
471                                                .getProperty("module");
472                                if (moduleProperty != null) {
473                                        String nextModule = moduleProperty.getValue();
474                                        ModuleTree mt = ModuleTree.instance();
475                                        if (!mt.contains(nextModule)) {
476                                                if(isDebugging) {
477                                                        log.debug("Skipping this entry (" + nextKey
478                                                                + ") because '" + nextModule
479                                                                + "' is not active");
480                                                }
481                                                continue;
482                                        } else if(isDebugging) {
483                                                log.debug("Processing this entry (" + nextKey
484                                                                + ") explicitly because '" + nextModule
485                                                                + "' is active");
486                                        }
487                                } else if(isDebugging) {
488                                        log.debug(nextKey + " has no module associated with it");
489                                }
490                                // String nextKey = (String) (it.next());
491
492                                if (isDebugging) {
493                                        log.debug("nextKey: " + nextKey);
494                                }
495                                if (nextKey == null || nextKey.trim().length() < 1) {
496                                        continue;
497                                }
498
499                                if (isDebugging) {
500                                        log.debug("nextVal: " + nextVal);
501                                }
502
503                                if (nextVal == null || nextVal.trim().length() < 1) {
504                                        log.warn("no menu mapping found for key: " + nextKey);
505                                        continue;
506                                }
507
508                                Action action = null;
509
510                                if (nextKey.indexOf(MENU_SEPARATOR_KEY) < 0) {
511
512                                        // if it's the recent files menu item, add all
513                                        // the existing entries.
514                                        // NOTE: we can't put the entries in the menu mappings
515                                        // configuration file since they are dynamic.
516                                        if (nextVal.equals("File->Recent Files")) {
517                                                for (Map.Entry<String, Action> entry : ptiiMenuActionsMap
518                                                                .entrySet()) {
519                                                        if (entry.getKey().startsWith(
520                                                                        "File->Recent Files->")) {
521                                                                addMenuFor(entry.getKey(), entry.getValue(),
522                                                                                _keplerMenubar, keplerMenuMap);
523                                                                //System.out.println("adding recent entry: " + entry.getKey());
524                                                        }
525                                                }
526                                                continue;
527                                        }
528
529                                        action = getActionFor(nextVal, ptiiMenuActionsMap,
530                                                        _tableauFrameInstance);
531
532                                        if (action == null) {
533                                                if (isDebugging) {
534                                                        log.warn("null action for value " + nextVal);
535                                                }
536                                                continue;
537                                        }
538                                        // action exists, and it's not a separator
539                                        // menuItemCount++;
540                                }
541
542                                addMenuFor(nextKey, action, _keplerMenubar, keplerMenuMap);
543                                // prevTopLevel
544                                // = _keplerMenubar.getMenu(_keplerMenubar.getMenuCount() -
545                                // 1).getText();
546                        }
547
548                } catch (Exception ex) {
549                        if (isDebugging) {
550                                log.warn("Exception creating Kepler menus: " + ex
551                                                + "\nDefaulting to PTII menus");
552                                if (isDebugging) {
553                                        ex.printStackTrace();
554                                }
555                                return _keplerMenubar;
556                        }
557                }
558
559                separatorJustAdded = false;
560
561                if(isDebugging) {
562                        log.debug("***************\nEND KEPLER MENUS:\n***************\n");
563                }
564                return _keplerMenubar;
565        }
566
567        public Map<String, Action> getPTIIMenuActionsMap() {
568
569                if (_ptiiMenubar == null) {
570                        _ptiiMenubar = _tableauFrameInstance.getJMenuBar();
571                }
572
573                if (_ptiiMenuActionsMap == null || _reloadPtolemyMenus) {
574                        if(isDebugging) {
575                                log.debug("**************\nEXISTING PTII MENUS:\n**************\n");
576                        }
577
578                        // NOTE: use a LinkedHashMap to preserve the order of the
579                        // recently opened files list
580                        _ptiiMenuActionsMap = new LinkedHashMap<String, Action>();
581
582                        for (int m = 0; m < _ptiiMenubar.getMenuCount(); m++) {
583                                JMenu nextMenu = _ptiiMenubar.getMenu(m);
584
585                                storePTIITopLevelMenus(nextMenu, nextMenu.getText()
586                                                .toUpperCase(), MENU_PATH_DELIMITER,
587                                                _ptiiMenuActionsMap);
588                        }
589                        _reloadPtolemyMenus = false;
590                        if(isDebugging) {
591                                log.debug("\n**************\nEND PTII MENUS:\n*****************\n");
592                        }
593                }
594                return _ptiiMenuActionsMap;
595        }
596
597        private static void storePTIITopLevelMenus(JMenu nextMenu, String menuPath,
598                        final String MENU_PATH_DELIMITER,
599                        Map<String, Action> ptiiMenuActionsMap) {
600
601                int totMenuItems = nextMenu.getMenuComponentCount();
602
603                for (int n = 0; n < totMenuItems; n++) {
604                        Component nextComponent = nextMenu.getMenuComponent(n);
605                        if (nextComponent instanceof JMenuItem) {
606                                storePTIIMenuItems((JMenuItem) nextComponent, new StringBuffer(
607                                                menuPath), MENU_PATH_DELIMITER, ptiiMenuActionsMap);                    
608                        }
609                        // (if it's not an instanceof JMenuItem, it must
610                        // be a separator, and can therefore be ignored)
611                }
612        }
613
614        public static JMenuItem addMenuFor(String key, Action action,
615                        JComponent topLvlContainer, Map<String, JMenuItem> keplerMenuMap) {
616
617                if (topLvlContainer == null) {
618                        if(isDebugging) {
619                                log.debug("NULL container received (eg JMenuBar) - returning NULL");
620                        }
621                        return null;
622                }
623                if (key == null) {
624                        if(isDebugging) {
625                                log.debug("NULL key received");
626                        }
627                        return null;
628                }
629                key = key.trim();
630
631                if (key.length() < 1) {
632                        if(isDebugging) {
633                                log.debug("BLANK key received");
634                        }
635                        return null;
636                }
637                if (action == null && key.indexOf(MENU_SEPARATOR_KEY) < 0) {
638                        if (isDebugging) {
639                                log.debug("NULL action received, but was not a separator: "
640                                                + key);
641                        }
642                        return null;
643                }
644
645                if (keplerMenuMap.containsKey(key)) {
646                        if (isDebugging) {
647                                log.debug("Menu already added; skipping: " + key);
648                        }
649                        return null;
650                }
651
652                // split delimited parts and ensure menus all exist
653                String[] menuLevel = key.split(MENU_PATH_DELIMITER);
654
655                int totLevels = menuLevel.length;
656
657                // create a menu for each "menuLevel" if it doesn't already exist
658                final StringBuffer nextLevelBuff = new StringBuffer();
659                String prevLevelStr = null;
660                JMenuItem leafMenuItem = null;
661
662                for (int levelIdx = 0; levelIdx < totLevels; levelIdx++) {
663
664                        // save previous value
665                        prevLevelStr = nextLevelBuff.toString();
666
667                        String nextLevelStr = menuLevel[levelIdx];
668                        // get the index of the first MNEMONIC_SYMBOL
669                        int mnemonicIdx = nextLevelStr.indexOf(MNEMONIC_SYMBOL);
670                        char mnemonicChar = 0;
671
672                        // if an MNEMONIC_SYMBOL exists, remove all underscores. Then, idx
673                        // of
674                        // first underscore becomes idx of letter it used to precede - this
675                        // is the mnemonic letter
676                        if (mnemonicIdx > -1) {
677                                nextLevelStr = nextLevelStr.replaceAll(MNEMONIC_SYMBOL, "");
678                                mnemonicChar = nextLevelStr.charAt(mnemonicIdx);
679                        }
680                        if (levelIdx != 0) {
681                                nextLevelBuff.append(MENU_PATH_DELIMITER);
682                        }
683                        nextLevelBuff.append(nextLevelStr);
684
685                        // don't add multiple separators together...
686                        if (nextLevelStr.indexOf(MENU_SEPARATOR_KEY) > -1) {
687                                if (separatorJustAdded == false) {
688
689                                        // Check if we're at the top level, since this makes sense
690                                        // only for
691                                        // context menu - we can't add a separator to a JMenuBar
692                                        if (levelIdx == 0) {
693                                                if (topLvlContainer instanceof JContextMenu) {
694                                                        ((JContextMenu) topLvlContainer).addSeparator();
695                                                }
696                                        } else {
697                                                JMenu parent = (JMenu) keplerMenuMap.get(prevLevelStr);
698
699                                                if (parent != null) {
700                                                        if (parent.getMenuComponentCount() < 1) {
701                                                                if(isDebugging) {
702                                                                        log.debug("------ NOT adding separator to parent "
703                                                                                + parent.getText()
704                                                                                + ", since it does not contain any menu items");
705                                                                }
706                                                        } else {
707                                                                if(isDebugging) {
708                                                                        log.debug("------ adding separator to parent "
709                                                                                + parent.getText());
710                                                                }
711                                                                // add separator to parent
712                                                                parent.addSeparator();
713                                                                separatorJustAdded = true;
714                                                        }
715                                                }
716                                        }
717                                }
718                        } else if (!keplerMenuMap.containsKey(nextLevelBuff.toString())) {
719                                // If menu has not already been created, we need
720                                // to create it and then add it to the parent level...
721
722                                JMenuItem menuItem = null;
723
724                                // if we're at a "leaf node" - need to create a JMenuItem
725                                if (levelIdx == totLevels - 1) {
726
727                                        // save old display name to use as actionCommand on
728                                        // menuitem,
729                                        // since some parts of PTII still
730                                        // use "if (actionCommand.equals("SaveAs")) {..." etc
731                                        String oldDisplayName = (String) action
732                                                        .getValue(Action.NAME);
733
734                                        // action.putValue(Action.NAME, nextLevelStr);
735
736                                        if (mnemonicChar > 0) {
737                                                action.putValue(GUIUtilities.MNEMONIC_KEY, new Integer(
738                                                                mnemonicChar));
739                                        }
740
741                                        // Now we look to see if it's a checkbox
742                                        // menu item, or just a regular one
743                                        String menuItemType = (String) (action
744                                                        .getValue(MENUITEM_TYPE));
745
746                                        if (menuItemType != null
747                                                        && menuItemType == CHECKBOX_MENUITEM_TYPE) {
748                                                menuItem = new JCheckBoxMenuItem(action);
749                                        } else {
750                                                menuItem = new JMenuItem(action);
751                                        }
752
753                                        // --------------------------------------------------------------
754                                        /** @todo - setting menu names - TEMPORARY FIX - FIXME */
755                                        // Currently, if we use the "proper" way of setting menu
756                                        // names -
757                                        // ie by using action.putValue(Action.NAME, "somename");,
758                                        // then
759                                        // the name appears on the port buttons on the toolbar,
760                                        // making
761                                        // them huge. As a temporary stop-gap, I am just setting the
762                                        // new
763                                        // display name using setText() instead of
764                                        // action.putValue(..,
765                                        // but this needs to be fixed elsewhere - we want to be able
766                                        // to
767                                        // use action.putValue(Action.NAME (ie uncomment the line
768                                        // above
769                                        // that reads:
770                                        // action.putValue(Action.NAME, nextLevelStr);
771                                        // and delete the line below that reads:
772                                        // menuItem.setText(nextLevelStr);
773                                        // otherwise this may bite us in future...
774                                        menuItem.setText(nextLevelStr);
775                                        // --------------------------------------------------------------
776
777                                        // set old display name as actionCommand on
778                                        // menuitem, for ptii backward-compatibility
779                                        menuItem.setActionCommand(oldDisplayName);
780
781                                        // add JMenuItem to the Action, so it can be accessed by
782                                        // Action code
783                                        action.putValue(NEW_JMENUITEM_KEY, menuItem);
784                                        leafMenuItem = menuItem;
785                                } else {
786                                        // if we're *not* at a "leaf node" - need to create a JMenu
787                                        menuItem = new JMenu(nextLevelStr);
788                                        if (mnemonicChar > 0) {
789                                                menuItem.setMnemonic(mnemonicChar);
790                                        }
791                                }
792                                // level 0 is a special case, since the container (JMenuBar or
793                                // JContextMenu) is not a JMenu or a JMenuItem, so we can't
794                                // use the same code to add child to parent...
795                                if (levelIdx == 0) {
796                                        if (topLvlContainer instanceof JMenuBar) {
797                                                // this handles JMenuBar menus
798                                                ((JMenuBar) topLvlContainer).add(menuItem);
799                                        } else if (topLvlContainer instanceof JContextMenu) {
800                                                // this handles popup context menus
801                                                ((JContextMenu) topLvlContainer).add(menuItem);
802                                        }
803                                        // add to Map
804                                        keplerMenuMap.put(nextLevelBuff.toString(), menuItem);
805                                        separatorJustAdded = false;
806                                } else {
807                                        JMenu parent = (JMenu) keplerMenuMap.get(prevLevelStr);
808                                        if (parent != null) {
809                                                // add to parent
810                                                parent.add(menuItem);
811                                                // add to Map
812                                                keplerMenuMap.put(nextLevelBuff.toString(), menuItem);
813                                                separatorJustAdded = false;
814                                        } else {
815                                                if (isDebugging) {
816                                                        log.debug("Parent menu is NULL" + prevLevelStr);
817                                                }
818                                        }
819                                }
820                        }
821                }
822                return leafMenuItem;
823        }
824        
825        public void clear() {
826        /*int removed =*/ MemoryCleaner.removeActionListeners(_ptiiMenubar);
827        //System.out.println("MenuMapper menubar action listeners removed: " + removed);
828        _ptiiMenuActionsMap.clear();
829        _tableauFrameInstance = null;
830        _mappers.remove(this);
831        }
832        
833        /** Remap all open menubars. */
834        public static void reloadAllMenubars() {
835                for(MenuMapper mapper : _mappers) {
836                        mapper.reloadPtolemyMenus();
837                }               
838        }
839        
840        /** Remap the menu. */
841        public void reloadPtolemyMenus() {
842                _reloadPtolemyMenus = true;
843                init();
844                _tableauFrameInstance.validate();
845        }
846
847        // /////////////////////////////////////////////////////////////////
848        // // private variables ////
849
850        private static final Log log = LogFactory
851                        .getLog(MenuMapper.class.getName());
852
853        private static final boolean isDebugging = log.isDebugEnabled();
854
855        private JMenuBar _ptiiMenubar;
856
857        private TableauFrame _tableauFrameInstance;
858
859        private Map<String, Action> _ptiiMenuActionsMap;
860
861        private static boolean separatorJustAdded = false;
862
863        // NOTE - MUST NOT contain the char defined in MNEMONIC_SYMBOL,
864        // or the String defined as MENU_PATH_DELIMITER
865        public static final String MENU_PATH_DELIMITER = "->";
866
867        // NOTE - MUST NOT contain the String defined in MNEMONIC_SYMBOL,
868        // or the String defined as MENU_PATH_DELIMITER
869        public static final String MENU_SEPARATOR_KEY = "MENU_SEPARATOR";
870
871        // NOTE - MUST NOT match the String defined as MENU_PATH_DELIMITER,
872        // or the String defined as MENU_SEPARATOR_KEY
873        public static final String MNEMONIC_SYMBOL = "~";
874        
875        /** A collection of all MenuMapper objects. */
876        private static Set<MenuMapper> _mappers = Collections.synchronizedSet(new HashSet<MenuMapper>());
877        
878        /** If true, reload the Ptolemy menu. */
879        private boolean _reloadPtolemyMenus = false;
880}