com.googlecode.blaisemath.app.MenuConfig.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.blaisemath.app.MenuConfig.java

Source

/**
 * MenuConfig.java
 * Created Nov 5, 2014
 */
package com.googlecode.blaisemath.app;

/*
 * #%L
 * BlaiseSketch
 * --
 * Copyright (C) 2014 - 2016 Elisha Peterson
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButton;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;

/**
 * <p>
 *   Provides utility methods for loading menus and toolbars from configuration
 *   files, merging them with action maps for functionality.
 * </p>
 * <p>
 *   The menu configuration files include one or more nested menus/toolbars.
 *   Menu content is a list of content items, where a content item may be either:
 * </p>
 * <ul>
 *   <li>An empty value corresponding to a separator</li>
 *   <li>A string corresponding to an action</li>
 *   <li>A customized action, encoded as a key-value pair where the key is the
 *     action and the value is a map of key-value pairs configuring the action's
 *     appearance (e.g. regular menu item vs. checkbox menu item)</li>
 *   <li>A submenu, encoded as a key-value pair where the key is the submenu name
 *     and the value is a list of content items</li>
 *   <li>A customized submenu, encoded as a key-value pair where the key is
 *     an action and the value is a map including a {@code name} key for the
 *     sub-menus display name and an {@code options} key referencing a static
 *     method that returns a list of options to use for the menu.</li>
 * </ul>
 * <p>
 *   Toolbars may not have embedded sub-menus.
 * </p>
 * <p>
 *   Here is an example:
 * </p>
 * <pre>{@code# YAML menu config
 * ---
 * MENUBAR:
 *   File:
 *     - load
 *     - save
 *     -
 *     - quit
 *   Edit:
 *     - Edit Something: {
 *       action: editThis,
 *       options: org.yours.package.Utils#editThisOptions
 *     }
 *     - SubMenu 2:
 *        - subOption1
 *        - subOption2
 *     -
 *     - toggle: { type: CHECKBOX }
 * TOOLBAR:
 *   - load
 *   - save
 *   -
 *   - toggle: { type: TOGGLE }
 *   -
 *   - toolbar1
 *   - toolbar2
 * Other Menu:
 *   - action1
 *   - action2
 * }</pre>
 * <p>
 *   The following configuration options for actions are supported:
 * </p>
 * <ul>
 *   <li>{@code type: CHECKBOX} to display the action with a checkbox in the menu or toolbar</li>
 *   <li>{@code type: TOGGLE} to display the action with a toggle button in the toolbar</li>
 * </ul>
 * 
 * @author elisha
 * @version 2.0
 */
public class MenuConfig {

    private static final Logger LOG = Logger.getLogger(MenuConfig.class.getName());

    public static final String MENU_SUFFIX = "_menu";
    public static final String DEFAULT_TOOLBAR_KEY = "TOOLBAR";
    public static final String DEFAULT_MENUBAR_KEY = "MENUBAR";

    // utility class
    private MenuConfig() {
    }

    //<editor-fold defaultstate="collapsed" desc="READING FROM CONFIGURATION FILE">

    /**
     * Gets the menubar configuration from file.
     * @param cls class with associated resource
     * @param key key for menubar component in config file
     * @return menus to comprise menubar, where values are lists with either strings or nested menus
     * @throws IOException if there's an error reading from the config file
     */
    public static List<Map> readMenuBarConfig(Class cls, String key) throws IOException {
        Object res = readConfig(cls).get(key);
        checkState(res instanceof List, "Expected menubar to be a list of maps but was " + res);
        checkState(Iterables.all((List) res, Predicates.instanceOf(Map.class)),
                "Expected list elements of menubar to be maps");
        return (List<Map>) res;
    }

    /**
     * Gets the toolbar configuration from file.
     * @param cls class with associated resource
     * @param key key for toolbar component in config file
     * @return list of actions for the toolbar
     * @throws IOException if there's an error reading from the config file
     */
    public static List<String> readToolBarConfig(Class cls, String key) throws IOException {
        Object res = readConfig(cls).get(key);
        checkState(res instanceof List, "Expected toolbar to be a list of strings");
        return (List<String>) res;
    }

