001    /*
002     * This file is part of McIDAS-V
003     *
004     * Copyright 2007-2013
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    
029    package edu.wisc.ssec.mcidasv.ui;
030    
031    import java.awt.Color;
032    import java.awt.Component;
033    import java.awt.Graphics;
034    import java.awt.event.ActionEvent;
035    import java.awt.event.ActionListener;
036    import java.util.ArrayList;
037    import java.util.Collections;
038    import java.util.Comparator;
039    import java.util.List;
040    import java.util.Map;
041    import java.util.Vector;
042    
043    import javax.swing.DefaultListCellRenderer;
044    import javax.swing.Icon;
045    import javax.swing.JButton;
046    import javax.swing.JCheckBox;
047    import javax.swing.JComboBox;
048    import javax.swing.JComponent;
049    import javax.swing.JLabel;
050    import javax.swing.JList;
051    import javax.swing.JMenu;
052    import javax.swing.JPanel;
053    import javax.swing.JTextField;
054    import javax.swing.ListCellRenderer;
055    
056    import org.w3c.dom.Document;
057    import org.w3c.dom.Element;
058    
059    import edu.wisc.ssec.mcidasv.ui.UIManager.ActionAttribute;
060    import edu.wisc.ssec.mcidasv.ui.UIManager.IdvActions;
061    
062    import ucar.unidata.idv.IdvResourceManager;
063    import ucar.unidata.idv.PluginManager;
064    import ucar.unidata.ui.TwoListPanel;
065    import ucar.unidata.ui.XmlUi;
066    import ucar.unidata.util.GuiUtils;
067    import ucar.unidata.util.LogUtil;
068    import ucar.unidata.util.TwoFacedObject;
069    import ucar.unidata.xml.XmlResourceCollection;
070    import ucar.unidata.xml.XmlUtil;
071    
072    public 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 tfo.toString().equals(SPACE);
164        }
165    
166        /**
167         * @return Current toolbar contents as action IDs mapped to labels.
168         */
169        private List<TwoFacedObject> getCurrentToolbar() {
170            List<TwoFacedObject> icons = new ArrayList<TwoFacedObject>();
171            List<String> currentIcons = uiManager.getCachedButtons();
172            IdvActions allActions = uiManager.getCachedActions();
173    
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                    tfo = new TwoFacedObject(desc, actionId);
181                } else {
182                    tfo = new TwoFacedObject(SPACE, SPACE + (spaceCount++));
183                }
184                icons.add(tfo);
185            }
186            return icons;
187        }
188    
189        /**
190         * Returns a {@link List} of {@link TwoFacedObject}s containing all of the
191         * actions known to McIDAS-V.
192         */
193        private List<TwoFacedObject> getAllActions() {
194            IdvActions allActions = uiManager.getCachedActions();
195            List<TwoFacedObject> actions = new ArrayList<TwoFacedObject>();
196    
197            List<String> actionIds = allActions.getAttributes(ActionAttribute.ID);
198            for (String actionId : actionIds) {
199                String label = allActions.getAttributeForAction(actionId, ActionAttribute.DESCRIPTION);
200                if (label == null)
201                    label = actionId;
202                actions.add(new TwoFacedObject(label, actionId));
203            }
204            return actions;
205        }
206    
207        /**
208         * Returns the {@link TwoListPanel} being used to store
209         * the lists of available and selected actions.
210         */
211        public TwoListPanel getTLP() {
212            return twoListPanel;
213        }
214    
215        /**
216         * Returns the {@link JComponent} that contains all of the toolbar editor's
217         * UI components.
218         */
219        public JComponent getContents() {
220            return contents;
221        }
222    
223        /**
224         * Initializes the editor window contents.
225         */
226        private void init() {
227            List<TwoFacedObject> currentIcons = getCurrentToolbar();
228            List<TwoFacedObject> actions = sortTwoFaced(getAllActions());
229    
230            JButton addSpaceButton = new JButton("Add space");
231            addSpaceButton.setActionCommand(CMD_ADDSPACE);
232            addSpaceButton.addActionListener(this);
233    
234            JButton reloadButton = new JButton(CMD_RELOAD);
235            reloadButton.setActionCommand(CMD_RELOAD);
236            reloadButton.addActionListener(this);
237    
238            JButton export1Button = new JButton(CMD_EXPORTPLUGIN);
239            export1Button.setToolTipText(TT_EXPORT_SELECT);
240            export1Button.setActionCommand(CMD_EXPORTPLUGIN);
241            export1Button.addActionListener(this);
242    
243            JButton export2Button = new JButton(CMD_EXPORTMENUPLUGIN);
244            export2Button.setToolTipText(TT_EXPORT_SELECTMENU);
245            export2Button.setActionCommand(CMD_EXPORTMENUPLUGIN);
246            export2Button.addActionListener(this);
247    
248            List<JComponent> buttons = new ArrayList<JComponent>(); 
249            buttons.add(new JLabel(" "));
250            buttons.add(addSpaceButton);
251            buttons.add(reloadButton);
252            buttons.add(new JLabel(" "));
253            buttons.add(export1Button);
254            buttons.add(export2Button);
255    
256            JPanel extra = GuiUtils.vbox(buttons);
257    
258            twoListPanel =
259                new TwoListPanel(actions, "Actions", currentIcons, "Toolbar", extra);
260    
261            ListCellRenderer renderer = new IconCellRenderer(this);
262            twoListPanel.getToList().setCellRenderer(renderer);
263            twoListPanel.getFromList().setCellRenderer(renderer);
264    
265            contents = GuiUtils.centerBottom(twoListPanel, new JLabel(" "));
266        }
267    
268        /**
269         * Export the selected actions as a menu to the plugin manager.
270         *
271         * @param tfos selected actions
272         */
273        private void doExportToMenu(Object[] tfos) {
274            if (menuNameFld == null) {
275                menuNameFld = new JTextField("", 10);
276    
277                Map<String, JMenu> menuIds = uiManager.getMenuIds();
278    
279                Vector<TwoFacedObject> menuIdItems = new Vector<TwoFacedObject>();
280                menuIdItems.add(new TwoFacedObject("None", null));
281    
282                for (String id : menuIds.keySet()) {
283                    JMenu menu = menuIds.get(id);
284                    menuIdItems.add(new TwoFacedObject(menu.getText(), id));
285                }
286    
287                menuIdBox = new JComboBox(menuIdItems);
288                menuOverwriteCbx = new JCheckBox("Overwrite", false);
289                menuOverwriteCbx.setToolTipText(TT_OVERWRITE);
290            }
291    
292            GuiUtils.tmpInsets = GuiUtils.INSETS_5;
293            JComponent dialogContents = GuiUtils.doLayout(new Component[] {
294                                            GuiUtils.rLabel("Menu Name:"),
295                                            menuNameFld,
296                                            GuiUtils.rLabel("Add to Menu:"),
297                                            GuiUtils.left(
298                                                GuiUtils.hbox(
299                                                    menuIdBox,
300                                                    menuOverwriteCbx)) }, 2,
301                                                        GuiUtils.WT_NY,
302                                                        GuiUtils.WT_N);
303            PluginManager pluginManager = uiManager.getIdv().getPluginManager();
304            while (true) {
305                if (!GuiUtils.askOkCancel(MENU_PLUGINEXPORT, dialogContents)) {
306                    return;
307                }
308    
309                String menuName = menuNameFld.getText().trim();
310                if (menuName.length() == 0) {
311                    LogUtil.userMessage(MSG_ENTER_NAME);
312                    continue;
313                }
314    
315                StringBuffer xml = new StringBuffer();
316                xml.append(XmlUtil.XML_HEADER);
317                String idXml = "";
318    
319                TwoFacedObject menuIdTfo = 
320                    (TwoFacedObject)menuIdBox.getSelectedItem();
321    
322                if (menuIdTfo.getId() != null) {
323                    idXml = XmlUtil.attr("id", menuIdTfo.getId().toString());
324                    if (menuOverwriteCbx.isSelected())
325                        idXml = idXml + XmlUtil.attr("replace", "true");
326                }
327    
328                xml.append("<menus>\n");
329                xml.append("<menu label=\"" + menuName + "\" " + idXml + ">\n");
330                for (int i = 0; i < tfos.length; i++) {
331                    TwoFacedObject tfo = (TwoFacedObject)tfos[i];
332                    if (isSpace(tfo)) {
333                        xml.append("<separator/>\n");
334                    } else {
335                        xml.append(
336                            XmlUtil.tag(
337                                "menuitem",
338                                XmlUtil.attrs(
339                                    "label", tfo.toString(), "action",
340                                    "action:" + tfo.getId().toString())));
341                    }
342                }
343                xml.append("</menu></menus>\n");
344                pluginManager.addText(xml.toString(), "menubar.xml");
345                return;
346            }
347        }
348    
349        /**
350         * Export the actions
351         *
352         * @param tfos the actions
353         */
354        private void doExport(Object[] tfos) {
355            StringBuffer content = new StringBuffer();
356            for (int i = 0; i < tfos.length; i++) {
357                TwoFacedObject tfo = (TwoFacedObject) tfos[i];
358                if (tfo.toString().equals(SPACE)) {
359                    content.append("<filler/>\n");
360                } else {
361                    content.append(
362                        XmlUtil.tag(
363                            "button",
364                            XmlUtil.attr(
365                                "action", "action:" + tfo.getId().toString())));
366                }
367            }
368            StringBuffer xml = new StringBuffer();
369            xml.append(XmlUtil.XML_HEADER);
370            xml.append(
371                XmlUtil.tag(
372                    "panel",
373                    XmlUtil.attrs("layout", "flow", "margin", "4", "vspace", "0")
374                    + XmlUtil.attrs(
375                        "hspace", "2", "i:space", "2", "i:width",
376                        "5"), content.toString()));
377            LogUtil.userMessage(
378                "Note, if a user has changed their toolbar the plugin toolbar will be ignored");
379            uiManager.getIdv().getPluginManager().addText(xml.toString(),
380                    "toolbar.xml");
381        }
382    
383        /**
384         * Handles events such as exporting plugins, reloading contents, and adding
385         * spaces.
386         * 
387         * @param ae The event that invoked this method.
388         */
389        public void actionPerformed(ActionEvent ae) {
390            String c = ae.getActionCommand();
391            if (c.equals(CMD_EXPORTMENUPLUGIN) || c.equals(CMD_EXPORTPLUGIN)) {
392                Object[] tfos = twoListPanel.getToList().getSelectedValues();
393                if (tfos.length == 0)
394                    LogUtil.userMessage(MSG_SELECT_ENTRIES);
395                else if (c.equals(CMD_EXPORTMENUPLUGIN))
396                    doExportToMenu(tfos);
397                else
398                    doExport(tfos);
399            }
400            else if (c.equals(CMD_RELOAD)) {
401                twoListPanel.reload();
402            } 
403            else if (c.equals(CMD_ADDSPACE)) {
404                twoListPanel.insertEntry(
405                    new TwoFacedObject(SPACE, SPACE+(spaceCount++)));
406            }
407        }
408    
409        /**
410         * Has <code>twoListPanel</code> been changed?
411         *
412         * @return <code>true</code> if there have been changes, <code>false</code>
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}s 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 <tt>objs</tt>.
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            /** Icon that represents spaces in the current toolbar actions. */
481            private static final Icon SPACE_ICON = 
482                new SpaceIcon(McvToolbarEditor.ICON_SIZE);
483    
484            /** Used to capture the normal cell renderer behaviors. */
485            private DefaultListCellRenderer defaultRenderer = 
486                new DefaultListCellRenderer();
487    
488            /** Used to determine the action ID to icon associations. */
489            private McvToolbarEditor editor;
490    
491            /**
492             * Associates this renderer with the {@link McvToolbarEditor} that
493             * created it.
494             * 
495             * @param editor Toolbar editor that contains relevant action ID to 
496             * icon mapping.
497             * 
498             * @throws NullPointerException if a null McvToolbarEditor was given.
499             */
500            public IconCellRenderer(final McvToolbarEditor editor) {
501                if (editor == null)
502                    throw new NullPointerException("Toolbar editor cannot be null");
503                this.editor = editor;
504            }
505    
506            // draws the icon associated with the action ID in value next to the
507            // text label.
508            public Component getListCellRendererComponent(JList list, Object value,
509                int index, boolean isSelected, boolean cellHasFocus) 
510            {
511                JLabel renderer = 
512                    (JLabel)defaultRenderer.getListCellRendererComponent(list, 
513                        value, index, isSelected, cellHasFocus);
514    
515                if (value instanceof TwoFacedObject) {
516                    TwoFacedObject tfo = (TwoFacedObject)value;
517                    String text = (String)tfo.getLabel();
518                    Icon icon;
519                    if (!isSpace(tfo))
520                        icon = editor.getActionIcon((String)tfo.getId());
521                    else
522                        icon = SPACE_ICON;
523                    renderer.setIcon(icon);
524                    renderer.setText(text);
525                }
526                return renderer;
527            }
528        }
529    
530        /**
531         * {@code SpaceIcon} is a class that represents a {@literal "space"} entry
532         * in the {@link TwoListPanel} that holds the current toolbar actions.
533         * 
534         * <p>Probably only of use in {@link IconCellRenderer}.
535         */
536        private static class SpaceIcon implements Icon {
537            /** {@code dimension * dimension} is the size of the icon. */
538            private final int dimension;
539    
540            /** 
541             * Creates a blank, square icon whose dimensions are {@code dimension} 
542             * 
543             * @param dimension Icon dimensions.
544             * 
545             * @throws IllegalArgumentException if dimension is less than or equal 
546             * zero.
547             */
548            public SpaceIcon(final int dimension) {
549                if (dimension <= 0)
550                    throw new IllegalArgumentException("Dimension must be a positive integer");
551                this.dimension = dimension;
552            }
553    
554            public int getIconHeight() { return dimension; }
555            public int getIconWidth()  { return dimension; }
556            public void paintIcon(Component c, Graphics g, int x, int y) {
557                g.setColor(new Color(255, 255, 255, 0));
558                g.drawRect(0, 0, dimension, dimension);
559            }
560        }
561    }
562