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