    /**
     * Gets the menu configuration from file.
     * @param cls class with associated resource
     * @param key key for menu component in config file
     * @return list of actions for the toolbar
     * @throws IOException if there's an error reading from the config file
     */
    public static List readMenuConfig(Class cls, String key) throws IOException {
        Object res = readConfig(cls).get(key);
        checkState(res instanceof List, "Expected menu to be a list");
        return (List) res;
    }

    /** 
     * Reads menu components from file.
     * @param cls class with associated resource
     * @return map, where values are either nested maps, or lists of strings representing actions
     * @throws IOException if there's an error reading from the config file
     */
    public static Map<String, Object> readConfig(Class cls) throws IOException {
        String path = "resources/" + cls.getSimpleName() + MENU_SUFFIX + ".yml";
        URL rsc = cls.getResource(path);
        checkNotNull(rsc, "Failed to locate " + path);
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        return mapper.readValue(rsc, Map.class);
    }

    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="CONVERTING CONFIGURATION CONTENT TO MENUS/ITEMS">

    /**
     * Build a menu from a given map entry with key the menu name, and value a list
     * or map that determines the menu items.
     * @param name menu name
     * @param items content of menu
     * @param am string/action mapping
     * @return menu
     */
    static JToolBar createToolBar(String name, List items, Map<String, Action> am) {
        JToolBar tb = new JToolBar(name);
        for (Object it : items) {
            boolean used = false;
            for (MenuConfigElement el : MenuConfigElement.values()) {
                if (el.appliesTo(it)) {
                    el.addToolbarItem(tb, it, am);
                    used = true;
                    break;
                }
            }
            if (!used) {
                LOG.log(Level.WARNING, "Invalid toolbar item:{0}", it);
            }
        }
        return tb;
    }

    /**
     * Build a menu from a given map entry with key the menu name, and value a list
     * or map that determines the menu items.
     * @param name menu name
     * @param menuContent content of menu
     * @param am string/action mapping
     * @return menu
     */
    static JMenu createMenu(String name, List menuContent, Map<String, Action> am) {
        JMenu menu = new JMenu(name);
        for (JMenuItem el : createMenuItemList(menuContent, am)) {
            if (el == null) {
                menu.addSeparator();
            } else {
                menu.add(el);
            }
        }
        return menu;
    }

    /**
     * Builds a menu based on a list of items provided as actions or submenus.
     * @param items configuration of menu items
     * @param actionMap string/action mapping to lookup actions
     * @return menu items, with null values for separators
     */
    static List<JMenuItem> createMenuItemList(List items, Map<String, javax.swing.Action> actionMap) {
        checkNotNull(items);
        checkNotNull(actionMap);
        List<JMenuItem> res = Lists.newArrayList();
        for (Object it : items) {
            boolean used = false;
            for (MenuConfigElement el : MenuConfigElement.values()) {
                if (el.appliesTo(it)) {
                    res.add(el.menuItem(it, actionMap));
                    used = true;
                    break;
                }
            }
            if (!used) {
                LOG.log(Level.WARNING, "Invalid menu item:{0}", it);
            }
        }
        return res;
    }

