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