001    /*
002     * $Id: McvToolbarEditor.java,v 1.13 2012/02/19 17:35:50 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
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    
031    package edu.wisc.ssec.mcidasv.ui;
032    
033    import java.awt.Color;
034    import java.awt.Component;
035    import java.awt.Graphics;
036    import java.awt.event.ActionEvent;
037    import java.awt.event.ActionListener;
038    import java.util.ArrayList;
039    import java.util.Collections;
040    import java.util.Comparator;
041    import java.util.List;
042    import java.util.Map;
043    import java.util.Vector;
044    
045    import javax.swing.DefaultListCellRenderer;
046    import javax.swing.Icon;
047    import javax.swing.JButton;
048    import javax.swing.JCheckBox;
049    import javax.swing.JComboBox;
050    import javax.swing.JComponent;
051    import javax.swing.JLabel;
052    import javax.swing.JList;
053    import javax.swing.JMenu;
054    import javax.swing.JPanel;
055    import javax.swing.JTextField;
056    import javax.swing.ListCellRenderer;
057    
058    import org.w3c.dom.Document;
059    import org.w3c.dom.Element;
060    
061    import edu.wisc.ssec.mcidasv.ui.UIManager.ActionAttribute;
062    import edu.wisc.ssec.mcidasv.ui.UIManager.IdvActions;
063    
064    import ucar.unidata.idv.IdvResourceManager;
065    import ucar.unidata.idv.PluginManager;
066    import ucar.unidata.ui.TwoListPanel;
067    import ucar.unidata.ui.XmlUi;
068    import ucar.unidata.util.GuiUtils;
069    import ucar.unidata.util.LogUtil;
070    import ucar.unidata.util.TwoFacedObject;
071    import ucar.unidata.xml.XmlResourceCollection;
072    import ucar.unidata.xml.XmlUtil;
073    
074    public 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