/* Copyright (C) 2006 Christian Schneider
*
* This file is part of Nomad.
*
* Nomad is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Nomad is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Nomad; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
* Created on Nov 19, 2006
*/
package net.sf.nmedit.nomad.core.menulayout;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import net.sf.nmedit.nmutils.Platform;
/**
* The menu builder class. Uses a {@link net.sf.nmedit.nomad.core.menulayout.MenuLayout} and
* a {@link java.util.ResourceBundle} to setup the {@link javax.swing.Action}s. When the ResourceBundle
* is changed at runtime each action (menu item) is updated automatically.
*
* <h1>Linking MenuLayout and ResourceBundle</h1>
*
* <p>Each MenuLayout entry has a key (the global entry-point).
* The key is used up to lookup the properties of the entry in the ResourceBundle.</p>
*
* <h1>ResourceBundle format</h1>
*
* <ul>
* <li><strong>name:</strong> looked up via the global entry-point key</li>
* <li><strong>mnemonic:</strong>
* The mnemonic character is encoded in the name-property. The mnemonic character
* is the character after the first ampersand (&) character. The &-character
* will not show up in the name property. <br />
* Example Values:<br/>
*
* <ul>
* <li>menu.file.save=&Save <br />
* name = Save, mnemonic = S
* </li>
* <li>menu.file=Save &As .. <br />
* name = Save As..., mnemonic = A
* </li>
* </ul>
*
* </li>
* <li><strong>keybinding (accelerator):</strong>
* the keybinding associated with an entry has the key [global entry-point]+"$keybinding".
* For example:<code>menu.file$keybinding</code>. <br />
*
* The keybinding syntax is
* <code> ((ALT|SHIFT|CTRL|META)\\+)*(\\w|\\d|F\\d\\d?)</code>.
*
* Valid values are for example:<br />
* <code>CTRL+C</code> <br />
* <code>SHIFT+CTRL+C</code> <br />
* <code>SHIFT+F1</code> <br />
* <code>ALT+SHIFT+F12</code>
* </li>
* <li><strong>short (long) description:</strong>
* the short (long) description associated with an entry has the key [global entry-point]+"$description"
* ([global entry-point]+"$long-description").
* For example: <code>menu.file$short-description (menu.file$long-description)</code>.</li>
*
* </ul>
*
* @author Christian Schneider
*/
public class MenuBuilder
{
private MenuLayout layout;
private ResourceBundle bundle;
public MenuBuilder(MenuLayout layout, ResourceBundle bundle)
{
this.layout = layout;
this.bundle = null;
setResourceBundle(bundle);
}
public MenuBuilder getClonedTree(String entryPoint)
{
MLEntry e = layout.getEntry(entryPoint);
if (e == null)
{
MLEntry empty = new MLEntry(entryPoint);
return new MenuBuilder(new MenuLayout(empty), bundle);
}
return new MenuBuilder(new MenuLayout(e.cloneTree()), bundle);
}
public void setResourceBundle(ResourceBundle bundle)
{
if (this.bundle != bundle)
{
this.bundle = bundle;
setActionProperties();
}
}
public MenuBuilder(MenuLayout layout)
{
this(layout, null);
}
public ResourceBundle getResourceBundle()
{
return bundle;
}
public MenuLayout getLayout()
{
return layout;
}
public MLEntry getEntry(String entryPoint)
{
return layout.getEntry(entryPoint);
}
public JPopupMenu createPopup(String entryPoint)
{
JPopupMenu mnPopup = new JPopupMenu();
createMenuContents(mnPopup, entryPoint);
return mnPopup;
}
public JMenuBar createMenuBar(String entryPoint)
{
JMenuBar mnBar = new JMenuBar();
createMenuContents(mnBar, entryPoint);
return mnBar;
}
private void createMenuContents(JComponent m, String entryPoint)
{
MLEntry e = getEntry(entryPoint);
if (e!=null)
{
createMenuContents(m , e);
}
}
private void createMenuContents(JComponent m, MLEntry parent)
{
int separatorIndex = 0;
for (int i=0;i<parent.size();i++)
{
MLEntry childEntry = parent.getEntryAt(i);
if (childEntry.size()>0)
{
if (childEntry.isFlat())
{
if (i>separatorIndex)
addSeparator(m);
createMenuContents(m, childEntry);
if (i<parent.size()-1)
{
separatorIndex = i+1;
addSeparator(m);
}
}
else
{
JMenu subMenu = new JMenu(childEntry);
createMenuContents(subMenu, childEntry);
m.add(subMenu);
}
}
else
{
JMenuItem item = new JMenuItem(childEntry);
m.add(item);
}
}
}
private void addSeparator(JComponent c)
{
if (c instanceof JMenu)
{
((JMenu) c).addSeparator();
}
else if (c instanceof JPopupMenu )
{
((JPopupMenu) c).addSeparator();
}
}
public boolean addActionListener( String entryPoint, ActionListener listener )
{
MLEntry e = getEntry(entryPoint);
if (e!=null)
{
e.addActionListener(listener);
return true;
}
return false;
}
public void removeActionListener( String entryPoint, ActionListener listener )
{
MLEntry e = getEntry(entryPoint);
if (e!=null)
{
e.removeActionListener(listener);
}
}
// utils
public final static String SUFFIX_KEYBINDING = "$keybinding";
public final static String SUFFIX_KEYBINDING_OSX = "$keybinding.osx";
public final static String SUFFIX_SHORT_DESC = "$description";
public final static String SUFFIX_LONG_DESC = "$long-description";
private static Map<String, Integer> modifierMap = new HashMap<String, Integer>();
static
{
modifierMap.put("SHIFT", KeyEvent.SHIFT_MASK);
modifierMap.put("ALT", KeyEvent.ALT_MASK);
modifierMap.put("ALTGR", KeyEvent.ALT_GRAPH_MASK);
modifierMap.put("META", KeyEvent.META_MASK);
modifierMap.put("CTRL", KeyEvent.CTRL_MASK);
}
private static Pattern kbPattern =
Pattern.compile("(((ALT)|(ALTGR)|(SHIFT)|(CTRL)|(META))\\+)*(\\w|\\d|F\\d\\d?)", Pattern.CASE_INSENSITIVE);
private String getStringOrNull(String key)
{
try
{
return bundle.getString(key);
}
catch (MissingResourceException e)
{
return null;
}
}
private void setActionProperties()
{
if (layout.getRoot() != null)
{
Iterator<MLEntry> i = layout.getRoot().bfsIterator();
while (i.hasNext())
{
setupAction(i.next());
}
}
}
private void setupAction(MLEntry entry)
{
String eName = null;
KeyStroke eAcceleratorKey = null;
Integer eMnemonicKey = 0;
String eShortDesc = null;
String eLongDesc = null;
final String bundleID = entry.getGlobalEntryPoint();
if (bundleID != null)
{
// get the name
eName = getStringOrNull(bundleID);
if (eName != null)
{
// get the mnemonic key,
// get index of the ampersand (&) identifying the mnemonic key
int amp = eName.indexOf('&');
// check if index is valid
if (amp>=0 && amp<eName.length()-1)
{
eMnemonicKey = new Integer(eName.codePointAt(amp+1));
// remove ampersand
eName = eName.substring(0, amp) + eName.substring(amp+1, eName.length());
}
}
// get descriptions
eShortDesc = getStringOrNull(bundleID+SUFFIX_SHORT_DESC);
eLongDesc = getStringOrNull(bundleID+SUFFIX_LONG_DESC);
String keybinding = null;
if (Platform.isFlavor(Platform.OS.MacOSFlavor)) {
keybinding = getStringOrNull(bundleID+SUFFIX_KEYBINDING_OSX);
}
if (keybinding == null) {
keybinding = getStringOrNull(bundleID+SUFFIX_KEYBINDING);
}
if (keybinding != null)
eAcceleratorKey = extractKeyStroke(keybinding);
}
if (eName != null)
entry.putValue(Action.NAME, eName);
if (eMnemonicKey != null)
entry.putValue(Action.MNEMONIC_KEY, eMnemonicKey);
if (eAcceleratorKey != null)
entry.putValue(Action.ACCELERATOR_KEY, eAcceleratorKey);
if (eShortDesc != null)
entry.putValue(Action.SHORT_DESCRIPTION, eShortDesc);
if (eLongDesc != null)
entry.putValue(Action.LONG_DESCRIPTION, eLongDesc);
}
public static KeyStroke extractKeyStroke(String keybinding)
{
Matcher m = kbPattern.matcher(keybinding);
if (m.matches())
{
int modifiers = 0;
for (int i=1;i<m.groupCount();i++)
{
String s = m.group(i);
if (s!=null)
{
Integer mod = modifierMap.get(s.toUpperCase());
if (mod!=null)
modifiers|=mod.intValue();
}
}
String key = m.group(m.groupCount());
if (key.length()>0)
{
int cp = key.codePointAt(0);
if (Character.charCount(cp) == key.length())
{
return KeyStroke.getKeyStroke(cp, modifiers);
}
if (key.length()>=2 && key.toUpperCase().startsWith("F"))
{
try
{
int FX = Integer.parseInt(key.substring(1));
if (1<=FX && FX<=12)
{
cp = KeyEvent.VK_F1+FX-1;
return KeyStroke.getKeyStroke(cp, modifiers);
}
}
catch (NumberFormatException e)
{
}
}
}
}
return null;
}
}