001package jmri.util.swing;
002
003import java.awt.Container;
004import java.beans.PropertyChangeListener;
005import java.lang.reflect.InvocationTargetException;
006import java.util.ArrayList;
007
008import javax.annotation.CheckForNull;
009import javax.annotation.Nonnull;
010import javax.swing.Action;
011import javax.swing.ButtonGroup;
012import javax.swing.JMenu;
013import javax.swing.JMenuItem;
014import javax.swing.JRadioButtonMenuItem;
015import javax.swing.UIManager;
016
017import jmri.util.SystemType;
018import jmri.util.jdom.LocaleSelector;
019
020import org.jdom2.Element;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Common utility methods for working with JMenus.
026 * <p>
027 * Chief among these is the loadMenu method, for creating a set of menus from an
028 * XML definition
029 *
030 * @author Bob Jacobsen Copyright 2003, 2010
031 * @since 2.9.4
032 */
033public class JMenuUtil extends GuiUtilBase {
034
035    static public JMenu[] loadMenu(String path, WindowInterface wi, Object context) {
036        Element root = rootFromName(path);
037
038        int n = root.getChildren("node").size();
039        JMenu[] retval = new JMenu[n];
040
041        int i = 0;
042        ArrayList<Integer> mnemonicList = new ArrayList<Integer>();
043        for (Object child : root.getChildren("node")) {
044            JMenu menuItem = jMenuFromElement((Element) child, wi, context);
045            retval[i++] = menuItem;
046            if (((Element) child).getChild("mnemonic") != null) {
047                int mnemonic = convertStringToKeyEvent(((Element) child).getChild("mnemonic").getText());
048                if (mnemonicList.contains(mnemonic)) {
049                    log.error("Menu item '{}' Mnemonic '{}' has already been assigned", menuItem.getText(), ((Element) child).getChild("mnemonic").getText());
050                } else {
051                    menuItem.setMnemonic(mnemonic);
052                    mnemonicList.add(mnemonic);
053                }
054            }
055        }
056        return retval;
057    }
058
059    static @Nonnull
060    JMenu jMenuFromElement(@CheckForNull Element main, WindowInterface wi, Object context) {
061        boolean addSep = false;
062        String name = "<none>";
063        if (main == null) {
064            log.warn("Menu from element called without an element");
065            return new JMenu(name);
066        }
067        name = LocaleSelector.getAttribute(main, "name");
068        //Next statement left in if the xml file hasn't been converted
069        if ((name == null) || (name.equals(""))) {
070            if (main.getChild("name") != null) {
071                name = main.getChild("name").getText();
072            }
073        }
074        JMenu menu = new JMenu(name);
075        ArrayList<Integer> mnemonicList = new ArrayList<>();
076        for (Object item : main.getChildren("node")) {
077            JMenuItem menuItem = null;
078            Element child = (Element) item;
079            if (child.getChildren("node").isEmpty()) {  // leaf
080                if ((child.getText().trim()).equals("separator")) {
081                    addSep = true;
082                } else {
083
084                    boolean optional = true;
085                    var instanceManagerOptionalClassElement = child.getChild("instanceManagerOptionalClass");
086                    var optionalFunctionElement = child.getChild("optionalFunction");
087
088                    if (instanceManagerOptionalClassElement != null
089                            && optionalFunctionElement != null) {
090
091                        var instanceManagerOptional = instanceManagerOptionalClassElement.getText();
092                        var optionalFunction = optionalFunctionElement.getText();
093                        Object o = jmri.InstanceManager.getDefault(instanceManagerOptional);
094                        try {
095                            var method = o.getClass().getDeclaredMethod(optionalFunction);
096                            Object result = method.invoke(o);
097                            if (result instanceof Boolean) {
098                                optional = (Boolean) result;
099                            }
100                        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
101                            log.error("Error during loading of menu", e);
102                        }
103                    }
104
105                    if (!(SystemType.isMacOSX()
106                            && UIManager.getLookAndFeel().isNativeLookAndFeel()
107                            && ((child.getChild("adapter") != null
108                            && child.getChild("adapter").getText().equals("apps.gui3.tabbedpreferences.TabbedPreferencesAction"))
109                            || (child.getChild("current") != null
110                            && child.getChild("current").getText().equals("quit"))))
111                            && optional) {
112                        if (addSep) {
113                            menu.addSeparator();
114                            addSep = false;
115                        }
116                        Action act = actionFromNode(child, wi, context);
117                        menu.add(menuItem = new JMenuItem(act));
118                    }
119                }
120            } else {
121                if (addSep) {
122                    menu.addSeparator();
123                    addSep = false;
124                }
125                if (child.getChild("group") != null && child.getChild("group").getText().equals("yes")) {
126                    //A seperate method is required for creating radio button groups
127                    menu.add(createMenuGroupFromElement(child, wi, context));
128                } else {
129                    menu.add(menuItem = jMenuFromElement(child, wi, context)); // not leaf
130                }
131            }
132            if (menuItem != null && child.getChild("current") != null) {
133                setMenuItemInterAction(context, child.getChild("current").getText(), menuItem);
134            }
135            if (menuItem != null && child.getChild("mnemonic") != null) {
136                int mnemonic = convertStringToKeyEvent(child.getChild("mnemonic").getText());
137                if (mnemonicList.contains(mnemonic)) {
138                    log.error("Menu Item '{}' Mnemonic '{}' has already been assigned", menuItem.getText(), child.getChild("mnemonic").getText());
139                } else {
140                    menuItem.setMnemonic(mnemonic);
141                    mnemonicList.add(mnemonic);
142                }
143            }
144        }
145        return menu;
146    }
147
148    static @Nonnull
149    JMenu createMenuGroupFromElement(@CheckForNull Element main, WindowInterface wi, Object context) {
150        String name = "<none>";
151        if (main == null) {
152            log.warn("Menu from element called without an element");
153            return new JMenu(name);
154        }
155        name = LocaleSelector.getAttribute(main, "name");
156        //Next statement left in if the xml file hasn't been converted
157        if ((name == null) || (name.equals(""))) {
158            if (main.getChild("name") != null) {
159                name = main.getChild("name").getText();
160            }
161        }
162        JMenu menu = new JMenu(name);
163        ButtonGroup group = new ButtonGroup();
164        for (Object item : main.getChildren("node")) {
165            Element elem = (Element) item;
166            Action act = actionFromNode(elem, wi, context);
167            JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(act);
168            group.add(menuItem);
169            menu.add(menuItem);
170            if (elem.getChild("current") != null) {
171                setMenuItemInterAction(context, elem.getChild("current").getText(), menuItem);
172            }
173        }
174
175        return menu;
176    }
177
178    static void setMenuItemInterAction(@Nonnull Object context, final String ref, final JMenuItem menuItem) {
179        java.lang.reflect.Method methodListener = null;
180        try {
181            methodListener = context.getClass().getMethod("addPropertyChangeListener", java.beans.PropertyChangeListener.class);
182        } catch (java.lang.NullPointerException e) {
183            log.error("Null object passed");
184            return;
185        } catch (SecurityException e) {
186            log.error("security exception unable to find remoteCalls for {}", context.getClass().getName());
187            return;
188        } catch (NoSuchMethodException e) {
189            log.error("No such method remoteCalls for {}", context.getClass().getName());
190            return;
191        }
192
193        try {
194            methodListener.invoke(context, new PropertyChangeListener() {
195                @Override
196                public void propertyChange(java.beans.PropertyChangeEvent e) {
197                    if (e.getPropertyName().equals(ref)) {
198                        String method = (String) e.getOldValue();
199                        if (method.equals("setSelected")) {
200                            menuItem.setSelected(true);
201                        } else if (method.equals("setEnabled")) {
202                            menuItem.setEnabled((Boolean) e.getNewValue());
203                        }
204                    }
205                }
206            });
207        } catch (IllegalArgumentException ex) {
208            log.error("IllegalArgument in setMenuItemInterAction ", ex);
209        } catch (IllegalAccessException ex) {
210            log.error("IllegalAccess in setMenuItemInterAction ", ex);
211        } catch (java.lang.reflect.InvocationTargetException ex) {
212            log.error("InvocationTarget {} in setMenuItemInterAction ", ref, ex);
213        } catch (java.lang.NullPointerException ex) {
214            log.error("NPE {} in setMenuItemInterAction ", context.getClass().getName(), ex);
215        }
216
217    }
218
219    static int convertStringToKeyEvent(@Nonnull String st) {
220        char a = (st.toLowerCase()).charAt(0);
221        int kcode = a - 32;
222        return kcode;
223    }
224
225    /**
226     * replace a menu item in its parent with another menu item
227     * <p>
228     * (at the same position in the parent menu)
229     *
230     * @param orginalMenuItem     the original menu item to be replaced
231     * @param replacementMenuItem the menu item to replace it with
232     * @return true if the original menu item was found and replaced
233     */
234    public static boolean replaceMenuItem(
235            @Nonnull JMenuItem orginalMenuItem,
236            @Nonnull JMenuItem replacementMenuItem) {
237        boolean result = false; // assume failure (pessimist!)
238        Container container = orginalMenuItem.getParent();
239        if (container != null) {
240            int index = container.getComponentZOrder(orginalMenuItem);
241            if (index > -1) {
242                container.remove(orginalMenuItem);
243                container.add(replacementMenuItem, index);
244                result = true;
245            }
246        }
247        return result;
248    }
249
250    private final static Logger log = LoggerFactory.getLogger(JMenuUtil.class);
251}   // class JMenuUtil