// Copyright 2000, SLAC, Stanford, California, U.S.A. package org.freehep.xml.menus; import java.awt.Component; import java.util.Enumeration; import java.util.Hashtable; import javax.swing.JToolBar.Separator; import javax.xml.parsers.SAXParserFactory; import org.freehep.util.images.ImageHandler; import org.freehep.xml.util.BadXMLException; import org.freehep.xml.util.ClassPathEntityResolver; import org.freehep.xml.util.SAXErrorHandler; import org.freehep.xml.util.SAXTraverser; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import java.net.MalformedURLException; import java.net.URL; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.awt.Event; import java.awt.Insets; import javax.swing.AbstractButton; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JSeparator; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.UIManager; /** * Create a set of menus by reading an XML file. * * @author Tony Johnson (tonyj@slac.stanford.edu) * @author Peter Armstrong * @version $Id: XMLMenuBuilder.java 8584 2006-08-10 23:06:37Z duns $ */ public class XMLMenuBuilder { public static final String LOCATION_PROPERTY = "Location"; private static ClassLoader defaultLoader; /** * Create an (empty) XMLMenuBuilder */ public XMLMenuBuilder() { // Make sure to change Look and feel of unparented menus UIManager.addPropertyChangeListener(new UIListener(this)); } /** * Build an XMLMenuSystem using the default SAX parser. * @param xml The URL from which to read the XML */ public void build(URL xml) throws SAXException, IOException { XMLReader reader; try { reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); } catch (SecurityException x) // JAXP doesn't work if downloaded into webstart { try { String parser = System.getProperty("org.xml.sax.driver"); if (parser == null) System.getProperties().setProperty("org.xml.sax.driver","org.apache.xerces.parsers.SAXParser"); } catch (SecurityException xx) {} reader = XMLReaderFactory.createXMLReader(); } catch (Exception x) { throw new RuntimeException("Error creating XML Parser",x); } build(xml,reader); } /** * Build an XMLMenuSystem * @param xml The URL from which to read the XML * @param reader The XMLReader to use to parse the XML */ public void build(URL xml, XMLReader reader) throws SAXException, IOException { reader.setFeature("http://xml.org/sax/features/validation", true); reader.setEntityResolver(new ClassPathEntityResolver(XMLMenuBuilder.class,"http://java.freehep.org/schemas/menus/")); MenuSystemTraverser t = new MenuSystemTraverser(); t.setReader(reader); reader.setErrorHandler(new SAXErrorHandler()); baseURL = xml; InputStream in = new BufferedInputStream(xml.openStream()); try { InputSource input = new InputSource(in); input.setSystemId(xml.toString()); input.setPublicId(xml.getFile()); reader.parse(input); } finally { in.close(); } } /* * sets the default class for menubars * @param klass the new default class, must be a subclass of JMenuBar */ public void setDefaultMenuBarClass(Class klass) throws IllegalDefaultClassException { defaultMenuBarClass = setDefaultClass(JMenuBar.class, klass); } /** * sets the default class of menus to be klass * @param klass the new default class, must be a subclass of JMenu */ public void setDefaultMenuClass(Class klass) throws IllegalDefaultClassException { defaultMenuClass = setDefaultClass(JMenu.class, klass); } /** * sets the default class of menu items to be klass * @param klass the new default class, must be a subclass of JMenuItem */ public void setDefaultMenuItemClass(Class klass) throws IllegalDefaultClassException { defaultMenuItemClass = setDefaultClass(JMenuItem.class, klass); } /** * sets the default class of radio menu items to be klass * @param klass the new default class, must be a subclass of JMenuItem */ public void setDefaultRadioMenuItemClass(Class klass) throws IllegalDefaultClassException { defaultRadioButtonMenuItemClass = setDefaultClass(JMenuItem.class, klass); } /** * sets the default class of checkbox menu items to be klass * @param klass the new default class, must be a subclass of JMenuItem */ public void setDefaultCheckBoxMenuItemClass(Class klass) throws IllegalDefaultClassException { defaultCheckBoxMenuItemClass = setDefaultClass(JMenuItem.class, klass); } /** * sets the default class of popup menus to be klass * @param klass the new default class, must be a subclass of JPopupMenu */ public void setDefaultPopupMenuClass(Class klass) throws IllegalDefaultClassException { defaultPopupMenuClass = setDefaultClass(JPopupMenu.class, klass); } /** * sets the default class of toolbars to be klass * @param klass the new default class, must be a subclass of JToolBar */ public void setDefaultToolBarClass(Class klass) throws IllegalDefaultClassException { defaultToolBarClass = setDefaultClass(JToolBar.class, klass); } /** * sets the default class of menu items to be klass * @param klass the new default class, must be a subclass of JButton */ public void setDefaultToolBarItemClass(Class klass) throws IllegalDefaultClassException { defaultToolBarButtonClass = setDefaultClass(JButton.class, klass); } /** * Override this method to create your own subclass of JMenuBar */ protected JMenuBar createMenuBar(String className) throws SAXException { Class klass; if (className == null) klass = defaultMenuBarClass; else klass = findClass(className); return (JMenuBar) createObject(klass, JMenuBar.class); } /** * Override this method to create your own subclass of JMenu */ protected JMenu createMenu(String className) throws SAXException { Class klass; if (className == null) klass = defaultMenuClass; else klass = findClass(className); return (JMenu) createObject(klass, JMenu.class); } /** * Override this method to create your own subclass of JMenuItem */ protected JMenuItem createMenuItem(String className, String name, String type, String command) throws SAXException { Class klass; if (className == null) { if (type.equals("checkbox")) klass = defaultCheckBoxMenuItemClass; else if (type.equals("radio")) klass = defaultRadioButtonMenuItemClass; else klass = defaultMenuItemClass; } else klass = findClass(className); JMenuItem result = (JMenuItem) createObject(klass, JMenuItem.class); result.setText(name); if (command != null) result.setActionCommand(command); return result; } /** * Override this method to create your own subclass of JPopupMenu */ protected JPopupMenu createPopupMenu(String className) throws SAXException { Class klass; if (className == null) klass = defaultPopupMenuClass; else klass = findClass(className); return (JPopupMenu) createObject(klass, JPopupMenu.class); } /** * Override this method to create your own subclass of JToolBar */ protected JToolBar createToolBar(String className) throws SAXException { Class klass; if (className == null) klass = defaultToolBarClass; else klass = findClass(className); return (JToolBar) createObject(klass, JToolBar.class); } /** * Override this method to create your own subclass of JButton */ protected AbstractButton createToolBarItem(String className, String name, String type, String command) throws SAXException { Class klass; if (className == null) klass = defaultToolBarButtonClass; else klass = findClass(className); if (type.equals("checkbox")) klass = JToggleButton.class; else klass = JButton.class; AbstractButton result = (AbstractButton) createObject(klass, AbstractButton.class); result.setMargin(new Insets(0,0,0,0)); if (command != null) result.setActionCommand(command); return result; } /** * Override this method for customized icon creation. * The default implementation treats the argument as a relative or absolute URL. * If relative it is interpreted relative to the URL of the menu file being read. * If the icon cannot be found a "broken" icon is substituted. */ protected Icon createIcon(String name) throws SAXException { try { URL url = null; if (name.startsWith("/")) url = getClass().getResource(name); if (url == null) url = new URL(baseURL,name); return ImageHandler.getIcon(url); } catch (MalformedURLException x) { throw new BadXMLException("Could not load image: "+name,x); } } private void merge(JComponent oldMenu, JComponent newMenu) { // Loop over each element of the new Menu, // and if it is not already in the old menu insert it as appropriate if (newMenu instanceof JMenu) newMenu = ((JMenu) newMenu).getPopupMenu(); if (oldMenu instanceof JMenu) oldMenu = ((JMenu) oldMenu).getPopupMenu(); outer: for (int i=0; i<newMenu.getComponentCount(); i++) { Component c = newMenu.getComponent(i); if (c instanceof JComponent) { JComponent comp = (JComponent) c; Integer location = (Integer) comp.getClientProperty(LOCATION_PROPERTY); if (location != null) { int oldCount = oldMenu.getComponentCount(); for (int j=0; j<oldCount; j++) { Component old = oldMenu.getComponent(j); if (old instanceof JComponent) { JComponent oldComp = (JComponent) old; Integer oldLocation = (Integer) oldComp.getClientProperty(LOCATION_PROPERTY); if (oldLocation != null) { if (oldLocation.equals(location)) { merge(oldComp,comp); continue outer; } else if (oldLocation.compareTo(location) > 0) { oldMenu.add(comp,j); i--; continue outer; } } } } } } oldMenu.add(c); i--; } } /** * Get the menu bar with ID id */ public JMenuBar getMenuBar(String id) { return (JMenuBar) menuBarHash.get(id); } /** * Get the tool bar with ID id */ public JToolBar getToolBar(String id) { return (JToolBar) toolBarHash.get(id); } /** * Get the popup menu with ID id */ public JPopupMenu getPopupMenu(String id) { return (JPopupMenu) popupMenuHash.get(id); } /** * Merge the popup menu with ID id onto the end of an existing menu * @param id The id of the popup menu * @param menu The menu to be merged */ public JPopupMenu mergePopupMenu(String id, JPopupMenu menu) { try { JPopupMenu merge = getPopupMenu(id); if (menu == null) return merge; Component[] c = merge.getComponents(); for (int i=0; i<c.length; i++) { if (c[i] instanceof JSeparator) menu.addSeparator(); else menu.add(deepCopy(c[i])); } return menu; } catch (SAXException x) { throw new RuntimeException("Error parsing XML",x); } } private Component deepCopy(Component c) throws SAXException { if (c instanceof JMenu) { JMenu old = (JMenu) c; JMenu menu = createMenu(old.getClass().getName()); Component[] cc = old.getPopupMenu().getComponents(); for (int i=0; i<cc.length; i++) { if (cc[i] instanceof JSeparator) menu.addSeparator(); else menu.add(deepCopy(cc[i])); } menu.setText(old.getText()); menu.setMnemonic(old.getMnemonic()); return menu; } else if (c instanceof JMenuItem) { JMenuItem old = (JMenuItem) c; JMenuItem item = createMenuItem(old.getClass().getName(),old.getText(),null,old.getActionCommand()); item.setToolTipText(old.getToolTipText()); item.setIcon(old.getIcon()); item.setMnemonic(old.getMnemonic()); item.setAccelerator(old.getAccelerator()); return item; } else return c; } // // private methods, variables and classes // private Object createObject(Class klass, Class superKlass) throws SAXException { try { if (!superKlass.isAssignableFrom(klass)) throw new SAXException("Illegal class "+klass); return klass.newInstance(); } catch (SAXException x) { throw x; } catch (Exception x) { throw new BadXMLException("Could not create "+klass,x); } } /** * Set the classloader that will be used to load classes named in the XML */ public static void setClassLoader(ClassLoader loader) { defaultLoader = loader; } private Class findClass(String className) throws SAXException { try { ClassLoader loader = defaultLoader; if (loader == null) loader = Thread.currentThread().getContextClassLoader(); if (loader == null) loader = getClass().getClassLoader(); return Class.forName(className,true,loader); } catch (ClassNotFoundException x) { throw new BadXMLException("Can not find class "+className,x); } } private Class setDefaultClass(Class baseClass, Class klass) throws IllegalDefaultClassException { if (baseClass.isAssignableFrom(klass)) return klass; throw new IllegalDefaultClassException("Cannot assign into " + baseClass + " from " + klass); } private Class defaultMenuBarClass = JMenuBar.class; private Class defaultToolBarClass = JToolBar.class; private Class defaultMenuItemClass = JMenuItem.class; private Class defaultRadioButtonMenuItemClass = JRadioButtonMenuItem.class; private Class defaultCheckBoxMenuItemClass = JCheckBoxMenuItem.class; private Class defaultPopupMenuClass = JPopupMenu.class; private Class defaultToolBarButtonClass = JButton.class; private Class defaultMenuClass = JMenu.class; private URL baseURL; private Hashtable menuBarHash = new Hashtable(); private Hashtable toolBarHash = new Hashtable(); private Hashtable popupMenuHash = new Hashtable(); private class MenuSystemTraverser extends SAXTraverser { protected SAXTraverser handleElement(String name, Attributes attrs) throws SAXException { if (name == "MenuSystem") return this; else if (name == "MenuBar") return new MenuBarTraverser(); else if (name == "ToolBar") return new ToolBarTraverser(); else if (name == "PopupMenu") return new PopupMenuTraverser(); else if (name == "Menu") return new MenuTraverser(); else if (name == "Component") return new MenuItemTraverser(); else if (name == "Accelerator") return new AcceleratorTraverser(); else if (name == "Separator") return new SeparatorTraverser(); else return super.handleElement(name,attrs); } } private class MenuBarTraverser extends MenuSystemTraverser { protected void handleSubElement(SAXTraverser sub) throws SAXException { if (sub instanceof MenuTraverser) { menuBar.add(((MenuTraverser) sub).getMenu()); } } protected void handleElementAttributes(Attributes attrs) throws SAXException { String klass = attrs.getValue("class"); menuBar = createMenuBar(klass); id = attrs.getValue("id"); } protected void handleEndElement(String name) { JMenuBar oldMenu = (JMenuBar) menuBarHash.get(id); if (oldMenu == null) menuBarHash.put(id,menuBar); else merge(oldMenu,menuBar); } private JMenuBar menuBar; private String id; } private class PopupMenuTraverser extends MenuSystemTraverser { protected void handleSubElement(SAXTraverser sub) throws SAXException { if (sub instanceof MenuItemTraverser) { menu.add(((MenuItemTraverser) sub).getMenuItem()); } else if (sub instanceof MenuTraverser) { menu.add(((MenuTraverser) sub).getMenu()); } else if (sub instanceof SeparatorTraverser) { JSeparator sep = new javax.swing.JPopupMenu.Separator(); String location = ((SeparatorTraverser) sub).getLocation(); if (location != null) sep.putClientProperty(LOCATION_PROPERTY,Integer.valueOf(location)); menu.add(sep); } } protected void handleElementAttributes(Attributes attrs) throws SAXException { String klass = attrs.getValue("class"); menu = createPopupMenu(klass); menu.setName(attrs.getValue("name")); id = attrs.getValue("id"); } protected void handleEndElement(String name) { popupMenuHash.put(id,menu); } private JPopupMenu menu; private String id; } private class MenuTraverser extends MenuSystemTraverser { protected void handleSubElement(SAXTraverser sub) { if (sub instanceof MenuItemTraverser) { menu.add(((MenuItemTraverser) sub).getMenuItem()); } else if (sub instanceof MenuTraverser) { menu.add(((MenuTraverser) sub).getMenu()); } else if (sub instanceof SeparatorTraverser) { JSeparator sep = new javax.swing.JPopupMenu.Separator(); String location = ((SeparatorTraverser) sub).getLocation(); if (location != null) sep.putClientProperty(LOCATION_PROPERTY,Integer.valueOf(location)); menu.add(sep); } } protected void handleElementAttributes(Attributes attrs) throws SAXException { String klass = attrs.getValue("class"); menu = createMenu(klass); menu.setText(attrs.getValue("name")); String mnemonic = attrs.getValue("mnemonic"); if (mnemonic != null) menu.setMnemonic(mnemonic.charAt(0)); String location = attrs.getValue("location"); if (location != null) menu.putClientProperty(LOCATION_PROPERTY,Integer.valueOf(location)); } JMenu getMenu() { return menu; } private JMenu menu; } private class ToolBarTraverser extends MenuSystemTraverser { protected SAXTraverser handleElement(String name, Attributes attrs) throws SAXException { if (name == "Component") { return new ToolBarItemTraverser(); } else return super.handleElement(name,attrs); } protected void handleSubElement(SAXTraverser sub) { if (sub instanceof ToolBarItemTraverser) { toolBar.add(((ToolBarItemTraverser) sub).getToolBarItem()); } else if (sub instanceof SeparatorTraverser) { JSeparator sep = new Separator(); sep.setOrientation(JSeparator.VERTICAL); // FREEHEP-382 String location = ((SeparatorTraverser) sub).getLocation(); if (location != null) sep.putClientProperty(LOCATION_PROPERTY,Integer.valueOf(location)); toolBar.add(sep); } } protected void handleElementAttributes(Attributes attrs) throws SAXException { id = attrs.getValue("id"); String klass = attrs.getValue("class"); toolBar = createToolBar(klass); toolBar.setName(attrs.getValue("name")); toolBar.setRollover(toBoolean(attrs.getValue("rollover"))); } protected void handleEndElement(String name) { toolBarHash.put(id,toolBar); } private String id; private JToolBar toolBar; } private abstract class ComponentTraverser extends MenuSystemTraverser { protected void handleSubElement(SAXTraverser sub) { if (sub instanceof AcceleratorTraverser) setAccelerator(((AcceleratorTraverser) sub).getAccelerator()); } abstract void setAccelerator(KeyStroke key); } private class AcceleratorTraverser extends SAXTraverser { protected void handleElementAttributes(Attributes attrs) throws SAXException { String theKey = attrs.getValue("key"); // command is equivalent to meta on Mac but Ctrl everywhere else String lcOSName = System.getProperty("os.name").toLowerCase(); boolean isMac = lcOSName.startsWith("mac os x"); String command = attrs.getValue("command"); // Not present in version 1.0 of DTD boolean is10 = command == null; boolean theCmdMod = !is10 && toBoolean(command); boolean theCtrlMod = (!isMac && theCmdMod) || toBoolean(attrs.getValue("ctrl")); boolean theAltMod = toBoolean(attrs.getValue("alt")); boolean theShiftMod = toBoolean(attrs.getValue("shift")); boolean theMetaMod = (isMac && theCmdMod) || toBoolean(attrs.getValue("meta")); if (is10 && isMac) { boolean temp = theCtrlMod; theCtrlMod = theMetaMod; theMetaMod = temp; } try { key = KeyStroke.getKeyStroke( KeyEvent.class.getField(theKey).getInt(null), ( (theCtrlMod ? Event.CTRL_MASK : 0) + (theAltMod ? Event.ALT_MASK : 0) + (theShiftMod ? Event.SHIFT_MASK : 0) + (theMetaMod ? Event.META_MASK : 0))); } catch (Exception eh) { throw new BadXMLException("Illegal accelerator key: "+theKey); } } KeyStroke getAccelerator() { return key; } protected KeyStroke key; } private class MenuItemTraverser extends ComponentTraverser { protected void handleElementAttributes(Attributes attrs) throws SAXException { String klass = attrs.getValue("class"); String name = attrs.getValue("name"); String type = attrs.getValue("type"); String command = attrs.getValue("command"); item = createMenuItem(klass,name,type,command); String mnemonic = attrs.getValue("mnemonic"); if (mnemonic != null) item.setMnemonic(mnemonic.charAt(0)); String location = attrs.getValue("location"); if (location != null) item.putClientProperty(LOCATION_PROPERTY,Integer.valueOf(location)); String desc = attrs.getValue("description"); if (desc == null) desc = name; item.setToolTipText(desc); String icon = attrs.getValue("icon"); if (icon != null) item.setIcon(createIcon(icon)); } protected void setAccelerator(KeyStroke key) { item.setAccelerator(key); } JMenuItem getMenuItem() { return item; } private JMenuItem item; } private class SeparatorTraverser extends MenuSystemTraverser { protected void handleElementAttributes(Attributes attrs) throws SAXException { location = attrs.getValue("location");; } String getLocation() { return location; } private String location; } private static class UIListener implements PropertyChangeListener { WeakReference link; UIListener(XMLMenuBuilder builder) { // make sure we dont prevent the XMLMenuBuilder from being GCd // by creating a ref from the static UTManager link = new WeakReference(builder); } public void propertyChange(PropertyChangeEvent evt) { XMLMenuBuilder builder = (XMLMenuBuilder) link.get(); if (builder == null) UIManager.removePropertyChangeListener(this); else if (evt.getPropertyName().equals("lookAndFeel")) { // change the UI of any cached menus Enumeration e = builder.popupMenuHash.elements(); while (e.hasMoreElements()) { JComponent c = (JComponent) e.nextElement(); SwingUtilities.updateComponentTreeUI(c); } } } } private class ToolBarItemTraverser extends ComponentTraverser { protected void handleElementAttributes(Attributes attrs) throws SAXException { String klass = attrs.getValue("class"); String name = attrs.getValue("name"); String type = attrs.getValue("type"); String command = attrs.getValue("command"); item = createToolBarItem(klass,name,type,command); String mnemonic = attrs.getValue("mnemonic"); if (mnemonic != null) item.setMnemonic(mnemonic.charAt(0)); String location = attrs.getValue("location"); if (location != null) item.putClientProperty(LOCATION_PROPERTY,location); String desc = attrs.getValue("description"); if (desc == null) desc = name; item.setToolTipText(desc); String icon = attrs.getValue("icon"); if (icon != null) item.setIcon(createIcon(icon)); } protected void setAccelerator(KeyStroke key) { //item.setAccelerator(key); } AbstractButton getToolBarItem() { return item; } private AbstractButton item; } public static class IllegalDefaultClassException extends RuntimeException { IllegalDefaultClassException(String s) { super(s); } } }