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