001/*
002 * $Id: McvToolbarEditor.java,v 1.12 2011/03/24 16:06:34 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.ui;
032
033import java.awt.Color;
034import java.awt.Component;
035import java.awt.Graphics;
036import java.awt.event.ActionEvent;
037import java.awt.event.ActionListener;
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.Comparator;
041import java.util.List;
042import java.util.Map;
043import java.util.Vector;
044
045import javax.swing.DefaultListCellRenderer;
046import javax.swing.Icon;
047import javax.swing.JButton;
048import javax.swing.JCheckBox;
049import javax.swing.JComboBox;
050import javax.swing.JComponent;
051import javax.swing.JLabel;
052import javax.swing.JList;
053import javax.swing.JMenu;
054import javax.swing.JPanel;
055import javax.swing.JTextField;
056import javax.swing.ListCellRenderer;
057
058import org.w3c.dom.Document;
059import org.w3c.dom.Element;
060
061import edu.wisc.ssec.mcidasv.ui.UIManager.ActionAttribute;
062import edu.wisc.ssec.mcidasv.ui.UIManager.IdvActions;
063
064import ucar.unidata.idv.IdvResourceManager;
065import ucar.unidata.idv.PluginManager;
066import ucar.unidata.ui.TwoListPanel;
067import ucar.unidata.ui.XmlUi;
068import ucar.unidata.util.GuiUtils;
069import ucar.unidata.util.LogUtil;
070import ucar.unidata.util.TwoFacedObject;
071import ucar.unidata.xml.XmlResourceCollection;
072import ucar.unidata.xml.XmlUtil;
073
074public class McvToolbarEditor implements ActionListener {
075
076    /** Size of the icons to be shown in the {@link TwoListPanel}. */
077    protected static final int ICON_SIZE = 16;
078
079    private static final String MENU_PLUGINEXPORT = "Export to Menu Plugin";
080
081    private static final String MSG_ENTER_NAME = "Please enter a menu name";
082
083    private static final String MSG_SELECT_ENTRIES = 
084        "Please select entries in the Toolbar list";
085
086    /** Add a "space" entry */
087    private static final String CMD_ADDSPACE = "Add Space";
088
089    /** Action command for reloading the toolbar list with original items */
090    private static final String CMD_RELOAD = "Reload Original";
091
092    /** action command */
093    private static final String CMD_EXPORTPLUGIN = "Export Selected to Plugin";
094
095    /** action command */
096    private static final String CMD_EXPORTMENUPLUGIN =
097        "Export Selected to Menu Plugin";
098
099    /** */
100    private static final String TT_EXPORT_SELECT = 
101        "Export the selected items to the plugin";
102
103    private static final String TT_EXPORT_SELECTMENU = 
104        "Export the selected items as a menu to the plugin";
105
106    private static final String TT_OVERWRITE = 
107        "Select this if you want to replace the selected menu with the new" +
108        "menu.";
109
110    /** ID that represents a "space" in the toolbar. */
111    private static final String SPACE = "-space-";
112
113    /** Provides simple IDs for the space entries. */
114    private int spaceCount = 0;
115
116    /** Used to notify the application that a toolbar update should occur. */
117    private UIManager uiManager;
118
119    /** All of the toolbar editor's GUI components. */
120    private JComponent contents;
121
122    /** The GUI component that stores both available and selected actions. */
123    private TwoListPanel twoListPanel;
124
125    /** The toolbar XML resources. */
126    XmlResourceCollection resources;
127
128    /** Used to export toolbars to plugin. */
129    private JTextField menuNameFld;
130
131    /** Used to export toolbars to plugin. */
132    private JComboBox menuIdBox;
133
134    /** Used to export toolbars to plugin. */
135    private JCheckBox menuOverwriteCbx;
136
137    /**
138     * Builds a toolbar editor and associates it with the {@link UIManager}.
139     *
140     * @param mngr The application's UI Manager.
141     */
142    public McvToolbarEditor(final UIManager mngr) {
143        uiManager = mngr;
144        resources = mngr.getIdv().getResourceManager().getXmlResources(
145            IdvResourceManager.RSC_TOOLBAR);
146        init();
147    }
148
149    /**
150     * Returns the icon associated with {@code actionId}.
151     */
152    protected Icon getActionIcon(final String actionId) {
153        return uiManager.getActionIcon(actionId, UIManager.ToolbarStyle.SMALL);
154    }
155
156    /**
157     * Determines if a given toolbar entry (in the form of a 
158     * {@link ucar.unidata.util.TwoFacedObject}) represents a space.
159     * 
160     * @param tfo The entry to test.
161     * 
162     * @return Whether or not the entry represents a space.
163     */
164    public static boolean isSpace(final TwoFacedObject tfo) {
165        return tfo.toString().equals(SPACE);
166    }
167
168    /**
169     * @return Current toolbar contents as action IDs mapped to labels.
170     */
171    private List<TwoFacedObject> getCurrentToolbar() {
172        List<TwoFacedObject> icons = new ArrayList<TwoFacedObject>();
173        List<String> currentIcons = uiManager.getCachedButtons();
174        IdvActions allActions = uiManager.getCachedActions();
175
176        for (String actionId : currentIcons) {
177            TwoFacedObject tfo;
178            if (actionId != null) {
179                String desc = allActions.getAttributeForAction(actionId, ActionAttribute.DESCRIPTION);
180                if (desc == null)
181                    desc = "No description associated with action \""+actionId+"\"";
182                tfo = new TwoFacedObject(desc, actionId);
183            } else {
184                tfo = new TwoFacedObject(SPACE, SPACE + (spaceCount++));
185            }
186            icons.add(tfo);
187        }
188        return icons;
189    }
190
191    /**
192     * Returns a {@link List} of {@link TwoFacedObject}s containing all of the
193     * actions known to McIDAS-V.
194     */
195    private List<TwoFacedObject> getAllActions() {
196        IdvActions allActions = uiManager.getCachedActions();
197        List<TwoFacedObject> actions = new ArrayList<TwoFacedObject>();
198
199        List<String> actionIds = allActions.getAttributes(ActionAttribute.ID);
200        for (String actionId : actionIds) {
201            String label = allActions.getAttributeForAction(actionId, ActionAttribute.DESCRIPTION);
202            if (label == null)
203                label = actionId;
204            actions.add(new TwoFacedObject(label, actionId));
205        }
206        return actions;
207    }
208
209    /**
210     * Returns the {@link TwoListPanel} being used to store
211     * the lists of available and selected actions.
212     */
213    public TwoListPanel getTLP() {
214        return twoListPanel;
215    }
216
217    /**
218     * Returns the {@link JComponent} that contains all of the toolbar editor's
219     * UI components.
220     */
221    public JComponent getContents() {
222        return contents;
223    }
224
225    /**
226     * Initializes the editor window contents.
227     */
228    private void init() {
229        List<TwoFacedObject> currentIcons = getCurrentToolbar();
230        List<TwoFacedObject> actions = sortTwoFaced(getAllActions());
231
232        JButton addSpaceButton = new JButton("Add space");
233        addSpaceButton.setActionCommand(CMD_ADDSPACE);
234        addSpaceButton.addActionListener(this);
235
236        JButton reloadButton = new JButton(CMD_RELOAD);
237        reloadButton.setActionCommand(CMD_RELOAD);
238        reloadButton.addActionListener(this);
239
240        JButton export1Button = new JButton(CMD_EXPORTPLUGIN);
241        export1Button.setToolTipText(TT_EXPORT_SELECT);
242        export1Button.setActionCommand(CMD_EXPORTPLUGIN);
243        export1Button.addActionListener(this);
244
245        JButton export2Button = new JButton(CMD_EXPORTMENUPLUGIN);
246        export2Button.setToolTipText(TT_EXPORT_SELECTMENU);
247        export2Button.setActionCommand(CMD_EXPORTMENUPLUGIN);
248        export2Button.addActionListener(this);
249
250        List<JComponent> buttons = new ArrayList<JComponent>(); 
251        buttons.add(new JLabel(" "));
252        buttons.add(addSpaceButton);
253        buttons.add(reloadButton);
254        buttons.add(new JLabel(" "));
255        buttons.add(export1Button);
256        buttons.add(export2Button);
257
258        JPanel extra = GuiUtils.vbox(buttons);
259
260        twoListPanel =
261            new TwoListPanel(actions, "Actions", currentIcons, "Toolbar", extra);
262
263        ListCellRenderer renderer = new IconCellRenderer(this);
264        twoListPanel.getToList().setCellRenderer(renderer);
265        twoListPanel.getFromList().setCellRenderer(renderer);
266
267        contents = GuiUtils.centerBottom(twoListPanel, new JLabel(" "));
268    }
269
270    /**
271     * Export the selected actions as a menu to the plugin manager.
272     *
273     * @param tfos selected actions
274     */
275    private void doExportToMenu(Object[] tfos) {
276        if (menuNameFld == null) {
277            menuNameFld = new JTextField("", 10);
278
279            Map<String, JMenu> menuIds = uiManager.getMenuIds();
280
281            Vector<TwoFacedObject> menuIdItems = new Vector<TwoFacedObject>();
282            menuIdItems.add(new TwoFacedObject("None", null));
283
284            for (String id : menuIds.keySet()) {
285                JMenu menu = menuIds.get(id);
286                menuIdItems.add(new TwoFacedObject(menu.getText(), id));
287            }
288
289            menuIdBox = new JComboBox(menuIdItems);
290            menuOverwriteCbx = new JCheckBox("Overwrite", false);
291            menuOverwriteCbx.setToolTipText(TT_OVERWRITE);
292        }
293
294        GuiUtils.tmpInsets = GuiUtils.INSETS_5;
295        JComponent dialogContents = GuiUtils.doLayout(new Component[] {
296                                        GuiUtils.rLabel("Menu Name:"),
297                                        menuNameFld,
298                                        GuiUtils.rLabel("Add to Menu:"),
299                                        GuiUtils.left(
300                                            GuiUtils.hbox(
301                                                menuIdBox,
302                                                menuOverwriteCbx)) }, 2,
303                                                    GuiUtils.WT_NY,
304                                                    GuiUtils.WT_N);
305        PluginManager pluginManager = uiManager.getIdv().getPluginManager();
306        while (true) {
307            if (!GuiUtils.askOkCancel(MENU_PLUGINEXPORT, dialogContents)) {
308                return;
309            }
310
311            String menuName = menuNameFld.getText().trim();
312            if (menuName.length() == 0) {
313                LogUtil.userMessage(MSG_ENTER_NAME);
314                continue;
315            }
316
317            StringBuffer xml = new StringBuffer();
318            xml.append(XmlUtil.XML_HEADER);
319            String idXml = "";
320
321            TwoFacedObject menuIdTfo = 
322                (TwoFacedObject)menuIdBox.getSelectedItem();
323
324            if (menuIdTfo.getId() != null) {
325                idXml = XmlUtil.attr("id", menuIdTfo.getId().toString());
326                if (menuOverwriteCbx.isSelected())
327                    idXml = idXml + XmlUtil.attr("replace", "true");
328            }
329
330            xml.append("<menus>\n");
331            xml.append("<menu label=\"" + menuName + "\" " + idXml + ">\n");
332            for (int i = 0; i < tfos.length; i++) {
333                TwoFacedObject tfo = (TwoFacedObject)tfos[i];
334                if (isSpace(tfo)) {
335                    xml.append("<separator/>\n");
336                } else {
337                    xml.append(
338                        XmlUtil.tag(
339                            "menuitem",
340                            XmlUtil.attrs(
341                                "label", tfo.toString(), "action",
342                                "action:" + tfo.getId().toString())));
343                }
344            }
345            xml.append("</menu></menus>\n");
346            pluginManager.addText(xml.toString(), "menubar.xml");
347            return;
348        }
349    }
350
351    /**
352     * Export the actions
353     *
354     * @param tfos the actions
355     */
356    private void doExport(Object[] tfos) {
357        StringBuffer content = new StringBuffer();
358        for (int i = 0; i < tfos.length; i++) {
359            TwoFacedObject tfo = (TwoFacedObject) tfos[i];
360            if (tfo.toString().equals(SPACE)) {
361                content.append("<filler/>\n");
362            } else {
363                content.append(
364                    XmlUtil.tag(
365                        "button",
366                        XmlUtil.attr(
367                            "action", "action:" + tfo.getId().toString())));
368            }
369        }
370        StringBuffer xml = new StringBuffer();
371        xml.append(XmlUtil.XML_HEADER);
372        xml.append(
373            XmlUtil.tag(
374                "panel",
375                XmlUtil.attrs("layout", "flow", "margin", "4", "vspace", "0")
376                + XmlUtil.attrs(
377                    "hspace", "2", "i:space", "2", "i:width",
378                    "5"), content.toString()));
379        LogUtil.userMessage(
380            "Note, if a user has changed their toolbar the plugin toolbar will be ignored");
381        uiManager.getIdv().getPluginManager().addText(xml.toString(),
382                "toolbar.xml");
383    }
384
385    /**
386     * Handles events such as exporting plugins, reloading contents, and adding
387     * spaces.
388     * 
389     * @param ae The event that invoked this method.
390     */
391    public void actionPerformed(ActionEvent ae) {
392        String c = ae.getActionCommand();
393        if (c.equals(CMD_EXPORTMENUPLUGIN) || c.equals(CMD_EXPORTPLUGIN)) {
394            Object[] tfos = twoListPanel.getToList().getSelectedValues();
395            if (tfos.length == 0)
396                LogUtil.userMessage(MSG_SELECT_ENTRIES);
397            else if (c.equals(CMD_EXPORTMENUPLUGIN))
398                doExportToMenu(tfos);
399            else
400                doExport(tfos);
401        }
402        else if (c.equals(CMD_RELOAD)) {
403            twoListPanel.reload();
404        } 
405        else if (c.equals(CMD_ADDSPACE)) {
406            twoListPanel.insertEntry(
407                new TwoFacedObject(SPACE, SPACE+(spaceCount++)));
408        }
409    }
410
411    /**
412     * Has <code>twoListPanel</code> been changed?
413     *
414     * @return <code>true</code> if there have been changes, <code>false</code>
415     * otherwise.
416     */
417    public boolean anyChanges() {
418        return twoListPanel.getChanged();
419    }
420
421    /**
422     * Writes out the toolbar xml.
423     */
424    public void doApply() {
425        Document doc  = resources.getWritableDocument("<panel/>");
426        Element  root = resources.getWritableRoot("<panel/>");
427        root.setAttribute(XmlUi.ATTR_LAYOUT, XmlUi.LAYOUT_FLOW);
428        root.setAttribute(XmlUi.ATTR_MARGIN, "4");
429        root.setAttribute(XmlUi.ATTR_VSPACE, "0");
430        root.setAttribute(XmlUi.ATTR_HSPACE, "2");
431        root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_SPACE), "2");
432        root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_WIDTH), "5");
433
434        XmlUtil.removeChildren(root);
435        List<TwoFacedObject> icons = twoListPanel.getCurrentEntries();
436        for (TwoFacedObject tfo : icons) {
437            Element element;
438            if (isSpace(tfo)) {
439                element = doc.createElement(XmlUi.TAG_FILLER);
440                element.setAttribute(XmlUi.ATTR_WIDTH, "5");
441            } else {
442                element = doc.createElement(XmlUi.TAG_BUTTON);
443                element.setAttribute(XmlUi.ATTR_ACTION,
444                                     "action:" + tfo.getId().toString());
445            }
446            root.appendChild(element);
447        }
448        try {
449            resources.writeWritable();
450        } catch (Exception exc) {
451            LogUtil.logException("Writing toolbar", exc);
452        }
453    }
454
455    /**
456     * <p>
457     * Sorts a {@link List} of 
458     * {@link TwoFacedObject}s by label. Case is ignored.
459     * </p>
460     * 
461     * @param objs The list that needs some sortin' out.
462     * 
463     * @return The sorted contents of <tt>objs</tt>.
464     */
465    private List<TwoFacedObject> sortTwoFaced(final List<TwoFacedObject> objs) {
466        Comparator<TwoFacedObject> comp = new Comparator<TwoFacedObject>() {
467            public int compare(final TwoFacedObject a, final TwoFacedObject b) {
468                return ((String)a.getLabel()).compareToIgnoreCase((String)b.getLabel());
469            }
470        };
471
472        List<TwoFacedObject> reordered = new ArrayList<TwoFacedObject>(objs);
473        Collections.sort(reordered, comp);
474        return reordered;
475    }
476
477    /**
478     * Renders a toolbar action and its icon within the {@link TwoListPanel}'s 
479     * {@link JList}s.
480     */
481    private static class IconCellRenderer implements ListCellRenderer {
482        /** Icon that represents spaces in the current toolbar actions. */
483        private static final Icon SPACE_ICON = 
484            new SpaceIcon(McvToolbarEditor.ICON_SIZE);
485
486        /** Used to capture the normal cell renderer behaviors. */
487        private DefaultListCellRenderer defaultRenderer = 
488            new DefaultListCellRenderer();
489
490        /** Used to determine the action ID to icon associations. */
491        private McvToolbarEditor editor;
492
493        /**
494         * Associates this renderer with the {@link McvToolbarEditor} that
495         * created it.
496         * 
497         * @param editor Toolbar editor that contains relevant action ID to 
498         * icon mapping.
499         * 
500         * @throws NullPointerException if a null McvToolbarEditor was given.
501         */
502        public IconCellRenderer(final McvToolbarEditor editor) {
503            if (editor == null)
504                throw new NullPointerException("Toolbar editor cannot be null");
505            this.editor = editor;
506        }
507
508        // draws the icon associated with the action ID in value next to the
509        // text label.
510        public Component getListCellRendererComponent(JList list, Object value,
511            int index, boolean isSelected, boolean cellHasFocus) 
512        {
513            JLabel renderer = 
514                (JLabel)defaultRenderer.getListCellRendererComponent(list, 
515                    value, index, isSelected, cellHasFocus);
516
517            if (value instanceof TwoFacedObject) {
518                TwoFacedObject tfo = (TwoFacedObject)value;
519                String text = (String)tfo.getLabel();
520                Icon icon;
521                if (!isSpace(tfo))
522                    icon = editor.getActionIcon((String)tfo.getId());
523                else
524                    icon = SPACE_ICON;
525                renderer.setIcon(icon);
526                renderer.setText(text);
527            }
528            return renderer;
529        }
530    }
531
532    /**
533     * {@code SpaceIcon} is a class that represents a {@literal "space"} entry
534     * in the {@link TwoListPanel} that holds the current toolbar actions.
535     * 
536     * <p>Probably only of use in {@link IconCellRenderer}.
537     */
538    private static class SpaceIcon implements Icon {
539        /** {@code dimension * dimension} is the size of the icon. */
540        private final int dimension;
541
542        /** 
543         * Creates a blank, square icon whose dimensions are {@code dimension} 
544         * 
545         * @param dimension Icon dimensions.
546         * 
547         * @throws IllegalArgumentException if dimension is less than or equal 
548         * zero.
549         */
550        public SpaceIcon(final int dimension) {
551            if (dimension <= 0)
552                throw new IllegalArgumentException("Dimension must be a positive integer");
553            this.dimension = dimension;
554        }
555
556        public int getIconHeight() { return dimension; }
557        public int getIconWidth()  { return dimension; }
558        public void paintIcon(Component c, Graphics g, int x, int y) {
559            g.setColor(new Color(255, 255, 255, 0));
560            g.drawRect(0, 0, dimension, dimension);
561        }
562    }
563}
564