001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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