    private static JButton popupButton(String name, final JPopupMenu popup) {
        final JButton button = new JButton(name);
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                popup.show(e.getComponent(), e.getX(), e.getY());
            }
        });
        return button;
    }

    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="GETTING STUFF OUT OF MAPS">

    /** Tests whether map is a singleton */
    static boolean isSingletonMap(Map<String, ?> map) {
        return map != null && map.size() == 1;
    }

    /** Convert a map with a single entry to the entry */
    static <X> Entry<String, X> singletonMapEntry(Map<String, X> map) {
        checkArgument(map != null);
        checkArgument(map.size() == 1);
        return map.entrySet().iterator().next();
    }

    /** Tests whether key exists as a key in a singleton map's value map */
    static boolean existsKeyNestedInSingletonMap(String key, Map<String, ?> map) {
        if (!isSingletonMap(map)) {
            return false;
        }
        Entry<String, ?> en = singletonMapEntry(map);
        if (!(en.getValue() instanceof Map)) {
            return false;
        }
        return ((Map) en.getValue()).containsKey(key);
    }

    //</editor-fold>

    /** Decode options supported for the menu config */
    private static enum MenuConfigElement {
        SEPARATOR() {
            @Override
            boolean appliesTo(Object item) {
                return item == null || (item instanceof String && ((String) item).isEmpty());
            }

            @Override
            JMenuItem menuItem(Object item, Map<String, Action> actionMap) {
                return null;
            }

            @Override
            void addToolbarItem(JToolBar tb, Object item, Map<String, Action> actionMap) {
                tb.addSeparator();
            }
        },
        ACTION() {
            @Override
            boolean appliesTo(Object item) {
                return item instanceof String;
            }

            @Override
            JMenuItem menuItem(Object item, Map<String, Action> actionMap) {
                return new JMenuItem(actionMap.get((String) item));
            }

            @Override
            void addToolbarItem(JToolBar tb, Object item, Map<String, Action> actionMap) {
                tb.add(actionMap.get((String) item));
            }
        },
        SPECIAL_ACTION() {
            @Override
            boolean appliesTo(Object item) {
                return item instanceof Map && existsKeyNestedInSingletonMap("type", (Map) item);
            }

            @Override
            JMenuItem menuItem(Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                Action action = actionMap.get(en.getKey());
                Map submap = (Map) en.getValue();
                Object type = submap.get("type");
                if ("CHECKBOX".equals(type)) {
                    return new JCheckBoxMenuItem(action);
                } else if ("RADIO".equals(type)) {
                    return new JRadioButtonMenuItem(action);
                } else {
                    LOG.log(Level.SEVERE, "Unsupported item type: {0}", type);
                    return null;
                }
            }

            @Override
            void addToolbarItem(JToolBar tb, Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                Action action = actionMap.get(en.getKey());
                Map submap = (Map) en.getValue();
                Object type = submap.get("type");
                if ("CHECKBOX".equals(type)) {
                    tb.add(new JCheckBox(action));
                } else if ("RADIO".equals(type)) {
                    tb.add(new JRadioButton(action));
                } else if ("TOGGLE".equals(type)) {
                    tb.add(new JToggleButton(action));
                } else {
                    LOG.log(Level.SEVERE, "Unsupported item type: {0}", type);
                }
            }
        },
        OPTION_MENU() {
            @Override
            boolean appliesTo(Object item) {
                return item instanceof Map && existsKeyNestedInSingletonMap("options", (Map) item);
            }

            @Override
            JMenuItem menuItem(Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                OptionMenuConfig config = new ObjectMapper().convertValue(en.getValue(), OptionMenuConfig.class);
                config.setAction(en.getKey());
                return config.createMenu(actionMap);
            }

            @Override
            void addToolbarItem(JToolBar tb, Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                OptionMenuConfig config = new ObjectMapper().convertValue(en.getValue(), OptionMenuConfig.class);
                config.setAction(en.getKey());
                tb.add(popupButton(config.getName(), config.createPopupMenu(actionMap)));
            }
        },
        SUB_MENU() {
            @Override
            boolean appliesTo(Object item) {
                return item instanceof Map && singletonMapEntry((Map) item).getValue() instanceof List;
            }

            @Override
            JMenuItem menuItem(Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                return createMenu(en.getKey(), (List) en.getValue(), actionMap);
            }

            @Override
            void addToolbarItem(JToolBar tb, Object item, Map<String, Action> actionMap) {
                Entry<String, Object> en = singletonMapEntry((Map) item);
                JPopupMenu popup = new JPopupMenu(en.getKey());
                for (JMenuItem mi : createMenuItemList((List) en.getValue(), actionMap)) {
                    popup.add(mi);
                }
                tb.add(popupButton(en.getKey(), popup));
            }
        };

        abstract boolean appliesTo(Object item);

        abstract JMenuItem menuItem(Object item, Map<String, javax.swing.Action> actionMap);

        abstract void addToolbarItem(JToolBar tb, Object item, Map<String, javax.swing.Action> actionMap);
    }

}