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 static java.util.Objects.requireNonNull; 032 033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 034import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.list; 035import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newHashSet; 036import static edu.wisc.ssec.mcidasv.util.XPathUtils.elements; 037 038import java.awt.BorderLayout; 039import java.awt.Color; 040import java.awt.Component; 041import java.awt.Dimension; 042import java.awt.Font; 043import java.awt.Graphics; 044import java.awt.Insets; 045import java.awt.Rectangle; 046import java.awt.Toolkit; 047import java.awt.event.ActionEvent; 048import java.awt.event.ActionListener; 049import java.awt.event.ComponentEvent; 050import java.awt.event.ComponentListener; 051import java.awt.event.MouseAdapter; 052import java.awt.event.MouseEvent; 053import java.awt.event.MouseListener; 054import java.awt.event.WindowEvent; 055import java.awt.event.WindowListener; 056import java.io.IOException; 057import java.io.InputStream; 058import java.lang.reflect.Method; 059import java.net.URL; 060import java.util.ArrayList; 061import java.util.Collection; 062import java.util.Collections; 063import java.util.HashMap; 064import java.util.Hashtable; 065import java.util.LinkedHashMap; 066import java.util.LinkedHashSet; 067import java.util.LinkedList; 068import java.util.List; 069import java.util.Locale; 070import java.util.Map; 071import java.util.Set; 072import java.util.Map.Entry; 073import java.util.Vector; 074import java.util.concurrent.ConcurrentHashMap; 075 076import javax.swing.AbstractAction; 077import javax.swing.Action; 078import javax.swing.BorderFactory; 079import javax.swing.ButtonGroup; 080import javax.swing.Icon; 081import javax.swing.ImageIcon; 082import javax.swing.JButton; 083import javax.swing.JCheckBox; 084import javax.swing.JComboBox; 085import javax.swing.JComponent; 086import javax.swing.JDialog; 087import javax.swing.JFrame; 088import javax.swing.JLabel; 089import javax.swing.JMenu; 090import javax.swing.JMenuBar; 091import javax.swing.JMenuItem; 092import javax.swing.JPanel; 093import javax.swing.JPopupMenu; 094import javax.swing.JRadioButtonMenuItem; 095import javax.swing.JScrollPane; 096import javax.swing.JTabbedPane; 097import javax.swing.JTextArea; 098import javax.swing.JTextField; 099import javax.swing.JToolBar; 100import javax.swing.JTree; 101import javax.swing.KeyStroke; 102import javax.swing.ScrollPaneConstants; 103import javax.swing.SwingUtilities; 104import javax.swing.WindowConstants; 105import javax.swing.border.BevelBorder; 106import javax.swing.event.MenuEvent; 107import javax.swing.event.MenuListener; 108import javax.swing.text.JTextComponent; 109import javax.swing.tree.DefaultMutableTreeNode; 110import javax.swing.tree.DefaultTreeCellRenderer; 111import javax.swing.tree.DefaultTreeModel; 112import javax.swing.tree.TreeSelectionModel; 113 114import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 115import org.fife.ui.rsyntaxtextarea.Theme; 116import org.slf4j.Logger; 117import org.slf4j.LoggerFactory; 118import org.w3c.dom.Document; 119import org.w3c.dom.Element; 120import org.w3c.dom.NodeList; 121 122import ucar.unidata.data.DataChoice; 123import ucar.unidata.data.DataOperand; 124import ucar.unidata.data.DataSelection; 125import ucar.unidata.data.DataSourceImpl; 126import ucar.unidata.data.DerivedDataChoice; 127import ucar.unidata.data.UserOperandValue; 128import ucar.unidata.geoloc.LatLonPointImpl; 129import ucar.unidata.idv.ControlDescriptor; 130import ucar.unidata.idv.IdvPersistenceManager; 131import ucar.unidata.idv.IdvPreferenceManager; 132import ucar.unidata.idv.IdvResourceManager; 133import ucar.unidata.idv.IntegratedDataViewer; 134import ucar.unidata.idv.SavedBundle; 135import ucar.unidata.idv.ViewManager; 136import ucar.unidata.idv.ViewState; 137import ucar.unidata.idv.IdvResourceManager.XmlIdvResource; 138import ucar.unidata.idv.control.DisplayControlImpl; 139import ucar.unidata.idv.ui.DataControlDialog; 140import ucar.unidata.idv.ui.DataSelectionWidget; 141import ucar.unidata.idv.ui.IdvComponentGroup; 142import ucar.unidata.idv.ui.IdvComponentHolder; 143import ucar.unidata.idv.ui.IdvUIManager; 144import ucar.unidata.idv.ui.IdvWindow; 145import ucar.unidata.idv.ui.IdvXmlUi; 146import ucar.unidata.idv.ui.ViewPanel; 147import ucar.unidata.idv.ui.WindowInfo; 148import ucar.unidata.metdata.NamedStationTable; 149import ucar.unidata.ui.ComponentHolder; 150import ucar.unidata.ui.HttpFormEntry; 151import ucar.unidata.ui.LatLonWidget; 152import ucar.unidata.ui.RovingProgress; 153import ucar.unidata.ui.XmlUi; 154import ucar.unidata.util.GuiUtils; 155import ucar.unidata.util.IOUtil; 156import ucar.unidata.util.LayoutUtil; 157import ucar.unidata.util.LogUtil; 158import ucar.unidata.util.MenuUtil; 159import ucar.unidata.util.Misc; 160import ucar.unidata.util.Msg; 161import ucar.unidata.util.ObjectListener; 162import ucar.unidata.util.PatternFileFilter; 163import ucar.unidata.util.StringUtil; 164import ucar.unidata.util.TwoFacedObject; 165import ucar.unidata.xml.XmlResourceCollection; 166import ucar.unidata.xml.XmlUtil; 167import edu.wisc.ssec.mcidasv.Constants; 168import edu.wisc.ssec.mcidasv.McIDASV; 169import edu.wisc.ssec.mcidasv.PersistenceManager; 170import edu.wisc.ssec.mcidasv.StateManager; 171import edu.wisc.ssec.mcidasv.supportform.McvStateCollector; 172import edu.wisc.ssec.mcidasv.supportform.SupportForm; 173import edu.wisc.ssec.mcidasv.util.Contract; 174import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 175import edu.wisc.ssec.mcidasv.util.MemoryMonitor; 176 177/** 178 * <p>Derive our own UI manager to do some specific things: 179 * 180 * <ul> 181 * <li>Removing displays</li> 182 * <li>Showing the dashboard</li> 183 * <li>Adding toolbar customization options</li> 184 * <li>Implement the McIDAS-V toolbar as a JToolbar.</li> 185 * <li>Deal with bundles without component groups.</li> 186 * </ul> 187 */ 188// TODO: investigate moving similar unpersisting code to persistence manager. 189public class UIManager extends IdvUIManager implements ActionListener { 190 191 private static final Logger logger = 192 LoggerFactory.getLogger(UIManager.class); 193 194 /** Id of the "New Display Tab" menu item for the file menu */ 195 public static final String MENU_NEWDISPLAY_TAB = "file.new.display.tab"; 196 197 /** The tag in the xml ui for creating the special example chooser */ 198 public static final String TAG_EXAMPLECHOOSER = "examplechooser"; 199 200 /** 201 * Used to keep track of ViewManagers inside a bundle. 202 */ 203 public static final HashMap<String, ViewManager> savedViewManagers = 204 new HashMap<>(); 205 206 /** 207 * Property name for whether or not the description field of the support 208 * form should perform line wrapping. 209 * */ 210 public static final String PROP_WRAP_SUPPORT_DESC = 211 "mcidasv.supportform.wrap"; 212 213 /** Action command for manipulating the size of the toolbar icons. */ 214 private static final String ACT_ICON_TYPE = "action.toolbar.seticonsize"; 215 216 /** Action command for removing all displays */ 217 private static final String ACT_REMOVE_DISPLAYS = "action.displays.remove"; 218 219 /** Action command for showing the dashboard */ 220 private static final String ACT_SHOW_DASHBOARD = "action.dashboard.show"; 221 222 /** Action command for showing the dashboard */ 223 private static final String ACT_SHOW_DATASELECTOR = "action.dataselector.show"; 224 225 /** Action command for showing the dashboard */ 226 private static final String ACT_SHOW_DISPLAYCONTROLLER = "action.displaycontroller.show"; 227 228 /** Action command for displaying the toolbar preference tab. */ 229 private static final String ACT_SHOW_PREF = "action.toolbar.showprefs"; 230 231 /** Message shown when an unknown action is in the toolbar. */ 232 private static final String BAD_ACTION_MSG = "Unknown action (%s) found in your toolbar. McIDAS-V will continue to load, but there will be no button associated with %s."; 233 234 /** Menu ID for the {@literal "Restore Saved Views"} submenu. */ 235 public static final String MENU_NEWVIEWS = "menu.tools.projections.restoresavedviews"; 236 237 /** Label for the "link" to the toolbar customization preference tab. */ 238 private static final String LBL_TB_EDITOR = "Customize..."; 239 240 /** Current representation of toolbar actions. */ 241 private ToolbarStyle currentToolbarStyle = 242 getToolbarStylePref(ToolbarStyle.MEDIUM); 243 244 /** The IDV property that reflects the size of the icons. */ 245 private static final String PROP_ICON_SIZE = "mcv.ui.iconsize"; 246 247 /** The URL of the script that processes McIDAS-V support requests. */ 248 private static final String SUPPORT_REQ_URL = 249 "https://www.ssec.wisc.edu/mcidas/misc/mc-v/supportreq/support.php"; 250 251 /** Separator to use between window title components. */ 252 protected static final String TITLE_SEPARATOR = " - "; 253 254 /** 255 * The currently "displayed" actions. Keeping this List allows us to get 256 * away with only reading the XML files upon starting the application and 257 * only writing the XML files upon exiting the application. This will avoid 258 * those redrawing delays. 259 */ 260 private List<String> cachedButtons; 261 262 /** Stores all available actions. */ 263 private final IdvActions idvActions; 264 265 /** Map of skin ids to their skin resource index. */ 266 private Map<String, Integer> skinIds = readSkinIds(); 267 268 /** An easy way to figure out who is holding a given ViewManager. */ 269 private Map<ViewManager, ComponentHolder> viewManagers = new HashMap<>(); 270 271 private int componentHolderCount; 272 273 private int componentGroupCount; 274 275 /** Cache for the results of {@link #getWindowTitleFromSkin(int)}. */ 276 private final Map<Integer, String> skinToTitle = new ConcurrentHashMap<>(); 277 278 /** Maps menu IDs to {@link JMenu}s. */ 279// private Hashtable<String, JMenu> menuIds; 280 private Hashtable<String, JMenuItem> menuIds; 281 282 /** The splash screen (minus easter egg). */ 283 private McvSplash splash; 284 285 /** 286 * A list of the toolbars that the IDV is playing with. Used to apply 287 * changes to *all* the toolbars in the application. 288 */ 289 private List<JToolBar> toolbars; 290 291 /** 292 * Keeping the reference to the toolbar menu mouse listener allows us to 293 * avoid constantly rebuilding the menu. 294 */ 295 private MouseListener toolbarMenu; 296 297 /** Keep the dashboard around so we don't have to re-create it each time. */ 298 protected IdvWindow dashboard; 299 300 /** False until {@link #initDone()}. */ 301 protected boolean initDone = false; 302 303 /** IDV instantiation--nice to keep around to reduce getIdv() calls. */ 304 private IntegratedDataViewer idv; 305 306 /** 307 * Hands off our IDV instantiation to IdvUiManager. 308 * 309 * @param idv The idv 310 */ 311 public UIManager(IntegratedDataViewer idv) { 312 super(idv); 313 314 this.idv = idv; 315 316 // cache the appropriate data for the toolbar. it'll make updates 317 // much snappier 318 idvActions = new IdvActions(getIdv(), IdvResourceManager.RSC_ACTIONS); 319 cachedButtons = readToolbar(); 320 } 321 322 /** 323 * Override the IDV method so that we hide component group button. 324 */ 325 @Override public IdvWindow createNewWindow(List viewManagers, 326 boolean notifyCollab, String title, String skinPath, Element skinRoot, 327 boolean show, WindowInfo windowInfo) 328 { 329 330 if (windowInfo != null) { 331 logger.trace("creating window: title='{}' bounds: {}", title, windowInfo.getBounds()); 332 } else { 333 logger.trace("creating window: title='{}' bounds: (no windowinfo)", title); 334 } 335 336 if (Constants.DATASELECTOR_NAME.equals(title)) { 337 show = false; 338 } 339 if (skinPath.contains("dashboard.xml")) { 340 show = false; 341 } 342 343 // used to force any new "display" windows to be the same size as the current window. 344 IdvWindow previousWindow = IdvWindow.getActiveWindow(); 345 346 IdvWindow w = super.createNewWindow(viewManagers, notifyCollab, title, 347 skinPath, skinRoot, show, windowInfo); 348 349 String iconPath = idv.getProperty(Constants.PROP_APP_ICON, null); 350 ImageIcon icon = GuiUtils.getImageIcon(iconPath, getClass(), true); 351 w.setIconImage(icon.getImage()); 352 353 // try to catch the dashboard 354 if (Constants.DATASELECTOR_NAME.equals(w.getTitle())) { 355 setDashboard(w); 356 } else if (!w.getComponentGroups().isEmpty()) { 357 // otherwise we need to hide the component group header and explicitly 358 // set the size of the window. 359 w.getComponentGroups().get(0).setShowHeader(false); 360 if (previousWindow != null) { 361 Rectangle r = previousWindow.getBounds(); 362 363 w.setBounds(new Rectangle(r.x, r.y, r.width, r.height)); 364 } 365 } else { 366 logger.trace("creating window with no component groups"); 367 } 368 369 initDisplayShortcuts(w); 370 371 RovingProgress progress = 372 (RovingProgress)w.getComponent(IdvUIManager.COMP_PROGRESSBAR); 373 374 if (progress != null) { 375 progress.start(); 376 } 377 return w; 378 } 379 380 /** 381 * Create the display window described by McIDAS-V's default display skin 382 * 383 * @return {@link IdvWindow} that was created. 384 */ 385 public IdvWindow buildDefaultSkin() { 386 return createNewWindow(new ArrayList(), false); 387 } 388 389 /** 390 * Create a new IdvWindow for the given viewManager. Put the 391 * contents of the viewManager into the window 392 * 393 * @return The new window 394 */ 395 public IdvWindow buildEmptyWindow() { 396 Element root = null; 397 String path = null; 398 String skinName = null; 399 400 path = getIdv().getProperty("mcv.ui.emptycompgroup", (String)null); 401 if (path != null) { 402 path = path.trim(); 403 } 404 405 if ((path != null) && (path.length() > 0)) { 406 try { 407 root = XmlUtil.getRoot(path, getClass()); 408 skinName = getStateManager().getTitle(); 409 String tmp = XmlUtil.getAttribute(root, "name", (String)null); 410 if (tmp == null) { 411 tmp = IOUtil.stripExtension(IOUtil.getFileTail(path)); 412 } 413 skinName = skinName + " - " + tmp; 414 } catch (Exception exc) { 415 logger.error("error building empty window", exc); 416 } 417 } 418 419 IdvWindow window = createNewWindow(new ArrayList(), false, skinName, path, root, true, null); 420 window.setVisible(true); 421 return window; 422 } 423 424 425 /** 426 * Sets {@link #dashboard} to {@code window}. This method also adds some 427 * listeners to {@code window} so that the state of the dashboard is 428 * automatically saved. 429 * 430 * @param window The dashboard. Nothing happens if {@link #dashboard} has 431 * already been set, or this parameter is {@code null}. 432 */ 433 private void setDashboard(final IdvWindow window) { 434 if (window == null || dashboard != null) 435 return; 436 437 dashboard = window; 438 dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 439 440 final Component comp = dashboard.getComponent(); 441 final ucar.unidata.idv.StateManager state = getIdv().getStateManager(); 442 443 // for some reason the component listener's "componentHidden" method 444 // would not fire the *first* time the dashboard is closed/hidden. 445 // the window listener catches it. 446 dashboard.addWindowListener(new WindowListener() { 447 public void windowClosed(final WindowEvent e) { 448 Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE); 449 if (saveViz) { 450 state.putPreference(Constants.PROP_SHOWDASHBOARD, false); 451 } 452 } 453 454 public void windowActivated(final WindowEvent e) { } 455 public void windowClosing(final WindowEvent e) { } 456 public void windowDeactivated(final WindowEvent e) { } 457 public void windowDeiconified(final WindowEvent e) { } 458 public void windowIconified(final WindowEvent e) { } 459 public void windowOpened(final WindowEvent e) { } 460 }); 461 462 dashboard.getComponent().addComponentListener(new ComponentListener() { 463 public void componentMoved(final ComponentEvent e) { 464 state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds()); 465 } 466 467 public void componentResized(final ComponentEvent e) { 468 state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds()); 469 } 470 471 public void componentShown(final ComponentEvent e) { 472 Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE); 473 if (saveViz) 474 state.putPreference(Constants.PROP_SHOWDASHBOARD, true); 475 } 476 477 public void componentHidden(final ComponentEvent e) { 478 Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE); 479 if (saveViz) 480 state.putPreference(Constants.PROP_SHOWDASHBOARD, false); 481 } 482 }); 483 484 Rectangle bounds = (Rectangle)state.getPreferenceOrProperty(Constants.PROP_DASHBOARD_BOUNDS); 485 if (bounds != null) 486 comp.setBounds(bounds); 487 } 488 489 /** 490 * Attempts to add all component holders in {@code info} to 491 * {@code group}. Especially useful when unpersisting a bundle and 492 * attempting to deal with its component groups. 493 * 494 * @param info The window we want to process. 495 * @param group Receives the holders in {@code info}. 496 * 497 * @return True if there were component groups in {@code info}. 498 */ 499 public boolean unpersistComponentGroups(final WindowInfo info, 500 final McvComponentGroup group) { 501 Collection<Object> comps = info.getPersistentComponents().values(); 502 503 if (comps.isEmpty()) { 504 return false; 505 } 506 507 for (Object comp : comps) { 508 // comp is typically always an IdvComponentGroup, but there are 509 // no guarantees... 510 if (! (comp instanceof IdvComponentGroup)) { 511 System.err.println("DEBUG: non IdvComponentGroup found in persistent components: " 512 + comp.getClass().getName()); 513 continue; 514 } 515 516 IdvComponentGroup bundleGroup = (IdvComponentGroup)comp; 517 518 // need to make a copy of this list to avoid a rogue 519 // ConcurrentModificationException 520 // TODO: determine which threads are clobbering each other. 521 List<IdvComponentHolder> holders = 522 new ArrayList<>(bundleGroup.getDisplayComponents()); 523 524 for (IdvComponentHolder holder : holders) { 525 group.quietAddComponent(holder); 526 } 527 528 group.redoLayout(); 529 530 JComponent groupContents = group.getContents(); 531 groupContents.setPreferredSize(groupContents.getSize()); 532 } 533 return true; 534 } 535 536 /** 537 * Override IdvUIManager's loadLookAndFeel so that we can force the IDV to 538 * load the Aqua look and feel if requested from the command line. 539 */ 540 @Override public void loadLookAndFeel() { 541 if (McIDASV.useAquaLookAndFeel) { 542 // since we must rely on the IDV to do the actual loading (due to 543 // our UIManager's name conflicting with javax.swing.UIManager's 544 // name), save the user's preference, replace it temporarily and 545 // have the IDV do its thing, then overwrite the temp preference 546 // with the saved preference. Blah! 547 String previousLF = getStore().get(PREF_LOOKANDFEEL, (String)null); 548 getStore().put(PREF_LOOKANDFEEL, "apple.laf.AquaLookAndFeel"); 549 super.loadLookAndFeel(); 550 getStore().put(PREF_LOOKANDFEEL, previousLF); 551 552 // locale code was taken out of IDVUIManager's "loadLookAndFeel". 553 // 554 // if the locale preference is set to "system", there will *NOT* 555 // be a PREF_LOCALE -> "Locale.EXAMPLE" key/value pair in main.xml; 556 // as you're using the default state of the preference. 557 // this also means that if there is a non-null value associated 558 // with PREF_LOCALE, the user has selected the US locale. 559 String locale = getStore().get(PREF_LOCALE, (String)null); 560 if (locale != null) { 561 Locale.setDefault(Locale.US); 562 } 563 } else { 564 super.loadLookAndFeel(); 565 } 566 } 567 568 /** 569 * Apply a specified RSyntaxTextArea theme to a given text area. 570 * 571 * @param clazz Class used to read {@code themePath}. 572 * @param textArea Text area to which the theme should be applied. 573 * @param themePath Path to the RSyntaxTextArea theme to apply. 574 */ 575 public static void applyTextTheme(Class<?> clazz, RSyntaxTextArea textArea, String themePath) { 576 try { 577 InputStream in = clazz.getResourceAsStream(themePath); 578 Theme theme = Theme.load(in); 579 theme.apply(textArea); 580 } catch (IOException e) { 581 logger.error("Could not change theme", e); 582 } 583 } 584 585 @Override public void handleWindowActivated(final IdvWindow window) { 586 List<ViewManager> viewManagers = window.getViewManagers(); 587 ViewManager newActive = null; 588 long lastActivatedTime = -1; 589 590 for (ViewManager viewManager : viewManagers) { 591 if (viewManager.getContents() == null) 592 continue; 593 594 if (!viewManager.getContents().isVisible()) 595 continue; 596 597 lastActiveFrame = window; 598 599 if (viewManager.getLastTimeActivated() > lastActivatedTime) { 600 newActive = viewManager; 601 lastActivatedTime = viewManager.getLastTimeActivated(); 602 } 603 } 604 605 if (newActive != null) { 606 getVMManager().setLastActiveViewManager(newActive); 607 } 608 } 609 610 /** 611 * Handles the windowing portions of bundle loading: wraps things in 612 * component groups (if needed), merges things into existing windows or 613 * creates new windows, and removes displays and data if asked nicely. 614 * 615 * @param windows WindowInfos from the bundle. 616 * @param newViewManagers ViewManagers stored in the bundle. 617 * @param okToMerge Put bundled things into an existing window? 618 * @param fromCollab Did this come from the collab stuff? 619 * @param didRemoveAll Remove all data and displays? 620 * 621 * @see IdvUIManager#unpersistWindowInfo(List, List, boolean, boolean, 622 * boolean) 623 */ 624 @Override public void unpersistWindowInfo(List windows, 625 List newViewManagers, boolean okToMerge, boolean fromCollab, 626 boolean didRemoveAll) 627 { 628 if (newViewManagers == null) { 629 newViewManagers = new ArrayList<>(); 630 } 631 632 // keep track of the "old" state if the user wants to remove things. 633 boolean mergeLayers = ((PersistenceManager)getPersistenceManager()).getMergeBundledLayers(); 634 List<IdvComponentHolder> holdersBefore = new ArrayList<>(); 635 List<IdvWindow> windowsBefore = new ArrayList<>(); 636 if (didRemoveAll) { 637 holdersBefore.addAll(McVGuiUtils.getAllComponentHolders()); 638 windowsBefore.addAll(McVGuiUtils.getAllDisplayWindows()); 639 } 640 641 // no reason to kill the displays if there aren't any windows in the 642 // bundle! 643 if ((mergeLayers) || (didRemoveAll && !windows.isEmpty())) { 644 killOldDisplays(holdersBefore, windowsBefore, (okToMerge || mergeLayers)); 645 } 646 647 for (WindowInfo info : (List<WindowInfo>)windows) { 648 newViewManagers.removeAll(info.getViewManagers()); 649 makeBundledDisplays(info, okToMerge, mergeLayers, fromCollab); 650 } 651 } 652 653 /** 654 * Removes data and displays that existed prior to loading a bundle. 655 * 656 * @param oldHolders Component holders around before loading. 657 * @param oldWindows Windows around before loading. 658 * @param merge Were the bundle contents merged into an existing window? 659 */ 660 public void killOldDisplays(final List<IdvComponentHolder> oldHolders, 661 final List<IdvWindow> oldWindows, final boolean merge) 662 { 663// System.err.println("killOldDisplays: merge="+merge); 664 // if we merged, this will ensure that any old holders in the merged 665 // window also get removed. 666 if (merge) { 667 for (IdvComponentHolder holder : oldHolders) { 668 holder.doRemove(); 669 } 670 } 671 672 // mop up any windows that no longer have component holders. 673 for (IdvWindow window : oldWindows) { 674 IdvComponentGroup group = McVGuiUtils.getComponentGroup(window); 675 676 List<IdvComponentHolder> holders = 677 McVGuiUtils.getComponentHolders(group); 678 679 // if the old set of holders contains all of this window's 680 // holders, this window can be deleted: 681 // 682 // this works fine for merging because the okToMerge stuff will 683 // remove all old holders from the current window, but if the 684 // bundle was merged into this window, containsAll() will fail 685 // due to there being a new holder. 686 // 687 // if the bundle was loaded into its own window, then 688 // all the old windows will pass this test. 689 if (oldHolders.containsAll(holders)) { 690 group.doRemove(); 691 window.dispose(); 692 } 693 } 694 } 695 696 697 /** 698 * A hack because Unidata moved the skins (taken from 699 * {@link IdvPersistenceManager}). 700 * 701 * @param skinPath original path 702 * @return fixed path 703 */ 704 private String fixSkinPath(String skinPath) { 705 if (skinPath == null) { 706 return null; 707 } 708 if (StringUtil.stringMatch( 709 skinPath, "^/ucar/unidata/idv/resources/[^/]+\\.xml")) { 710 skinPath = 711 StringUtil.replace(skinPath, "/ucar/unidata/idv/resources/", 712 "/ucar/unidata/idv/resources/skins/"); 713 } 714 return skinPath; 715 } 716 717 /** 718 * <p> 719 * Uses the contents of {@code info} to rebuild a display that has been 720 * bundled. If {@code merge} is true, the displayable parts of the bundle 721 * will be put into the current window. Otherwise a new window is created 722 * and the relevant parts of the bundle will occupy that new window. 723 * </p> 724 * 725 * @param info WindowInfo to use with creating the new window. 726 * @param merge Merge created things into an existing window? 727 * @param mergeLayers Whether or not layers should be merged. 728 * @param fromCollab Whether or not this is in response to a 729 * {@literal "collaboration"} event. 730 */ 731 public void makeBundledDisplays(final WindowInfo info, final boolean merge, final boolean mergeLayers, final boolean fromCollab) { 732 // need a way to get the last active view manager (for real) 733 IdvWindow window = IdvWindow.getActiveWindow(); 734 ViewManager last = ((PersistenceManager)getPersistenceManager()).getLastViewManager(); 735 String skinPath = info.getSkinPath(); 736 737 // create a new window if we're not merging (or the active window is 738 // invalid), otherwise sticking with the active window is fine. 739 if ((merge || (mergeLayers)) && last != null) { 740 List<IdvWindow> windows = IdvWindow.getWindows(); 741 for (IdvWindow tmpWindow : windows) { 742 if (tmpWindow.getComponentGroups().isEmpty()) { 743 continue; 744 } 745 List<IdvComponentGroup> groups = tmpWindow.getComponentGroups(); 746 for (IdvComponentGroup group : groups) { 747 List<IdvComponentHolder> holders = group.getDisplayComponents(); 748 for (IdvComponentHolder holder : holders) { 749 List<ViewManager> vms = holder.getViewManagers(); 750 if (vms != null && vms.contains(last)) { 751 window = tmpWindow; 752 753 if (mergeLayers) { 754 mergeLayers(info, window, fromCollab); 755 } 756 break; 757 } 758 } 759 } 760 } 761 } 762 else if ((window == null) || (!merge) || (window.getComponentGroups().isEmpty())) { 763 try { 764 Element skinRoot = 765 XmlUtil.getRoot(Constants.BLANK_COMP_GROUP, getClass()); 766 767 window = createNewWindow(null, false, "McIDAS-V", 768 Constants.BLANK_COMP_GROUP, skinRoot, false, null); 769 770 window.setBounds(info.getBounds()); 771 window.setVisible(true); 772 773 } catch (Throwable e) { 774 logger.error("Problem making bundled displays", e); 775 } 776 } 777 778 McvComponentGroup group = 779 (McvComponentGroup)window.getComponentGroups().get(0); 780 781 // if the bundle contains only component groups, ensure they get merged 782 // into group. 783 unpersistComponentGroups(info, group); 784 } 785 786 private void mergeLayers(final WindowInfo info, final IdvWindow window, final boolean fromCollab) { 787 List<ViewManager> newVms = McVGuiUtils.getViewManagers(info); 788 List<ViewManager> oldVms = McVGuiUtils.getViewManagers(window); 789 790 if (oldVms.size() == newVms.size()) { 791 List<ViewManager> merged = new ArrayList<>(); 792 for (int vmIdx = 0; 793 (vmIdx < newVms.size()) 794 && (vmIdx < oldVms.size()); 795 vmIdx++) 796 { 797 ViewManager newVm = newVms.get(vmIdx); 798 ViewManager oldVm = oldVms.get(vmIdx); 799 if (oldVm.canBe(newVm)) { 800 oldVm.initWith(newVm, fromCollab); 801 merged.add(newVm); 802 } 803 } 804 805 Collection<Object> comps = info.getPersistentComponents().values(); 806 807 for (Object comp : comps) { 808 if (!(comp instanceof IdvComponentGroup)) 809 continue; 810 811 IdvComponentGroup group = (IdvComponentGroup)comp; 812 List<IdvComponentHolder> holders = group.getDisplayComponents(); 813 List<IdvComponentHolder> emptyHolders = new ArrayList<>(); 814 for (IdvComponentHolder holder : holders) { 815 List<ViewManager> vms = holder.getViewManagers(); 816 for (ViewManager vm : merged) { 817 if (vms.contains(vm)) { 818 vms.remove(vm); 819 getVMManager().removeViewManager(vm); 820 List<DisplayControlImpl> controls = vm.getControlsForLegend(); 821 for (DisplayControlImpl dc : controls) { 822 try { 823 dc.doRemove(); 824 } catch (Exception e) { } 825 getViewPanel().removeDisplayControl(dc); 826 getViewPanel().viewManagerDestroyed(vm); 827 828 vm.clearDisplays(); 829 830 } 831 } 832 } 833 holder.setViewManagers(vms); 834 835 if (vms.isEmpty()) { 836 emptyHolders.add(holder); 837 } 838 } 839 840 for (IdvComponentHolder holder : emptyHolders) { 841 holder.doRemove(); 842 group.removeComponent(holder); 843 } 844 } 845 } 846 } 847 848 /** 849 * Make a window title. The format for window titles is: 850 * {@literal <window>TITLE_SEPARATOR<document>} 851 * 852 * @param win Window title. 853 * @param doc Document or window sub-content. 854 * @return Formatted window title. 855 */ 856 protected static String makeTitle(final String win, final String doc) { 857 if (win == null) 858 return ""; 859 else if (doc == null) 860 return win; 861 else if (doc.equals("untitled")) 862 return win; 863 864 return win.concat(TITLE_SEPARATOR).concat(doc); 865 } 866 867 /** 868 * Make a window title. The format for window titles is: 869 * 870 * <pre> 871 * <window>TITLE_SEPARATOR<document>TITLE_SEPARATOR<other> 872 * </pre> 873 * 874 * @param window Window title. 875 * @param document Document or window sub content. 876 * @param other Other content to include. 877 * @return Formatted window title. 878 */ 879 protected static String makeTitle(final String window, 880 final String document, final String other) 881 { 882 if (other == null) 883 return makeTitle(window, document); 884 885 return window.concat(TITLE_SEPARATOR).concat(document).concat( 886 TITLE_SEPARATOR).concat(other); 887 } 888 889 /** 890 * Split window title using {@code TITLE_SEPARATOR}. 891 * 892 * @param title The window title to split 893 * @return Parts of the title with the white space trimmed. 894 */ 895 protected static String[] splitTitle(final String title) { 896 String[] splt = title.split(TITLE_SEPARATOR); 897 for (int i = 0; i < splt.length; i++) { 898 splt[i] = splt[i].trim(); 899 } 900 return splt; 901 } 902 903 /** 904 * Overridden to prevent the IDV's {@code StateManager} instantiation of {@link ucar.unidata.idv.mac.MacBridge}. 905 * McIDAS-V uses different approaches for OS X compatibility. 906 * 907 * @return Always returns {@code false}. 908 * 909 * @deprecated Use {@link edu.wisc.ssec.mcidasv.McIDASV#isMac()} instead. 910 */ 911 // TODO: be sure to bring back the override annotation once we've upgraded our idv.jar. 912 public boolean isMac() { 913 return false; 914 } 915 916 /* (non-Javadoc) 917 * @see ucar.unidata.idv.ui.IdvUIManager#about() 918 */ 919 public void about() { 920 java.awt.EventQueue.invokeLater(() -> { 921 AboutFrame frame = new AboutFrame((McIDASV)idv); 922 // pop up the window right away; the system information tab won't 923 // be populated until the user has selected the tab. 924 frame.setVisible(true); 925 }); 926 } 927 928 /** 929 * Handles all the ActionEvents that occur for widgets contained within 930 * this class. It's not so pretty, but it isolates the event handling in 931 * one place (and reduces the number of action listeners to one). 932 * 933 * @param e The event that triggered the call to this method. 934 */ 935 public void actionPerformed(ActionEvent e) { 936 String cmd = e.getActionCommand(); 937 boolean toolbarEditEvent = false; 938 939 // handle selecting large icons 940 if (cmd.startsWith(ToolbarStyle.LARGE.getAction())) { 941 currentToolbarStyle = ToolbarStyle.LARGE; 942 toolbarEditEvent = true; 943 } 944 945 // handle selecting medium icons 946 else if (cmd.startsWith(ToolbarStyle.MEDIUM.getAction())) { 947 currentToolbarStyle = ToolbarStyle.MEDIUM; 948 toolbarEditEvent = true; 949 } 950 951 // handle selecting small icons 952 else if (cmd.startsWith(ToolbarStyle.SMALL.getAction())) { 953 currentToolbarStyle = ToolbarStyle.SMALL; 954 toolbarEditEvent = true; 955 } 956 957 // handle the user selecting the show toolbar preference menu item 958 else if (cmd.startsWith(ACT_SHOW_PREF)) { 959 IdvPreferenceManager prefs = idv.getPreferenceManager(); 960 prefs.showTab(Constants.PREF_LIST_TOOLBAR); 961 toolbarEditEvent = true; 962 } 963 964 // handle the user toggling the size of the icon 965 else if (cmd.startsWith(ACT_ICON_TYPE)) 966 toolbarEditEvent = true; 967 968 // handle the user removing displays 969 else if (cmd.startsWith(ACT_REMOVE_DISPLAYS)) 970 idv.removeAllDisplays(); 971 972 // handle popping up the dashboard. 973 else if (cmd.startsWith(ACT_SHOW_DASHBOARD)) 974 showDashboard(); 975 976 // handle popping up the data explorer. 977 else if (cmd.startsWith(ACT_SHOW_DATASELECTOR)) 978 showDashboard("Data Sources"); 979 980 // handle popping up the display controller. 981 else if (cmd.startsWith(ACT_SHOW_DISPLAYCONTROLLER)) 982 showDashboard("Layer Controls"); 983 984 else 985 System.err.println("Unsupported action event!"); 986 987 // if the user did something to change the toolbar, hide the current 988 // toolbar, replace it, and then make the new toolbar visible. 989 if (toolbarEditEvent == true) { 990 991 getStateManager().writePreference(PROP_ICON_SIZE, 992 currentToolbarStyle.getSizeAsString()); 993 994 // destroy the menu so it can be properly updated during rebuild 995 toolbarMenu = null; 996 997 for (JToolBar toolbar : toolbars) { 998 toolbar.setVisible(false); 999 populateToolbar(toolbar); 1000 toolbar.setVisible(true); 1001 } 1002 } 1003 } 1004 1005 public JComponent getDisplaySelectorComponent() { 1006 DefaultMutableTreeNode root = new DefaultMutableTreeNode(""); 1007 DefaultTreeModel model = new DefaultTreeModel(root); 1008 final JTree tree = new JTree(model); 1009 tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); 1010 tree.getSelectionModel().setSelectionMode( 1011 TreeSelectionModel.SINGLE_TREE_SELECTION 1012 ); 1013 DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); 1014 renderer.setIcon(null); 1015 renderer.setOpenIcon(null); 1016 renderer.setClosedIcon(null); 1017 tree.setCellRenderer(renderer); 1018 1019 for (IdvWindow w : McVGuiUtils.getAllDisplayWindows()) { 1020 String title = w.getTitle(); 1021 TwoFacedObject winTFO = new TwoFacedObject(title, w); 1022 DefaultMutableTreeNode winNode = new DefaultMutableTreeNode(winTFO); 1023 for (IdvComponentHolder h : McVGuiUtils.getComponentHolders(w)) { 1024 String hName = h.getName(); 1025 TwoFacedObject tmp = new TwoFacedObject(hName, h); 1026 DefaultMutableTreeNode holderNode = new DefaultMutableTreeNode(tmp); 1027 //for (ViewManager v : (List<ViewManager>)h.getViewManagers()) { 1028 for (int i = 0; i < h.getViewManagers().size(); i++) { 1029 ViewManager v = (ViewManager)h.getViewManagers().get(i); 1030 String vName = v.getName(); 1031 TwoFacedObject tfo = null; 1032 1033 if (vName != null && vName.length() > 0) 1034 tfo = new TwoFacedObject(vName, v); 1035 else 1036 tfo = new TwoFacedObject(Constants.PANEL_NAME + " " + (i+1), v); 1037 1038 holderNode.add(new DefaultMutableTreeNode(tfo)); 1039 } 1040 winNode.add(holderNode); 1041 } 1042 root.add(winNode); 1043 } 1044 1045 // select the appropriate view 1046 tree.addTreeSelectionListener(evt -> { 1047 DefaultMutableTreeNode node = 1048 (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); 1049 if (node == null || !(node.getUserObject() instanceof TwoFacedObject)) { 1050 return; 1051 } 1052 TwoFacedObject tfo = (TwoFacedObject) node.getUserObject(); 1053 1054 Object obj = tfo.getId(); 1055 if (obj instanceof ViewManager) { 1056 ViewManager viewManager = (ViewManager) tfo.getId(); 1057 idv.getVMManager().setLastActiveViewManager(viewManager); 1058 } else if (obj instanceof McvComponentHolder) { 1059 McvComponentHolder holder = (McvComponentHolder)obj; 1060 holder.setAsActiveTab(); 1061 } else if (obj instanceof IdvWindow) { 1062 IdvWindow window1 = (IdvWindow)obj; 1063 window1.toFront(); 1064 } 1065 }); 1066 1067 // expand all the nodes 1068 for (int i = 0; i < tree.getRowCount(); i++) { 1069 tree.expandPath(tree.getPathForRow(i)); 1070 } 1071 1072 return tree; 1073 } 1074 1075 /** 1076 * Builds the JPopupMenu that appears when a user right-clicks in the 1077 * toolbar. 1078 * 1079 * @return MouseListener that listens for right-clicks in the toolbar. 1080 */ 1081 private MouseListener constructToolbarMenu() { 1082 JMenuItem large = ToolbarStyle.LARGE.buildMenuItem(this); 1083 JMenuItem medium = ToolbarStyle.MEDIUM.buildMenuItem(this); 1084 JMenuItem small = ToolbarStyle.SMALL.buildMenuItem(this); 1085 1086 JMenuItem toolbarPrefs = new JMenuItem(LBL_TB_EDITOR); 1087 toolbarPrefs.setActionCommand(ACT_SHOW_PREF); 1088 toolbarPrefs.addActionListener(this); 1089 1090 switch (currentToolbarStyle) { 1091 case LARGE: 1092 large.setSelected(true); 1093 break; 1094 1095 case MEDIUM: 1096 medium.setSelected(true); 1097 break; 1098 1099 case SMALL: 1100 small.setSelected(true); 1101 break; 1102 1103 default: 1104 break; 1105 } 1106 1107 ButtonGroup group = new ButtonGroup(); 1108 group.add(large); 1109 group.add(medium); 1110 group.add(small); 1111 1112 JPopupMenu popup = new JPopupMenu(); 1113 popup.setBorder(new BevelBorder(BevelBorder.RAISED)); 1114 popup.add(large); 1115 popup.add(medium); 1116 popup.add(small); 1117 popup.addSeparator(); 1118 popup.add(toolbarPrefs); 1119 1120 return new PopupListener(popup); 1121 } 1122 1123 /** 1124 * Queries the stored preferences to determine the preferred 1125 * {@link ToolbarStyle}. If there was no preference, {@code defaultStyle} 1126 * is used. 1127 * 1128 * @param defaultStyle {@code ToolbarStyle} to use if there was no value 1129 * associated with the toolbar style preference. 1130 * 1131 * @return The preferred {@code ToolbarStyle} or {@code defaultStyle}. 1132 * 1133 * @throws AssertionError if {@code PROP_ICON_SIZE} had returned an integer 1134 * value that did not correspond to a valid {@code ToolbarStyle}. 1135 */ 1136 private ToolbarStyle getToolbarStylePref(final ToolbarStyle defaultStyle) { 1137 assert defaultStyle != null; 1138 String storedStyle = getStateManager().getPreferenceOrProperty(PROP_ICON_SIZE, (String)null); 1139 if (storedStyle == null) { 1140 return defaultStyle; 1141 } 1142 1143 int intSize = Integer.valueOf(storedStyle); 1144 1145 // can't switch on intSize using ToolbarStyles as the case... 1146 if (intSize == ToolbarStyle.LARGE.getSize()) { 1147 return ToolbarStyle.LARGE; 1148 } 1149 if (intSize == ToolbarStyle.MEDIUM.getSize()) { 1150 return ToolbarStyle.MEDIUM; 1151 } 1152 if (intSize == ToolbarStyle.SMALL.getSize()) { 1153 return ToolbarStyle.SMALL; 1154 } 1155 1156 // uh oh 1157 throw new AssertionError("Invalid preferred icon size: " + intSize); 1158 } 1159 1160 /** 1161 * Given a valid action and icon size, build a JButton for the toolbar. 1162 * 1163 * @param action The action whose corresponding icon we want. 1164 * 1165 * @return A JButton for the given action with an appropriate-sized icon. 1166 */ 1167 private JButton buildToolbarButton(String action) { 1168 IdvAction a = idvActions.getAction(action); 1169 if (a == null) { 1170 return null; 1171 } 1172 1173 JButton button = new JButton(idvActions.getStyledIconFor(action, currentToolbarStyle)); 1174 1175 // the IDV will take care of action handling! so nice! 1176 button.addActionListener(idv); 1177 button.setActionCommand(a.getAttribute(ActionAttribute.ACTION)); 1178 button.addMouseListener(toolbarMenu); 1179 button.setToolTipText(a.getAttribute(ActionAttribute.DESCRIPTION)); 1180 1181 return button; 1182 } 1183 1184 @Override public JPanel doMakeStatusBar(final IdvWindow window) { 1185 if (window == null) { 1186 return new JPanel(); 1187 } 1188 JLabel msgLabel = new JLabel(" "); 1189 LogUtil.addMessageLogger(msgLabel); 1190 1191 window.setComponent(COMP_MESSAGELABEL, msgLabel); 1192 1193 IdvXmlUi xmlUI = window.getXmlUI(); 1194 if (xmlUI != null) { 1195 xmlUI.addComponent(COMP_MESSAGELABEL, msgLabel); 1196 } 1197 JLabel waitLabel = new JLabel(IdvWindow.getNormalIcon()); 1198 waitLabel.addMouseListener(new ObjectListener(null) { 1199 public void mouseClicked(final MouseEvent e) { 1200 getIdv().clearWaitCursor(); 1201 } 1202 }); 1203 window.setComponent(COMP_WAITLABEL, waitLabel); 1204 1205 RovingProgress progress = doMakeRovingProgressBar(); 1206 window.setComponent(COMP_PROGRESSBAR, progress); 1207 1208// Monitoring label = new MemoryPanel(); 1209// ((McIDASV)getIdv()).getMonitorManager().addListener(label); 1210// window.setComponent(Constants.COMP_MONITORPANEL, label); 1211 1212 boolean isClockShowing = Boolean.getBoolean(getStateManager().getPreferenceOrProperty(PROP_SHOWCLOCK_VIEW, "true")); 1213 MemoryMonitor mm = new MemoryMonitor(getStateManager(), 75, 95, isClockShowing); 1214 mm.setBorder(getStatusBorder()); 1215 1216 // MAKE PRETTY NOW! 1217 progress.setBorder(getStatusBorder()); 1218 waitLabel.setBorder(getStatusBorder()); 1219 msgLabel.setBorder(getStatusBorder()); 1220// ((JPanel)label).setBorder(getStatusBorder()); 1221 1222// JPanel msgBar = GuiUtils.leftCenter((JPanel)label, msgLabel); 1223 JPanel msgBar = LayoutUtil.leftCenter(mm, msgLabel); 1224 JPanel statusBar = LayoutUtil.centerRight(msgBar, progress); 1225 statusBar.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); 1226 return statusBar; 1227 } 1228 1229 /** 1230 * Make the roving progress bar 1231 * 1232 * @return Roving progress bar 1233 */ 1234 public RovingProgress doMakeRovingProgressBar() { 1235 RovingProgress progress = new RovingProgress(Constants.MCV_BLUE) { 1236 private Font labelFont; 1237 1238 public void paintInner(Graphics g) { 1239 //Catch if we're not in a wait state 1240 if (!IdvWindow.getWaitState() && super.isRunning()) { 1241 stop(); 1242 return; 1243 } 1244 if (!super.isRunning()) { 1245 super.paintInner(g); 1246 return; 1247 } 1248 super.paintInner(g); 1249 } 1250 1251 public void paintLabel(Graphics g, Rectangle bounds) { 1252 if (labelFont == null) { 1253 labelFont = g.getFont(); 1254 labelFont = labelFont.deriveFont(Font.BOLD); 1255 } 1256 g.setFont(labelFont); 1257 g.setColor(Color.black); 1258 if (DataSourceImpl.getOutstandingGetDataCalls() > 0) { 1259 g.drawString(" Reading data", 5, bounds.height - 4); 1260 } else if (!idv.getAllDisplaysIntialized()){ 1261 g.drawString(" Creating layers", 5, bounds.height - 4); 1262 } 1263 1264 } 1265 1266 public synchronized void stop() { 1267 super.stop(); 1268 super.reset(); 1269 } 1270 }; 1271 progress.setPreferredSize(new Dimension(130, 10)); 1272 return progress; 1273 } 1274 1275 /** 1276 * Overrides the IDV's getToolbarUI so that McV can return its own toolbar 1277 * and not deal with the way the IDV handles toolbars. This method also 1278 * updates the toolbar data member so that other methods can fool around 1279 * with whatever the IDV thinks is a toolbar (without having to rely on the 1280 * IDV window manager code). 1281 * 1282 * <p> 1283 * Not that the IDV code is bad of course--I just can't handle that pause 1284 * while the toolbar is rebuilt! 1285 * </p> 1286 * 1287 * @return A new toolbar based on the contents of toolbar.xml. 1288 */ 1289 @Override public JComponent getToolbarUI() { 1290 if (toolbars == null) { 1291 toolbars = new LinkedList<JToolBar>(); 1292 } 1293 JToolBar toolbar = new JToolBar(); 1294 populateToolbar(toolbar); 1295 toolbars.add(toolbar); 1296 return toolbar; 1297 } 1298 1299 /** 1300 * Return a McV-style toolbar to the IDV. 1301 * 1302 * @return A fancy-pants toolbar. 1303 */ 1304 @Override protected JComponent doMakeToolbar() { 1305 return getToolbarUI(); 1306 } 1307 1308 /** 1309 * Uses the cached XML to create a toolbar. Any updates to the toolbar 1310 * happen almost instantly using this approach. Do note that if there are 1311 * any components in the given toolbar they will be removed. 1312 * 1313 * @param toolbar A reference to the toolbar that needs buttons and stuff. 1314 */ 1315 private void populateToolbar(JToolBar toolbar) { 1316 // clear out the toolbar's current denizens, if any. just a nicety. 1317 if (toolbar.getComponentCount() > 0) { 1318 toolbar.removeAll(); 1319 } 1320 1321 // ensure that the toolbar popup menu appears 1322 if (toolbarMenu == null) { 1323 toolbarMenu = constructToolbarMenu(); 1324 } 1325 1326 toolbar.addMouseListener(toolbarMenu); 1327 1328 // add the actions that should appear in the toolbar. 1329 for (String action : cachedButtons) { 1330 1331 // null actions are considered separators. 1332 if (action == null) { 1333 toolbar.addSeparator(); 1334 } 1335 // otherwise we've got a button to add 1336 else { 1337 JButton b = buildToolbarButton(action); 1338 if (b != null) { 1339 toolbar.add(b); 1340 } else { 1341 String err = String.format(BAD_ACTION_MSG, action, action); 1342 LogUtil.userErrorMessage(err); 1343 } 1344 } 1345 } 1346 1347 if (getStore().get(Constants.PREF_SHOW_SYSTEM_BUNDLES, true)) { 1348 toolbar.addSeparator(); 1349 1350 BundleTreeNode treeRoot = buildBundleTree(SavedBundle.TYPE_SYSTEM); 1351 if (treeRoot != null) { 1352 1353 // add the favorite bundles to the toolbar (hello Tom Whittaker!) 1354 for (BundleTreeNode tmp : treeRoot.getChildren()) { 1355 1356 // if this node doesn't have a bundle, it's considered a parent 1357 if (tmp.getBundle() == null) { 1358 addBundleTree(toolbar, tmp); 1359 } 1360 // otherwise it's just another button to add. 1361 else { 1362 addBundle(toolbar, tmp); 1363 } 1364 } 1365 } 1366 } 1367 1368 toolbar.addSeparator(); 1369 1370 BundleTreeNode treeRoot = buildBundleTree(SavedBundle.TYPE_FAVORITE); 1371 if (treeRoot != null) { 1372 1373 // add the favorite bundles to the toolbar (hello Tom Whittaker!) 1374 for (BundleTreeNode tmp : treeRoot.getChildren()) { 1375 1376 // if this node doesn't have a bundle, it's considered a parent 1377 if (tmp.getBundle() == null) { 1378 addBundleTree(toolbar, tmp); 1379 } 1380 // otherwise it's just another button to add. 1381 else { 1382 addBundle(toolbar, tmp); 1383 } 1384 } 1385 } 1386 } 1387 1388 /** 1389 * Given a reference to the current toolbar and a bundle tree node, build a 1390 * button representation of the bundle and add it to the toolbar. 1391 * 1392 * @param toolbar Toolbar to which we add the bundle. 1393 * @param node Node within the bundle tree that contains our bundle. 1394 */ 1395 private void addBundle(JToolBar toolbar, BundleTreeNode node) { 1396 final SavedBundle bundle = node.getBundle(); 1397 1398 ImageIcon fileIcon = 1399 GuiUtils.getImageIcon("/auxdata/ui/icons/File.gif"); 1400 1401 JButton button = new JButton(node.getName(), fileIcon); 1402 button.setToolTipText("Click to open favorite: " + node.getName()); 1403 button.addActionListener(e -> { 1404 // running in a separate thread is kinda nice! *HEH* 1405 Misc.run(UIManager.this, "processBundle", bundle); 1406 }); 1407 toolbar.add(button); 1408 } 1409 1410 /** 1411 * Handle loading the given {@link SavedBundle SavedBundle}. 1412 * 1413 * <p>Overridden in McIDAS-V to allow {@literal "default"} bundles to 1414 * show the same bundle loading dialog as a {@literal "favorite"} bundle. 1415 * </p> 1416 * 1417 * @param bundle Bundle to process. Cannot be {@code null}. 1418 */ 1419 @Override public void processBundle(SavedBundle bundle) { 1420 showWaitCursor(); 1421 LogUtil.message("Loading bundle: " + bundle.getName()); 1422 int type = bundle.getType(); 1423 boolean checkToRemove = (type == SavedBundle.TYPE_FAVORITE) || (type == SavedBundle.TYPE_SYSTEM); 1424 boolean ok = getPersistenceManager().decodeXmlFile(bundle.getUrl(), 1425 bundle.getName(), 1426 checkToRemove); 1427 if (ok && (bundle.getType() == SavedBundle.TYPE_DATA)) { 1428 showDataSelector(); 1429 } 1430 LogUtil.message(""); 1431 showNormalCursor(); 1432 } 1433 1434 /** 1435 * Builds two things, given a toolbar and a tree node: a JButton that 1436 * represents a {@literal "first-level"} parent node and a JPopupMenu that 1437 * appears upon clicking the JButton. The button is then added to the given 1438 * toolbar. 1439 * 1440 * <p>{@literal "First-level"} means the given node is a child of the root 1441 * node.</p> 1442 * 1443 * @param toolbar Toolbar to which we add the bundle tree. 1444 * @param node Node we want to add. 1445 */ 1446 private void addBundleTree(JToolBar toolbar, BundleTreeNode node) { 1447 ImageIcon catIcon = 1448 GuiUtils.getImageIcon("/auxdata/ui/icons/Folder.gif"); 1449 1450 final JButton button = new JButton(node.getName(), catIcon); 1451 final JPopupMenu popup = new JPopupMenu(); 1452 1453 button.setToolTipText("Show Favorites category: " + node.getName()); 1454 1455 button.addActionListener(new ActionListener() { 1456 public void actionPerformed(ActionEvent e) { 1457 popup.show(button, 0, button.getHeight()); 1458 } 1459 }); 1460 1461 toolbar.add(button); 1462 1463 // recurse through the child nodes 1464 for (BundleTreeNode kid : node.getChildren()) { 1465 buildPopupMenu(kid, popup); 1466 } 1467 } 1468 1469 /** 1470 * Writes the currently displayed toolbar buttons to the toolbar XML. This 1471 * has mostly been ripped off from ToolbarEditor. :( 1472 */ 1473 public void writeToolbar() { 1474 XmlResourceCollection resources = 1475 getResourceManager() 1476 .getXmlResources(IdvResourceManager.RSC_TOOLBAR); 1477 1478 String actionPrefix = "action:"; 1479 1480 // ensure that the IDV can read the XML we're generating. 1481 Document doc = resources.getWritableDocument("<panel/>"); 1482 Element root = resources.getWritableRoot("<panel/>"); 1483 root.setAttribute(XmlUi.ATTR_LAYOUT, XmlUi.LAYOUT_FLOW); 1484 root.setAttribute(XmlUi.ATTR_MARGIN, "4"); 1485 root.setAttribute(XmlUi.ATTR_VSPACE, "0"); 1486 root.setAttribute(XmlUi.ATTR_HSPACE, "2"); 1487 root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_SPACE), "2"); 1488 root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_WIDTH), "5"); 1489 1490 // clear out any pesky kids from previous relationships. XML don't need 1491 // no baby-mama drama. 1492 XmlUtil.removeChildren(root); 1493 1494 // iterate through the actions that have toolbar buttons in use and add 1495 // 'em to the XML. 1496 for (String action : cachedButtons) { 1497 Element e; 1498 if (action != null) { 1499 e = doc.createElement(XmlUi.TAG_BUTTON); 1500 e.setAttribute(XmlUi.ATTR_ACTION, (actionPrefix + action)); 1501 } else { 1502 e = doc.createElement(XmlUi.TAG_FILLER); 1503 e.setAttribute(XmlUi.ATTR_WIDTH, "5"); 1504 } 1505 root.appendChild(e); 1506 } 1507 1508 // write the XML 1509 try { 1510 resources.writeWritable(); 1511 } catch (Exception e) { 1512 logger.error("Problem writing resources", e); 1513 } 1514 } 1515 1516 /** 1517 * Read the contents of the toolbar XML into a List. We're essentially just 1518 * throwing actions into the list. 1519 * 1520 * @return The actions/buttons that live in the toolbar xml. Note that if 1521 * an element is {@code null}, this element represents a {@literal "space"} 1522 * that should appear in both the Toolbar and the Toolbar Preferences. 1523 */ 1524 public List<String> readToolbar() { 1525 final Element root = getToolbarRoot(); 1526 if (root == null) { 1527 return null; 1528 } 1529 1530 final NodeList elements = XmlUtil.getElements(root); 1531 List<String> data = new ArrayList<>(elements.getLength()); 1532 for (int i = 0; i < elements.getLength(); i++) { 1533 Element child = (Element)elements.item(i); 1534 if (child.getTagName().equals(XmlUi.TAG_BUTTON)) { 1535 data.add( 1536 XmlUtil.getAttribute(child, ATTR_ACTION, (String)null) 1537 .substring(7)); 1538 } else { 1539 data.add(null); 1540 } 1541 } 1542 return data; 1543 } 1544 1545 /** 1546 * Returns the icon associated with {@code actionId}. Note that associating 1547 * the {@literal "missing icon"} icon with an action is allowable. 1548 * 1549 * @param actionId Action ID whose associated icon is to be returned. 1550 * @param style Returned icon's size will be the size associated with the 1551 * specified {@code ToolbarStyle}. 1552 * 1553 * @return Either the icon corresponding to {@code actionId} or the default 1554 * {@literal "missing icon"} icon. 1555 * 1556 * @throws NullPointerException if {@code actionId} is null. 1557 */ 1558 protected Icon getActionIcon(final String actionId, 1559 final ToolbarStyle style) 1560 { 1561 if (actionId == null) 1562 throw new NullPointerException("Action ID cannot be null"); 1563 1564 Icon actionIcon = idvActions.getStyledIconFor(actionId, style); 1565 if (actionIcon != null) 1566 return actionIcon; 1567 1568 String icon = "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/range-bearing%d.png"; 1569 URL tmp = getClass().getResource(String.format(icon, style.getSize())); 1570 return new ImageIcon(tmp); 1571 } 1572 1573 /** 1574 * Returns the known {@link IdvAction}s in the form of {@link IdvActions}. 1575 * 1576 * @return {@link #idvActions} 1577 */ 1578 public IdvActions getCachedActions() { 1579 return idvActions; 1580 } 1581 1582 /** 1583 * Returns the actions that currently make up the McIDAS-V toolbar. 1584 * 1585 * @return {@link List} of {@link ActionAttribute#ID}s that make up the 1586 * current toolbar buttons. 1587 */ 1588 public List<String> getCachedButtons() { 1589 if (cachedButtons == null) { 1590 cachedButtons = readToolbar(); 1591 } 1592 return cachedButtons; 1593 } 1594 1595 /** 1596 * Make the menu of actions. 1597 * 1598 * <p>Overridden in McIDAS-V so that we can fool the IDV into working with 1599 * our icons that allow for multiple {@literal "styles"}. 1600 * 1601 * @param obj Object to call. 1602 * @param method Method to call. 1603 * @param makeCall if {@code true}, call 1604 * {@link IntegratedDataViewer#handleAction(String)}. 1605 * 1606 * @return List of {@link JMenu}s that represent our action menus. 1607 */ 1608 @Override public List<JMenu> makeActionMenu(final Object obj, 1609 final String method, final boolean makeCall) 1610 { 1611 List<JMenu> menu = arrList(); 1612 IdvActions actions = getCachedActions(); 1613 for (String group : actions.getAllGroups()) { 1614 List<JMenuItem> items = arrList(); 1615 for (IdvAction action : actions.getActionsForGroup(group)) { 1616 String cmd = (makeCall) ? action.getCommand() : action.getId(); 1617 String desc = action.getAttribute(ActionAttribute.DESCRIPTION); 1618// items.add(GuiUtils.makeMenuItem(desc, obj, method, cmd)); 1619 items.add(makeMenuItem(desc, obj, method, cmd)); 1620 } 1621// menu.add(GuiUtils.makeMenu(group, items)); 1622 menu.add(makeMenu(group, items)); 1623 } 1624 return menu; 1625 } 1626 1627 /** 1628 * {@inheritDoc} 1629 */ 1630 public static JMenuItem makeMenuItem(String label, Object obj, 1631 String method, Object arg) 1632 { 1633 return MenuUtil.makeMenuItem(label, obj, method, arg); 1634 } 1635 1636 /** 1637 * {@inheritDoc} 1638 */ 1639 @SuppressWarnings("unchecked") 1640 public static JMenu makeMenu(String name, List menuItems) { 1641 return MenuUtil.makeMenu(name, menuItems); 1642 } 1643 1644 /** 1645 * Returns the collection of action identifiers. 1646 * 1647 * <p>Overridden in McIDAS-V so that we can fool the IDV into working with 1648 * our icons that allow for multiple {@literal "styles"}. 1649 * 1650 * @return {@link List} of {@link String}s that correspond to 1651 * {@link IdvAction IdvActions}. 1652 */ 1653 @Override public List<String> getActions() { 1654 return idvActions.getAttributes(ActionAttribute.ID); 1655 } 1656 1657 /** 1658 * Looks for the XML {@link Element} representation of the action 1659 * associated with {@code actionId}. 1660 * 1661 * <p>Overridden in McIDAS-V so that we can fool the IDV into working with 1662 * our icons that allow for multiple {@literal "styles"}. 1663 * 1664 * @param actionId ID of the action whose {@literal "action node"} is desired. Cannot be {@code null}. 1665 * 1666 * @return {@literal "action node"} associated with {@code actionId}. 1667 * 1668 * @throws NullPointerException if {@code actionId} is {@code null}. 1669 */ 1670 @Override public Element getActionNode(final String actionId) { 1671 requireNonNull(actionId, "Null action id strings are invalid"); 1672 return idvActions.getElementForAction(actionId); 1673 } 1674 1675 /** 1676 * Searches for an action identified by a given {@code actionId}, and 1677 * returns the value associated with its {@code attr}. 1678 * 1679 * <p>Overridden in McIDAS-V so that we can fool the IDV into working with 1680 * our icons that allow for multiple {@literal "styles"}. 1681 * 1682 * @param actionId ID of the action whose attribute value is desired. Cannot be {@code null}. 1683 * @param attr The attribute whose value is desired. Cannot be {@code null}. 1684 * 1685 * @return Value associated with the given action and given attribute. 1686 * 1687 * @throws NullPointerException if {@code actionId} or {@code attr} is {@code null}. 1688 */ 1689 @Override public String getActionAttr(final String actionId, 1690 final String attr) 1691 { 1692 requireNonNull(actionId, "Null action id strings are invalid"); 1693 requireNonNull(attr, "Null attributes are invalid"); 1694 ActionAttribute actionAttr = ActionAttribute.valueOf(attr.toUpperCase()); 1695 return idvActions.getAttributeForAction(stripAction(actionId), actionAttr); 1696 } 1697 1698 /** 1699 * Attempts to verify that {@code element} represents a {@literal "valid"} 1700 * IDV action. 1701 * 1702 * @param element {@link Element} to check. {@code null} values permitted, 1703 * but they return {@code false}. 1704 * 1705 * @return {@code true} if {@code element} had all required 1706 * {@link ActionAttribute}s. {@code false} otherwise, or if 1707 * {@code element} is {@code null}. 1708 */ 1709 private static boolean isValidIdvAction(final Element element) { 1710 if (element == null) { 1711 return false; 1712 } 1713 for (ActionAttribute attribute : ActionAttribute.values()) { 1714 if (!attribute.isRequired()) { 1715 continue; 1716 } 1717 if (!XmlUtil.hasAttribute(element, attribute.asIdvString())) { 1718 return false; 1719 } 1720 } 1721 return true; 1722 } 1723 1724 /** 1725 * Builds a {@link Map} of {@link ActionAttribute}s to values for a given 1726 * {@link Element}. If {@code element} does not contain an optional attribute, 1727 * use the attribute's default value. 1728 * 1729 * @param element {@literal "Action node"} of interest. {@code null} 1730 * permitted, but results in an empty {@code Map}. 1731 * 1732 * @return Mapping of {@code ActionAttribute}s to values, or an empty 1733 * {@code Map} if {@code element} is {@code null}. 1734 */ 1735 private static Map<ActionAttribute, String> actionElementToMap( 1736 final Element element) 1737 { 1738 if (element == null) { 1739 return Collections.emptyMap(); 1740 } 1741 // loop through set of action attributes; if element contains attribute "A", add it; return results. 1742 Map<ActionAttribute, String> attrs = new LinkedHashMap<>(); 1743 for (ActionAttribute attribute : ActionAttribute.values()) { 1744 String idvStr = attribute.asIdvString(); 1745 if (XmlUtil.hasAttribute(element, idvStr)) { 1746 attrs.put(attribute, XmlUtil.getAttribute(element, idvStr)); 1747 } else { 1748 attrs.put(attribute, attribute.defaultValue()); 1749 } 1750 } 1751 return attrs; 1752 } 1753 1754 /** 1755 * <p> 1756 * Builds a tree out of the bundles that should appear within the McV 1757 * toolbar. A tree is a nice way to store this data, as the default IDV 1758 * behavior is to act kinda like a file explorer when it displays these 1759 * bundles. 1760 * </p> 1761 * 1762 * <p> 1763 * The tree makes it REALLY easy to replicate the default IDV 1764 * functionality. 1765 * </p> 1766 * 1767 * @param bundleType One of {@link SavedBundle#TYPE_FAVORITE}, 1768 * {@link SavedBundle#TYPE_DISPLAY}, {@link SavedBundle#TYPE_DATA, or 1769 * {@link SavedBundle#TYPE_SYSTEM}. 1770 * 1771 * @return The root BundleTreeNode for the tree containing toolbar bundles. 1772 */ 1773 public BundleTreeNode buildBundleTree(int bundleType) { 1774 final String TOOLBAR = "Toolbar"; 1775 1776 final List<SavedBundle> bundles = 1777 getPersistenceManager().getBundles(bundleType); 1778 1779 // handy reference to parent nodes; bundle count * 4 seems pretty safe 1780 final Map<String, BundleTreeNode> mapper = new HashMap<>(bundles.size() * 4); 1781 1782 // iterate through all toolbar bundles 1783 for (SavedBundle bundle : bundles) { 1784 String categoryPath = ""; 1785 String grandParentPath; 1786 1787 // build the "path" to the bundle. these paths basically look like 1788 // "Toolbar>category>subcategory>." so "category" is a category of 1789 // toolbar bundles and subcategory is a subcategory of that. The 1790 // IDV will build nice JPopupMenus with everything that appears in 1791 // "category," so McV needs to do the same thing. thus McV needs to 1792 // figure out the complete path to each toolbar bundle! 1793 List<String> categories = (List<String>)bundle.getCategories(); 1794 if (categories == null || categories.isEmpty() || !TOOLBAR.equals(categories.get(0))) { 1795 continue; 1796 } 1797 1798 for (String category : categories) { 1799 grandParentPath = categoryPath; 1800 categoryPath += category + '>'; 1801 1802 if (!mapper.containsKey(categoryPath)) { 1803 BundleTreeNode grandParent = mapper.get(grandParentPath); 1804 BundleTreeNode parent = new BundleTreeNode(category); 1805 if (grandParent != null) { 1806 grandParent.addChild(parent); 1807 } 1808 mapper.put(categoryPath, parent); 1809 } 1810 } 1811 1812 // so the tree book-keeping (if any) is done and we can just add 1813 // the current SavedBundle to its parent node within the tree. 1814 BundleTreeNode parent = mapper.get(categoryPath); 1815 parent.addChild(new BundleTreeNode(bundle.getName(), bundle)); 1816 } 1817 1818 // return the root of the tree. 1819 return mapper.get("Toolbar>"); 1820 } 1821 1822 /** 1823 * Recursively builds the contents of the (first call) JPopupMenu. This is 1824 * where that tree annoyance stuff comes in handy. This is basically a 1825 * simple tree traversal situation. 1826 * 1827 * @param node The node that we're trying to use to build the contents. 1828 * @param comp The component to which we add node contents. 1829 */ 1830 private void buildPopupMenu(BundleTreeNode node, JComponent comp) { 1831 // if the current node has no bundle, it's considered a parent node 1832 if (node.getBundle() == null) { 1833 // parent nodes mean that we have to create a JMenu and add it 1834 JMenu test = new JMenu(node.getName()); 1835 comp.add(test); 1836 1837 // recurse through children to continue building. 1838 for (BundleTreeNode kid : node.getChildren()) { 1839 buildPopupMenu(kid, test); 1840 } 1841 1842 } else { 1843 // nodes with bundles can simply be added to the JMenu 1844 // (or JPopupMenu) 1845 JMenuItem mi = new JMenuItem(node.getName()); 1846 final SavedBundle theBundle = node.getBundle(); 1847 1848 mi.addActionListener(new ActionListener() { 1849 public void actionPerformed(ActionEvent ae) { 1850 //Do it in a thread 1851 Misc.run(UIManager.this, "processBundle", theBundle); 1852 } 1853 }); 1854 1855 comp.add(mi); 1856 } 1857 } 1858 1859 @Override public void initDone() { 1860 super.initDone(); 1861 // not super excited about how this works. 1862 // showBasicWindow(true); 1863 1864 if (getStore().get(Constants.PREF_VERSION_CHECK, true)) { 1865 StateManager stateManager = (StateManager)getStateManager(); 1866 stateManager.checkForNewerVersion(false); 1867 stateManager.checkForNotice(false); 1868 } 1869 1870 initDone = true; 1871 showDashboard(); 1872 } 1873 1874 /** 1875 * Create the splash screen if needed 1876 */ 1877 public void initSplash() { 1878 if (getProperty(PROP_SHOWSPLASH, true) 1879 && !getArgsManager().getNoGui() 1880 && !getArgsManager().getIsOffScreen() 1881 && !getArgsManager().testMode) { 1882 splash = new McvSplash(idv); 1883 splashMsg("Loading Programs"); 1884 } 1885 } 1886 1887 /** 1888 * Create (if null) and show the HelpTipDialog. If checkPrefs is true 1889 * then only create the dialog if the PREF_HELPTIPSHOW preference is true. 1890 * 1891 * @param checkPrefs Should the user preferences be checked 1892 */ 1893 /** THe help tip dialog */ 1894 private McvHelpTipDialog helpTipDialog; 1895 1896 public void initHelpTips(boolean checkPrefs) { 1897 try { 1898 if (getIdv().getArgsManager().getIsOffScreen()) { 1899 return; 1900 } 1901 if (checkPrefs) { 1902 if (!getStore().get(McvHelpTipDialog.PREF_HELPTIPSHOW, true)) { 1903 return; 1904 } 1905 } 1906 if (helpTipDialog == null) { 1907 IdvResourceManager resourceManager = getResourceManager(); 1908 helpTipDialog = new McvHelpTipDialog( 1909 resourceManager.getXmlResources( 1910 resourceManager.RSC_HELPTIPS), getIdv(), getStore(), 1911 getIdvClass(), 1912 getStore().get( 1913 McvHelpTipDialog.PREF_HELPTIPSHOW, true)); 1914 } 1915 helpTipDialog.setVisible(true); 1916 GuiUtils.toFront(helpTipDialog); 1917 } catch (Throwable excp) { 1918 logException("Reading help tips", excp); 1919 } 1920 } 1921 1922 /** 1923 * If created, close the HelpTipDialog window. 1924 */ 1925 public void closeHelpTips() { 1926 if (helpTipDialog != null) { 1927 helpTipDialog.setVisible(false); 1928 } 1929 } 1930 1931 /** 1932 * Create (if null) and show the HelpTipDialog 1933 */ 1934 public void showHelpTips() { 1935 initHelpTips(false); 1936 } 1937 1938 /** 1939 * Populate a menu with bundles known to the {@code PersistenceManager}. 1940 * 1941 * @param inBundleMenu The menu to populate 1942 */ 1943 public void makeBundleMenu(JMenu inBundleMenu) { 1944 final int bundleType = IdvPersistenceManager.BUNDLES_FAVORITES; 1945 1946 JMenuItem mi; 1947 mi = new JMenuItem("Manage..."); 1948 McVGuiUtils.setMenuImage(mi, Constants.ICON_FAVORITEMANAGE_SMALL); 1949 mi.setMnemonic(GuiUtils.charToKeyCode("M")); 1950 inBundleMenu.add(mi); 1951 mi.addActionListener(ae -> showBundleDialog(bundleType)); 1952 1953 final List bundles = getPersistenceManager().getBundles(bundleType); 1954 if (bundles.isEmpty()) { 1955 return; 1956 } 1957 final String title = 1958 getPersistenceManager().getBundleTitle(bundleType); 1959 final String bundleDir = 1960 getPersistenceManager().getBundleDirectory(bundleType); 1961 1962 JMenu bundleMenu = new JMenu(title); 1963 McVGuiUtils.setMenuImage(bundleMenu, Constants.ICON_FAVORITE_SMALL); 1964 bundleMenu.setMnemonic(GuiUtils.charToKeyCode(title)); 1965 1966// getPersistenceManager().initBundleMenu(bundleType, bundleMenu); 1967 1968 Hashtable catMenus = new Hashtable(); 1969 inBundleMenu.addSeparator(); 1970 inBundleMenu.add(bundleMenu); 1971 for (int i = 0; i < bundles.size(); i++) { 1972 SavedBundle bundle = (SavedBundle) bundles.get(i); 1973 List categories = bundle.getCategories(); 1974 JMenu catMenu = bundleMenu; 1975 String mainCategory = ""; 1976 for (int catIdx = 0; catIdx < categories.size(); catIdx++) { 1977 String category = (String) categories.get(catIdx); 1978 mainCategory += "." + category; 1979 JMenu tmpMenu = (JMenu) catMenus.get(mainCategory); 1980 if (tmpMenu == null) { 1981 tmpMenu = new JMenu(category); 1982 catMenu.add(tmpMenu); 1983 catMenus.put(mainCategory, tmpMenu); 1984 } 1985 catMenu = tmpMenu; 1986 } 1987 1988 final SavedBundle theBundle = bundle; 1989 mi = new JMenuItem(bundle.getName()); 1990 mi.addActionListener(new ActionListener() { 1991 public void actionPerformed(ActionEvent ae) { 1992 //Do it in a thread 1993 Misc.run(UIManager.this, "processBundle", theBundle); 1994 } 1995 }); 1996 catMenu.add(mi); 1997 } 1998 } 1999 2000 /** 2001 * Overridden to build a custom Window menu. 2002 * @see ucar.unidata.idv.ui.IdvUIManager#makeWindowsMenu(JMenu, IdvWindow) 2003 */ 2004 @Override public void makeWindowsMenu(final JMenu windowMenu, final IdvWindow idvWindow) { 2005 JMenuItem mi; 2006 boolean first = true; 2007 2008 mi = new JMenuItem("Show Data Explorer"); 2009 McVGuiUtils.setMenuImage(mi, Constants.ICON_DATAEXPLORER_SMALL); 2010 mi.addActionListener(this); 2011 mi.setActionCommand(ACT_SHOW_DASHBOARD); 2012 windowMenu.add(mi); 2013 2014 makeTabNavigationMenu(windowMenu); 2015 2016 @SuppressWarnings("unchecked") // it's how the IDV does it. 2017 List windows = new ArrayList(IdvWindow.getWindows()); 2018 for (int i = 0; i < windows.size(); i++) { 2019 final IdvWindow window = (IdvWindow)windows.get(i); 2020 2021 // Skip the main window 2022 if (window.getIsAMainWindow()) { 2023 continue; 2024 } 2025 2026 String title = window.getTitle(); 2027 String titleParts[] = splitTitle(title); 2028 2029 if (titleParts.length == 2) { 2030 title = titleParts[1]; 2031 } 2032 2033 // Skip the data explorer and display controller 2034 String dataSelectorNameParts[] = splitTitle(Constants.DATASELECTOR_NAME); 2035 if (title.equals(Constants.DATASELECTOR_NAME) || title.equals(dataSelectorNameParts[1])) { 2036 continue; 2037 } 2038 2039 // Add a meaningful name if there is none 2040 if (title.isEmpty()) { 2041 title = "<Unnamed>"; 2042 } 2043 2044 if (window.isVisible()) { 2045 mi = new JMenuItem(title); 2046 mi.addActionListener(ae -> window.toFront()); 2047 2048 if (first) { 2049 windowMenu.addSeparator(); 2050 first = false; 2051 } 2052 2053 windowMenu.add(mi); 2054 } 2055 } 2056 Msg.translateTree(windowMenu); 2057 } 2058 2059 /** 2060 * Add tab navigation {@link JMenuItem JMenuItems} to the given 2061 * {@code menu}. 2062 * 2063 * @param menu Menu to which tab navigation menu items should be added. 2064 * Cannot be {@code null}. 2065 */ 2066 private void makeTabNavigationMenu(final JMenu menu) { 2067 if (!didInitActions) { 2068 didInitActions = true; 2069 initTabNavActions(); 2070 } 2071 2072 if (McVGuiUtils.getAllComponentHolders().size() <= 1) { 2073 return; 2074 } 2075 2076 menu.addSeparator(); 2077 2078 menu.add(new JMenuItem(nextDisplayAction)); 2079 menu.add(new JMenuItem(prevDisplayAction)); 2080 menu.add(new JMenuItem(showDisplayAction)); 2081 2082 if (!McVGuiUtils.getAllComponentGroups().isEmpty()) { 2083 menu.addSeparator(); 2084 } 2085 2086 Msg.translateTree(menu); 2087 } 2088 2089 /** 2090 * Add in the dynamic menu for displaying formulas 2091 * 2092 * @param menu edit menu to add to 2093 */ 2094 public void makeFormulasMenu(JMenu menu) { 2095 MenuUtil.makeMenu(menu, getJythonManager().doMakeFormulaDataSourceMenuItems(null)); 2096 } 2097 2098 /** Whether or not the list of available actions has been initialized. */ 2099 private boolean didInitActions = false; 2100 2101 /** Key combo for the popup with list of displays. */ 2102 private ShowDisplayAction showDisplayAction; 2103 2104 /** 2105 * Key combo for moving to the previous display relative to the current. For 2106 * key combos the lists of displays in the current window is circular. 2107 */ 2108 private PrevDisplayAction prevDisplayAction; 2109 2110 /** 2111 * Key combo for moving to the next display relative to the current. For 2112 * key combos the lists of displays in the current window is circular. 2113 */ 2114 private NextDisplayAction nextDisplayAction; 2115 2116 /** Modifier key, like {@literal "control"} or {@literal "shift"}. */ 2117 private static final String PROP_KB_MODIFIER = "mcidasv.tabbedui.display.kbmodifier"; 2118 2119 /** Key that pops up the list of displays. Used in conjunction with {@code PROP_KB_MODIFIER}. */ 2120 private static final String PROP_KB_SELECT_DISPLAY = "mcidasv.tabbedui.display.kbselect"; 2121 2122 /** Key for moving to the previous display. Used in conjunction with {@code PROP_KB_MODIFIER}. */ 2123 private static final String PROP_KB_DISPLAY_PREV = "mcidasv.tabbedui.display.kbprev"; 2124 2125 /** Key for moving to the next display. Used in conjunction with {@code PROP_KB_MODIFIER}. */ 2126 private static final String PROP_KB_DISPLAY_NEXT = "mcidasv.tabbedui.display.kbnext"; 2127 2128 /** Key for showing the dashboard. Used in conjunction with {@code PROP_KB_MODIFIER}. */ 2129 private static final String PROP_KB_SHOW_DASHBOARD = "mcidasv.tabbedui.display.kbdashboard"; 2130 2131 /** Key for showing the Jython Shell. Used in conjunction with {@code PROP_KB_MODIFIER}. */ 2132 private static final String PROP_KB_SHOW_SHELL = "mcidasv.tabbedui.display.kjythonshell"; 2133 2134 /** Key for showing the Jython Library (modifier is {@literal "alt"} key). */ 2135 private static final String PROP_KB_SHOW_LIBRARY = "mcidasv.tabbedui.display.kjythonlibrary"; 2136 2137 // TODO: make all this stuff static: mod + acc don't need to read the properties file. 2138 // look at: http://community.livejournal.com/jkff_en/341.html 2139 // look at: effective java, particularly the stuff about enums 2140 private void initTabNavActions() { 2141 String mod = idv.getProperty(PROP_KB_MODIFIER, "control") + " "; 2142 String acc = idv.getProperty(PROP_KB_SELECT_DISPLAY, "L"); 2143 2144 String stroke = mod + acc; 2145 showDisplayAction = new ShowDisplayAction(KeyStroke.getKeyStroke(stroke)); 2146 2147 acc = idv.getProperty(PROP_KB_DISPLAY_PREV, "P"); 2148 stroke = mod + acc; 2149 prevDisplayAction = new PrevDisplayAction(KeyStroke.getKeyStroke(stroke)); 2150 2151 acc = idv.getProperty(PROP_KB_DISPLAY_NEXT, "N"); 2152 stroke = mod + acc; 2153 nextDisplayAction = new NextDisplayAction(KeyStroke.getKeyStroke(stroke)); 2154 } 2155 2156 /** 2157 * Add all the show window keyboard shortcuts. To make keyboard shortcuts 2158 * global, i.e., available no matter what window is active, the appropriate 2159 * actions have to be added the the window contents action and input maps. 2160 * 2161 * FIXME: This can't be the right way to do this! 2162 * 2163 * @param window IdvWindow that requires keyboard shortcut capability. 2164 */ 2165 private void initDisplayShortcuts(IdvWindow window) { 2166 //mjh aug2014 make sure showDisplayAction etc. are initialized: 2167 initTabNavActions(); 2168 didInitActions = true; 2169 2170 JComponent jcomp = window.getContents(); 2171 jcomp.getActionMap().put("show_disp", showDisplayAction); 2172 jcomp.getActionMap().put("prev_disp", prevDisplayAction); 2173 jcomp.getActionMap().put("next_disp", nextDisplayAction); 2174 jcomp.getActionMap().put("show_dashboard", new AbstractAction() { 2175 private static final long serialVersionUID = -364947940824325949L; 2176 public void actionPerformed(ActionEvent evt) { 2177 showDashboard(); 2178 } 2179 }); 2180 jcomp.getActionMap().put("show_jython_shell", new AbstractAction() { 2181 @Override public void actionPerformed(ActionEvent e) { 2182 getJythonManager().createShell(); 2183 } 2184 }); 2185 jcomp.getActionMap().put("show_jython_library", new AbstractAction() { 2186 @Override public void actionPerformed(ActionEvent e) { 2187 getJythonManager().showJythonEditor(); 2188 } 2189 }); 2190 2191 String mod = getIdv().getProperty(PROP_KB_MODIFIER, "control"); 2192 String acc = getIdv().getProperty(PROP_KB_SELECT_DISPLAY, "L"); 2193 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2194 KeyStroke.getKeyStroke(mod + ' ' + acc), 2195 "show_disp" 2196 ); 2197 2198 acc = getIdv().getProperty(PROP_KB_SHOW_DASHBOARD, "MINUS"); 2199 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2200 KeyStroke.getKeyStroke(mod + ' ' + acc), 2201 "show_dashboard" 2202 ); 2203 2204 acc = getIdv().getProperty(PROP_KB_DISPLAY_NEXT, "N"); 2205 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2206 KeyStroke.getKeyStroke(mod + ' ' + acc), 2207 "next_disp" 2208 ); 2209 2210 acc = getIdv().getProperty(PROP_KB_DISPLAY_PREV, "P"); 2211 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2212 KeyStroke.getKeyStroke(mod + ' ' + acc), 2213 "prev_disp" 2214 ); 2215 2216 acc = getIdv().getProperty(PROP_KB_SHOW_SHELL, "J"); 2217 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2218 KeyStroke.getKeyStroke(mod + ' ' + acc), 2219 "show_jython_shell"); 2220 2221 mod = "alt"; 2222 acc = getIdv().getProperty(PROP_KB_SHOW_LIBRARY, "J"); 2223 jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 2224 KeyStroke.getKeyStroke(mod + ' ' + acc), 2225 "show_jython_library"); 2226 } 2227 2228 /** 2229 * Show Bruce's display selector widget. 2230 */ 2231 protected void showDisplaySelector() { 2232 IdvWindow mainWindow = IdvWindow.getActiveWindow(); 2233 JPanel contents = new JPanel(); 2234 contents.setLayout(new BorderLayout()); 2235 JComponent comp = getDisplaySelectorComponent(); 2236 final JDialog dialog = new JDialog(mainWindow.getFrame(), "List Displays", true); 2237 dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 2238 contents.add(comp, BorderLayout.CENTER); 2239 JButton button = new JButton("OK"); 2240 button.addActionListener(new ActionListener() { 2241 public void actionPerformed(ActionEvent evt) { 2242 final ViewManager vm = getVMManager().getLastActiveViewManager(); 2243 // final DisplayProps disp = getDisplayProps(vm); 2244 // if (disp != null) 2245 // showDisplay(disp); 2246 final McvComponentHolder holder = (McvComponentHolder)getViewManagerHolder(vm); 2247 if (holder != null) { 2248 holder.setAsActiveTab(); 2249 } 2250 2251 // have to do this on the event dispatch thread so we make 2252 // sure it happens after showDisplay 2253 SwingUtilities.invokeLater(new Runnable() { 2254 public void run() { 2255 //setActiveDisplay(disp, disp.managers.indexOf(vm)); 2256 if (holder != null) { 2257 getVMManager().setLastActiveViewManager(vm); 2258 } 2259 } 2260 }); 2261 2262 dialog.dispose(); 2263 } 2264 }); 2265 JPanel buttonPanel = new JPanel(); 2266 buttonPanel.add(button); 2267 dialog.add(buttonPanel, BorderLayout.AFTER_LAST_LINE); 2268 JScrollPane scroller = new JScrollPane(contents); 2269 scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); 2270 scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 2271 dialog.add(scroller, BorderLayout.CENTER); 2272 dialog.setSize(200, 300); 2273 dialog.setLocationRelativeTo(mainWindow.getFrame()); 2274 dialog.setVisible(true); 2275 } 2276 2277 private class ShowDisplayAction extends AbstractAction { 2278 private static final long serialVersionUID = -4609753725057124244L; 2279 private static final String ACTION_NAME = "List Displays..."; 2280 public ShowDisplayAction(KeyStroke k) { 2281 super(ACTION_NAME); 2282 putValue(Action.ACCELERATOR_KEY, k); 2283 } 2284 2285 public void actionPerformed(ActionEvent e) { 2286 showDisplaySelector(); 2287 } 2288 } 2289 2290 private class PrevDisplayAction extends AbstractAction { 2291 private static final long serialVersionUID = -3551890663976755671L; 2292 private static final String ACTION_NAME = "Previous Display"; 2293 2294 public PrevDisplayAction(KeyStroke k) { 2295 super(ACTION_NAME); 2296 putValue(Action.ACCELERATOR_KEY, k); 2297 } 2298 2299 public void actionPerformed(ActionEvent e) { 2300 McvComponentHolder prev = (McvComponentHolder)McVGuiUtils.getBeforeActiveHolder(); 2301 if (prev != null) { 2302 prev.setAsActiveTab(); 2303 } 2304 } 2305 } 2306 2307 private class NextDisplayAction extends AbstractAction { 2308 private static final long serialVersionUID = 5431901451767117558L; 2309 private static final String ACTION_NAME = "Next Display"; 2310 2311 public NextDisplayAction(KeyStroke k) { 2312 super(ACTION_NAME); 2313 putValue(Action.ACCELERATOR_KEY, k); 2314 } 2315 2316 public void actionPerformed(ActionEvent e) { 2317 McvComponentHolder next = (McvComponentHolder)McVGuiUtils.getAfterActiveHolder(); 2318 if (next != null) { 2319 next.setAsActiveTab(); 2320 } 2321 } 2322 } 2323 2324 /** 2325 * Populate a "new display" menu from the available skin list. Many thanks 2326 * to Bruce for doing this in the venerable TabbedUIManager. 2327 * 2328 * @param newDisplayMenu menu to populate. 2329 * @param inWindow Is the skinned display to be created in a window? 2330 * 2331 * @see IdvResourceManager#RSC_SKIN 2332 * 2333 * @return Menu item populated with display skins 2334 */ 2335 protected JMenuItem doMakeNewDisplayMenu(JMenuItem newDisplayMenu, 2336 final boolean inWindow) 2337 { 2338 if (newDisplayMenu != null) { 2339 2340 String skinFilter = "idv.skin"; 2341 if (!inWindow) { 2342 skinFilter = "mcv.skin"; 2343 } 2344 2345 final XmlResourceCollection skins = 2346 getResourceManager().getXmlResources( 2347 IdvResourceManager.RSC_SKIN); 2348 2349 Map<String, JMenu> menus = new Hashtable<>(); 2350 for (int i = 0; i < skins.size(); i++) { 2351 final Element root = skins.getRoot(i); 2352 if (root == null) { 2353 continue; 2354 } 2355 2356 // filter out mcv or idv skins based on whether or not we're 2357 // interested in tabs or new windows. 2358 final String skinid = skins.getProperty("skinid", i); 2359 if ((skinid != null) && skinid.startsWith(skinFilter)) { 2360 continue; 2361 } 2362 2363 final int skinIndex = i; 2364 List<String> names = 2365 StringUtil.split(skins.getShortName(i), ">", true, true); 2366 2367 JMenuItem theMenu = newDisplayMenu; 2368 String path = ""; 2369 for (int nameIdx = 0; nameIdx < names.size() - 1; nameIdx++) { 2370 String catName = names.get(nameIdx); 2371 path = path + '>' + catName; 2372 JMenu tmpMenu = menus.get(path); 2373 if (tmpMenu == null) { 2374 tmpMenu = new JMenu(catName); 2375 theMenu.add(tmpMenu); 2376 menus.put(path, tmpMenu); 2377 } 2378 theMenu = tmpMenu; 2379 } 2380 2381 final String name = names.get(names.size() - 1); 2382 2383 IdvWindow window = IdvWindow.getActiveWindow(); 2384 for (final McvComponentGroup group : McVGuiUtils.idvGroupsToMcv(window)) { 2385 JMenuItem mi = new JMenuItem(name); 2386 2387 mi.addActionListener(ae -> { 2388 if (!inWindow) { 2389 createNewTab(skinid); 2390 } else { 2391 createNewWindow(null, true, 2392 getStateManager().getTitle(), skins.get( 2393 skinIndex).toString(), skins.getRoot( 2394 skinIndex, false), inWindow, null); 2395 } 2396 }); 2397 theMenu.add(mi); 2398 } 2399 } 2400 2401 // attach the dynamic skin menu item to the tab menu. 2402// if (!inWindow) { 2403// ((JMenu)newDisplayMenu).addSeparator(); 2404// IdvWindow window = IdvWindow.getActiveWindow(); 2405// 2406// final McvComponentGroup group = 2407// (McvComponentGroup)window.getComponentGroups().get(0); 2408// 2409// JMenuItem mi = new JMenuItem("Choose Your Own Adventure..."); 2410// mi.addActionListener(new ActionListener() { 2411// 2412// public void actionPerformed(ActionEvent e) { 2413// makeDynamicSkin(group); 2414// } 2415// }); 2416// newDisplayMenu.add(mi); 2417// } 2418 } 2419 return newDisplayMenu; 2420 } 2421 2422 // for the time being just create some basic viewmanagers. 2423// public void makeDynamicSkin(McvComponentGroup group) { 2424// // so I have my megastring (which I hate--a class that can generate XML would be cooler) (though it would boil down to the same thing...) 2425// try { 2426// Document doc = XmlUtil.getDocument(SKIN_TEMPLATE); 2427// Element root = doc.getDocumentElement(); 2428// Element rightChild = doc.createElement("idv.view"); 2429// rightChild.setAttribute("class", "ucar.unidata.idv.TransectViewManager"); 2430// rightChild.setAttribute("viewid", "viewright1337"); 2431// rightChild.setAttribute("id", "viewright"); 2432// rightChild.setAttribute("properties", "name=Panel 1;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;initialSplitPaneLocation=0.2;legendOnLeft=true;size=300:400;shareGroup=view%versionuid%;"); 2433// 2434// Element leftChild = doc.createElement("idv.view"); 2435// leftChild.setAttribute("class", "ucar.unidata.idv.MapViewManager"); 2436// leftChild.setAttribute("viewid", "viewleft1337"); 2437// leftChild.setAttribute("id", "viewleft"); 2438// leftChild.setAttribute("properties", "name=Panel 2;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;size=300:400;shareGroup=view%versionuid%;"); 2439// 2440// Element startNode = XmlUtil.findElement(root, "splitpane", "embeddednode", "true"); 2441// startNode.appendChild(rightChild); 2442// startNode.appendChild(leftChild); 2443// group.makeDynamicSkin(root); 2444// } catch (Exception e) { 2445// LogUtil.logException("Error: parsing skin template:", e); 2446// } 2447// } 2448// 2449// private static final String SKIN_TEMPLATE = 2450// "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 2451// "<skin embedded=\"true\">\n" + 2452// " <ui>\n" + 2453// " <panel layout=\"border\" bgcolor=\"red\">\n" + 2454// " <idv.menubar place=\"North\"/>\n" + 2455// " <panel layout=\"border\" place=\"Center\">\n" + 2456// " <panel layout=\"flow\" place=\"North\">\n" + 2457// " <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" + 2458// " <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" + 2459// " </panel>\n" + 2460// " <splitpane embeddednode=\"true\" resizeweight=\"0.5\" onetouchexpandable=\"true\" orientation=\"h\" bgcolor=\"blue\" layout=\"grid\" cols=\"2\" place=\"Center\">\n" + 2461// " </splitpane>\n" + 2462// " </panel>\n" + 2463// " <component idref=\"bottom_bar\"/>\n" + 2464// " </panel>\n" + 2465// " </ui>\n" + 2466// " <styles>\n" + 2467// " <style class=\"iconbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip);ui.setBorder(this,etched);\" mouse_exit=\"ui.setText(idv.messagelabel,);ui.setBorder(this,button);\"/>\n" + 2468// " <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" + 2469// " </styles>\n" + 2470// " <components>\n" + 2471// " <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" + 2472// " </components>\n" + 2473// " <properties>\n" + 2474// " <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" + 2475// " </properties>\n" + 2476// "</skin>\n"; 2477 2478 private int holderCount; 2479 2480 /** 2481 * Associates a given ViewManager with a given ComponentHolder. 2482 * 2483 * @param vm The ViewManager that is inside {@code holder}. 2484 * @param holder The ComponentHolder that contains {@code vm}. 2485 */ 2486 public void setViewManagerHolder(ViewManager vm, ComponentHolder holder) { 2487 viewManagers.put(vm, holder); 2488 holderCount = getComponentHolders().size(); 2489 } 2490 2491 public Set<ComponentHolder> getComponentHolders() { 2492 return newHashSet(viewManagers.values()); 2493 } 2494 2495 public int getComponentHolderCount() { 2496 return holderCount; 2497 } 2498 2499 public int getComponentGroupCount() { 2500 return getComponentGroups().size(); 2501 } 2502 2503 /** 2504 * Returns the ComponentHolder containing the given ViewManager. 2505 * 2506 * @param vm The ViewManager whose ComponentHolder is needed. 2507 * 2508 * @return Either {@code null} or the {@code ComponentHolder}. 2509 */ 2510 public ComponentHolder getViewManagerHolder(ViewManager vm) { 2511 return viewManagers.get(vm); 2512 } 2513 2514 /** 2515 * Disassociate a given {@code ViewManager} from its 2516 * {@code ComponentHolder}. 2517 * 2518 * @param vm {@code ViewManager} to disassociate. 2519 * 2520 * @return The associated {@code ComponentHolder}. 2521 */ 2522 public ComponentHolder removeViewManagerHolder(ViewManager vm) { 2523 ComponentHolder holder = viewManagers.remove(vm); 2524 holderCount = getComponentHolders().size(); 2525 return holder; 2526 } 2527 2528 /** 2529 * Overridden to keep the dashboard around after it's initially created. 2530 * Also give the user the ability to show a particular tab. 2531 * 2532 * @see ucar.unidata.idv.ui.IdvUIManager#showDashboard() 2533 */ 2534 @Override public void showDashboard() { 2535 showDashboard(""); 2536 } 2537 2538 /** 2539 * Creates the {@link McIDASVViewPanel} component that shows up in the 2540 * dashboard. 2541 * 2542 * @return McIDAS-V specific view panel. 2543 */ 2544 @Override protected ViewPanel doMakeViewPanel() { 2545 ViewPanel vp = new McIDASVViewPanel(idv); 2546 vp.getContents(); 2547 return vp; 2548 } 2549 2550 /** 2551 * Build a mapping of {@literal "skin"} IDs to their indicies within skin 2552 * resources. 2553 * 2554 * @return Map of skin ids to their index within the skin resource. 2555 */ 2556 private Map<String, Integer> readSkinIds() { 2557 XmlResourceCollection skins = 2558 getResourceManager().getXmlResources(IdvResourceManager.RSC_SKIN); 2559 Map<String, Integer> ids = new HashMap<>(skins.size()); 2560 for (int i = 0; i < skins.size(); i++) { 2561 String id = skins.getProperty("skinid", i); 2562 if (id != null) { 2563 ids.put(id, i); 2564 } 2565 } 2566 return ids; 2567 } 2568 2569 /** 2570 * Adds a skinned component holder to the active component group. 2571 * 2572 * @param skinId The value of the skin's skinid attribute. 2573 */ 2574 public void createNewTab(final String skinId) { 2575 IdvWindow activeWindow = IdvWindow.getActiveWindow(); 2576 IdvComponentGroup group = 2577 McVGuiUtils.getComponentGroup(activeWindow); 2578 if (skinIds.containsKey(skinId)) { 2579 group.makeSkin(skinIds.get(skinId)); 2580 } 2581 JFrame frame = activeWindow.getFrame(); 2582 if (frame != null) { 2583 frame.setPreferredSize(frame.getSize()); 2584 } 2585 } 2586 2587 /** 2588 * Method to do the work of showing the Data Explorer (nee Dashboard). 2589 * 2590 * @param tabName Name of the tab that should be made active. 2591 * Cannot be {@code null}, but empty {@code String} values 2592 * will not change the active tab. 2593 */ 2594 @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows. 2595 public void showDashboard(String tabName) { 2596 if (!initDone) { 2597 return; 2598 } else if (dashboard == null) { 2599 showWaitCursor(); 2600 doMakeBasicWindows(); 2601 showNormalCursor(); 2602 String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME); 2603 for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) { 2604 if (title.equals(window.getTitle())) { 2605 dashboard = window; 2606 dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 2607 } 2608 } 2609 } else { 2610 dashboard.show(); 2611 } 2612 2613 if (tabName.isEmpty()) { 2614 return; 2615 } 2616 2617 // Dig two panels deep looking for a JTabbedPane 2618 // If you find one, try to show the requested tab name 2619 JComponent contents = dashboard.getContents(); 2620 JComponent component = (JComponent)contents.getComponent(0); 2621 JTabbedPane tPane = null; 2622 if (component instanceof JTabbedPane) { 2623 tPane = (JTabbedPane)component; 2624 } 2625 else { 2626 JComponent component2 = (JComponent)component.getComponent(0); 2627 if (component2 instanceof JTabbedPane) { 2628 tPane = (JTabbedPane)component2; 2629 } 2630 } 2631 if (tPane != null) { 2632 for (int i=0; i<tPane.getTabCount(); i++) { 2633 if (tabName.equals(tPane.getTitleAt(i))) { 2634 tPane.setSelectedIndex(i); 2635 break; 2636 } 2637 } 2638 } 2639 } 2640 2641 /** 2642 * Show the support request form 2643 * 2644 * @param description Default value for the description form entry 2645 * @param stackTrace The stack trace that caused this error. 2646 * @param dialog The dialog to put the gui in, if non-null. 2647 */ 2648 public void showSupportForm(final String description, 2649 final String stackTrace, final JDialog dialog) 2650 { 2651 java.awt.EventQueue.invokeLater(() -> { 2652 // TODO: mcvstatecollector should have a way to gather the 2653 // exception information.. 2654 McIDASV mcv = (McIDASV)getIdv(); 2655 new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true); 2656 }); 2657 } 2658 2659 /** 2660 * Attempts to locate and display a dashboard component using an ID. 2661 * 2662 * @param id ID of the desired component. 2663 * 2664 * @return True if {@code id} corresponds to a component. False otherwise. 2665 */ 2666 public boolean showDashboardComponent(String id) { 2667 Object comp = findComponent(id); 2668 if (comp != null) { 2669 GuiUtils.showComponentInTabs((JComponent)comp); 2670 return true; 2671 } else { 2672 super.showDashboard(); 2673 for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) { 2674 String title = makeTitle( 2675 getStateManager().getTitle(), 2676 Constants.DATASELECTOR_NAME 2677 ); 2678 if (title.equals(window.getTitle())) { 2679 dashboard = window; 2680 dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 2681 } 2682 } 2683 } 2684 return false; 2685 } 2686 2687 /** 2688 * Close and dispose of the splash window (if it has been created). 2689 */ 2690 @Override 2691 public void splashClose() { 2692 if (splash != null) { 2693 splash.doClose(); 2694 } 2695 } 2696 2697 /** 2698 * Show a message in the splash screen (if it exists) 2699 * 2700 * @param m The message to show 2701 */ 2702 @Override public void splashMsg(String m) { 2703 if (splash != null) { 2704 splash.splashMsg(m); 2705 } 2706 } 2707 2708 /** 2709 * Uses a given toolbar editor to repopulate all toolbars so that they 2710 * correspond to the user's choice of actions. 2711 * 2712 * @param tbe The toolbar editor that contains the actions the user wants. 2713 */ 2714 public void setCurrentToolbars(final McvToolbarEditor tbe) { 2715 List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries(); 2716 List<String> buttonIds = new ArrayList<>(tfos.size()); 2717 for (TwoFacedObject tfo : tfos) { 2718 if (McvToolbarEditor.isSpace(tfo)) { 2719 buttonIds.add(null); 2720 } else { 2721 buttonIds.add(TwoFacedObject.getIdString(tfo)); 2722 } 2723 } 2724 2725 cachedButtons = buttonIds; 2726 2727 for (JToolBar toolbar : toolbars) { 2728 toolbar.setVisible(false); 2729 populateToolbar(toolbar); 2730 toolbar.setVisible(true); 2731 } 2732 } 2733 2734 /** 2735 * Append a string and object to the buffer 2736 * 2737 * @param sb StringBuffer to append to 2738 * @param name Name of the object 2739 * @param value the object value 2740 */ 2741 private void append(StringBuffer sb, String name, Object value) { 2742 sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>"); 2743 } 2744 2745 private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) { 2746 JMenuItem mi = new JMenuItem(); 2747 if (cd != null) { 2748 mi = new JMenuItem(cd.getLabel()); 2749 mi.addActionListener(new ObjectListener(cd) { 2750 public void actionPerformed(ActionEvent ev) { 2751 idv.doMakeControl(new ArrayList(), 2752 (ControlDescriptor)theObject); 2753 } 2754 }); 2755 } 2756 return mi; 2757 } 2758 2759 /* (non-javadoc) 2760 * Overridden so that the toolbar will update upon saving a bundle. 2761 */ 2762 @Override public void displayTemplatesChanged() { 2763 super.displayTemplatesChanged(); 2764 for (JToolBar toolbar : toolbars) { 2765 toolbar.setVisible(false); 2766 populateToolbar(toolbar); 2767 toolbar.setVisible(true); 2768 } 2769 } 2770 2771 /** 2772 * Called when there has been any change to the favorite bundles and is 2773 * most useful for triggering an update to the {@literal "toolbar bundles"}. 2774 */ 2775 @Override public void favoriteBundlesChanged() { 2776 SwingUtilities.invokeLater(() -> { 2777 for (JToolBar toolbar : toolbars) { 2778 toolbar.setVisible(false); 2779 populateToolbar(toolbar); 2780 toolbar.setVisible(true); 2781 } 2782 }); 2783 } 2784 2785 /** 2786 * Show the support request form in a non-swing thread. We do this because we cannot 2787 * call the HttpFormEntry.showUI from a swing thread 2788 * 2789 * @param description Default value for the description form entry 2790 * @param stackTrace The stack trace that caused this error. 2791 * @param dialog The dialog to put the gui in, if non-null. 2792 */ 2793 2794 private void showSupportFormInThread(String description, 2795 String stackTrace, JDialog dialog) { 2796 List<HttpFormEntry> entries = new ArrayList<>(); 2797 2798 StringBuffer extra = new StringBuffer("<h3>McIDAS-V</h3>\n"); 2799 Hashtable<String, String> table = 2800 ((StateManager)getStateManager()).getVersionInfo(); 2801 append(extra, "mcv.version.general", table.get("mcv.version.general")); 2802 append(extra, "mcv.version.build", table.get("mcv.version.build")); 2803 append(extra, "idv.version.general", table.get("idv.version.general")); 2804 append(extra, "idv.version.build", table.get("idv.version.build")); 2805 2806 extra.append("<h3>OS</h3>\n"); 2807 append(extra, "os.name", System.getProperty("os.name")); 2808 append(extra, "os.arch", System.getProperty("os.arch")); 2809 append(extra, "os.version", System.getProperty("os.version")); 2810 2811 extra.append("<h3>Java</h3>\n"); 2812 append(extra, "java.vendor", System.getProperty("java.vendor")); 2813 append(extra, "java.version", System.getProperty("java.version")); 2814 append(extra, "java.home", System.getProperty("java.home")); 2815 2816 StringBuffer javaInfo = new StringBuffer(); 2817 javaInfo.append("Java: home: " + System.getProperty("java.home")); 2818 javaInfo.append(" version: " + System.getProperty("java.version")); 2819 2820 Class c = null; 2821 try { 2822 c = Class.forName("javax.media.j3d.VirtualUniverse"); 2823 Method method = Misc.findMethod(c, "getProperties", 2824 new Class[] {}); 2825 if (method == null) { 2826 javaInfo.append("j3d <1.3"); 2827 } else { 2828 try { 2829 Map m = (Map)method.invoke(c, new Object[] {}); 2830 javaInfo.append(" j3d:" + m.get("j3d.version")); 2831 append(extra, "j3d.version", m.get("j3d.version")); 2832 append(extra, "j3d.vendor", m.get("j3d.vendor")); 2833 append(extra, "j3d.renderer", m.get("j3d.renderer")); 2834 } catch (Exception exc) { 2835 javaInfo.append(" j3d:" + "unknown"); 2836 } 2837 } 2838 } catch (ClassNotFoundException exc) { 2839 append(extra, "j3d", "none"); 2840 } 2841 2842 boolean persistCC = getStore().get("mcv.supportreq.cc", true); 2843 2844 JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC); 2845 ccMyself.addActionListener(e -> { 2846 JCheckBox cb = (JCheckBox)e.getSource(); 2847 getStore().put("mcv.supportreq.cc", cb.isSelected()); 2848 }); 2849 2850 boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true); 2851 2852 HttpFormEntry descriptionEntry; 2853 HttpFormEntry nameEntry; 2854 HttpFormEntry emailEntry; 2855 HttpFormEntry orgEntry; 2856 2857 entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2858 "form_data[fromName]", "Name:", 2859 getStore().get(PROP_HELP_NAME, (String) null))); 2860 entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2861 "form_data[email]", "Your Email:", 2862 getStore().get(PROP_HELP_EMAIL, (String) null))); 2863 entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2864 "form_data[organization]", "Organization:", 2865 getStore().get(PROP_HELP_ORG, (String) null))); 2866 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2867 "form_data[subject]", "Subject:")); 2868 2869 entries.add( 2870 new HttpFormEntry( 2871 HttpFormEntry.TYPE_LABEL, "", 2872 "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>")); 2873 entries.add(descriptionEntry = 2874 new FormEntry(doWrap, HttpFormEntry.TYPE_AREA, 2875 "form_data[description]", "Description:", 2876 description, 5, 30, true)); 2877 2878 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE, 2879 "form_data[att_two]", "Attachment 1:", "", 2880 false)); 2881 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE, 2882 "form_data[att_three]", "Attachment 2:", "", 2883 false)); 2884 2885 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2886 "form_data[submit]", "", "Send Email")); 2887 2888 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2889 "form_data[p_version]", "", 2890 getStateManager().getVersion() 2891 + " build date:" 2892 + getStateManager().getBuildDate())); 2893 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2894 "form_data[opsys]", "", 2895 System.getProperty("os.name"))); 2896 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2897 "form_data[hardware]", "", 2898 javaInfo.toString())); 2899 2900 JLabel topLabel = 2901 new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>"); 2902 2903 JCheckBox includeBundleCbx = 2904 new JCheckBox("Include Current State as Bundle", false); 2905 2906 List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself); 2907 2908 boolean alreadyHaveDialog = true; 2909 if (dialog == null) { 2910 // NOTE: if the dialog is modeless you can leave alreadyHaveDialog 2911 // alone. If the dialog is modal you need to set alreadyHaveDialog 2912 // to false. 2913 // If alreadyHaveDialog is false with a modeless dialog, the later 2914 // call to HttpFormEntry.showUI will return false and break out of 2915 // the while loop without talking to the HTTP server. 2916 dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(), 2917 "Support Request Form", false); 2918// alreadyHaveDialog = false; 2919 } 2920 2921 JLabel statusLabel = GuiUtils.cLabel(" "); 2922 JComponent bottom = LayoutUtil.vbox(LayoutUtil.leftVbox(checkboxes), statusLabel); 2923 2924 while (true) { 2925 //Show form. Check if user pressed cancel. 2926 statusLabel.setText(" "); 2927 if ( !HttpFormEntry.showUI(entries, LayoutUtil.inset(topLabel, 10), 2928 bottom, dialog, alreadyHaveDialog)) { 2929 break; 2930 } 2931 statusLabel.setText("Posting support request..."); 2932 2933 //Save persistent state 2934 getStore().put(PROP_HELP_NAME, nameEntry.getValue()); 2935 getStore().put(PROP_HELP_ORG, orgEntry.getValue()); 2936 getStore().put(PROP_HELP_EMAIL, emailEntry.getValue()); 2937 getStore().save(); 2938 2939 List<HttpFormEntry> entriesToPost = 2940 new ArrayList<>(entries); 2941 2942 if ((stackTrace != null) && (stackTrace.length() > 0)) { 2943 entriesToPost.remove(descriptionEntry); 2944 String newDescription = 2945 descriptionEntry.getValue() 2946 + "\n\n******************\nStack trace:\n" + stackTrace; 2947 entriesToPost.add( 2948 new HttpFormEntry( 2949 HttpFormEntry.TYPE_HIDDEN, "form_data[description]", 2950 "Description:", newDescription, 5, 30, true)); 2951 } 2952 2953 try { 2954 extra.append(idv.getPluginManager().getPluginHtml()); 2955 extra.append(getResourceManager().getHtmlView()); 2956 2957 entriesToPost.add(new HttpFormEntry("form_data[att_extra]", 2958 "extra.html", extra.toString().getBytes())); 2959 2960 if (includeBundleCbx.isSelected()) { 2961 entriesToPost.add( 2962 new HttpFormEntry( 2963 "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV, 2964 idv.getPersistenceManager().getBundleXml( 2965 true).getBytes())); 2966 } 2967 entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2968 "form_data[cc_user]", "", 2969 Boolean.toString(getStore().get("mcv.supportreq.cc", true)))); 2970 2971 String[] results = 2972 HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL); 2973 2974 if (results[0] != null) { 2975 GuiUtils.showHtmlDialog( 2976 results[0], "Support Request Response - Error", 2977 "Support Request Response - Error", null, true); 2978 continue; 2979 } 2980 String html = results[1]; 2981 if (html.toLowerCase().indexOf("your email has been sent") 2982 >= 0) { 2983 LogUtil.userMessage("Your support request has been sent"); 2984 break; 2985 } else if (html.toLowerCase().indexOf("required fields") 2986 >= 0) { 2987 LogUtil.userErrorMessage( 2988 "<html>There was a problem submitting your request. <br>Is your email correct?</html>"); 2989 } else { 2990 GuiUtils.showHtmlDialog( 2991 html, "Unknown Support Request Response", 2992 "Unknown Support Request Response", null, true); 2993 System.err.println(html.toLowerCase()); 2994 } 2995 } catch (Exception exc) { 2996 LogUtil.logException("Doing support request form", exc); 2997 } 2998 } 2999 dialog.dispose(); 3000 } 3001 3002 @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 3003 List viewManagers, Element skinRoot) 3004 { 3005 return new McIDASVXmlUi(window, viewManagers, idv, skinRoot); 3006 } 3007 3008 /** 3009 * DeInitialize the given menu before it is shown 3010 * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu) 3011 */ 3012 @Override 3013 protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) { 3014 super.handleMenuDeSelected(id, menu, idvWindow); 3015 } 3016 3017 /** 3018 * Initialize the given menu before it is shown 3019 * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu) 3020 */ 3021 @Override 3022 protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) { 3023 if (id.equals(MENU_NEWVIEWS)) { 3024 ViewManager last = getVMManager().getLastActiveViewManager(); 3025 menu.removeAll(); 3026 makeViewStateMenu(menu, last); 3027 } else if (id.equals("bundles")) { 3028 menu.removeAll(); 3029 makeBundleMenu(menu); 3030 } else if (id.equals(MENU_NEWDISPLAY_TAB)) { 3031 menu.removeAll(); 3032 doMakeNewDisplayMenu(menu, false); 3033 } else if (id.equals(MENU_NEWDISPLAY)) { 3034 menu.removeAll(); 3035 doMakeNewDisplayMenu(menu, true); 3036 } else if (id.equals("menu.tools.projections.deletesaved")) { 3037 menu.removeAll(); 3038 makeDeleteViewsMenu(menu); 3039 } else if (id.equals("file.default.layout")) { 3040 makeDefaultLayoutMenu(menu); 3041 } else if (id.equals("tools.formulas")) { 3042 menu.removeAll(); 3043 makeFormulasMenu(menu); 3044 } else { 3045 super.handleMenuSelected(id, menu, idvWindow); 3046 } 3047 } 3048 3049 /** A cache of the operand name to value for the user choices */ 3050 private Map operandCache; 3051 3052 @Override public List selectUserChoices(String msg, List userOperands) { 3053 if (operandCache == null) { 3054 operandCache = 3055 (Hashtable) getStore().getEncodedFile("operandcache.xml"); 3056 if (operandCache == null) { 3057 operandCache = new Hashtable(); 3058 } 3059 } 3060 List fields = new ArrayList(); 3061 List components = new ArrayList(); 3062 List persistentCbxs = new ArrayList(); 3063 components.add(new JLabel("Property")); 3064 components.add(new JLabel("Value")); 3065 components.add(new JLabel("Save in Bundle")); 3066 for (int i = 0; i < userOperands.size(); i++) { 3067 DataOperand operand = (DataOperand)userOperands.get(i); 3068 String fieldType = operand.getProperty("type"); 3069 if (fieldType == null) { 3070 fieldType = FIELDTYPE_TEXT; 3071 } 3072 DerivedDataChoice formula = operand.getDataChoice(); 3073 String description = operand.getDescription(); 3074 if (formula != null) { 3075 description = formula.toString(); 3076 } 3077 3078 String label = operand.getLabel(); 3079 Object dflt = operand.getUserDefault(); 3080 Object cacheKeyNewStyle = Misc.newList(description, label, fieldType); 3081 Object cacheKey = Misc.newList(label, fieldType); 3082 3083 Object cachedOperand = null; 3084 boolean oldStyle = operandCache.containsKey(cacheKey); 3085 boolean newStyle = operandCache.containsKey(cacheKeyNewStyle); 3086 3087 // if new style, always use that and ignore old style 3088 // if no new style, proceed as before. 3089 if (newStyle) { 3090 cachedOperand = operandCache.get(cacheKeyNewStyle); 3091 } else if (oldStyle) { 3092 cachedOperand = operandCache.get(cacheKey); 3093 } 3094 3095 if (cachedOperand != null) { 3096 dflt = cachedOperand; 3097 } 3098 3099 JCheckBox cbx = new JCheckBox("", operand.isPersistent()); 3100 persistentCbxs.add(cbx); 3101 JComponent field = null; 3102 JComponent fieldComp = null; 3103 if (fieldType.equals(FIELDTYPE_TEXT)) { 3104 String rowString = operand.getProperty("rows"); 3105 if (rowString == null) { 3106 rowString = "1"; 3107 } 3108 int rows = new Integer(rowString).intValue(); 3109 if (rows == 1) { 3110 field = new JTextField((dflt != null) 3111 ? dflt.toString() 3112 : "", 15); 3113 } else { 3114 field = new JTextArea((dflt != null) 3115 ? dflt.toString() 3116 : "", rows, 15); 3117 fieldComp = GuiUtils.makeScrollPane(field, 200, 100); 3118 } 3119 } else if (fieldType.equals(FIELDTYPE_BOOLEAN)) { 3120 field = new JCheckBox("", ((dflt != null) 3121 ? new Boolean( 3122 dflt.toString()).booleanValue() 3123 : true)); 3124 } else if (fieldType.equals(FIELDTYPE_CHOICE)) { 3125 String choices = operand.getProperty("choices"); 3126 if (choices == null) { 3127 throw new IllegalArgumentException( 3128 "No 'choices' attribute defined for operand: " 3129 + operand); 3130 } 3131 List l = StringUtil.split(choices, ";", true, true); 3132 field = new JComboBox(new Vector(l)); 3133 if ((dflt != null) && l.contains(dflt)) { 3134 ((JComboBox) field).setSelectedItem(dflt); 3135 } 3136 } else if (fieldType.equals(FIELDTYPE_FILE)) { 3137 JTextField fileFld = new JTextField(((dflt != null) 3138 ? dflt.toString() 3139 : ""), 30); 3140 field = fileFld; 3141 String patterns = operand.getProperty("filepattern"); 3142 List filters = null; 3143 if (patterns != null) { 3144 filters = new ArrayList(); 3145 List toks = StringUtil.split(patterns, ";", true, true); 3146 for (int tokIdx = 0; tokIdx < toks.size(); tokIdx++) { 3147 String tok = (String) toks.get(tokIdx); 3148 List subToks = StringUtil.split(tok, ":", true, true); 3149 if (subToks.size() == 2) { 3150 filters.add( 3151 new PatternFileFilter( 3152 (String)subToks.get(0), 3153 (String)subToks.get(1))); 3154 } else { 3155 filters.add(new PatternFileFilter(tok, tok)); 3156 } 3157 } 3158 } 3159 fieldComp = GuiUtils.centerRight(GuiUtils.hfill(fileFld), 3160 GuiUtils.makeFileBrowseButton(fileFld, filters)); 3161 } else if (fieldType.equals(FIELDTYPE_LOCATION)) { 3162 List l = ((dflt != null) 3163 ? StringUtil.split(dflt.toString(), ";", true, true) 3164 : (List) new ArrayList()); 3165 final LatLonWidget llw = new LatLonWidget(); 3166 field = llw; 3167 if (l.size() == 2) { 3168 llw.setLat(Misc.decodeLatLon(l.get(0).toString())); 3169 llw.setLon(Misc.decodeLatLon(l.get(1).toString())); 3170 } 3171 final JButton centerPopupBtn = 3172 GuiUtils.getImageButton("/auxdata/ui/icons/Map16.gif", 3173 getClass()); 3174 centerPopupBtn.setToolTipText("Center on current displays"); 3175 centerPopupBtn.addActionListener(ae -> popupCenterMenu(centerPopupBtn, llw)); 3176 JComponent centerPopup = GuiUtils.inset(centerPopupBtn, 3177 new Insets(0, 0, 0, 4)); 3178 fieldComp = GuiUtils.hbox(llw, centerPopup); 3179 } else if (fieldType.equals(FIELDTYPE_AREA)) { 3180 //TODO: 3181 } else { 3182 throw new IllegalArgumentException("Unknown type: " 3183 + fieldType + " for operand: " + operand); 3184 } 3185 3186 fields.add(field); 3187 label = StringUtil.replace(label, "_", " "); 3188 components.add(GuiUtils.rLabel(label)); 3189 components.add((fieldComp != null) 3190 ? fieldComp 3191 : field); 3192 components.add(cbx); 3193 } 3194 // GuiUtils.tmpColFills = new int[] { GridBagConstraints.HORIZONTAL, 3195 // GridBagConstraints.NONE, 3196 // GridBagConstraints.NONE }; 3197 GuiUtils.tmpInsets = GuiUtils.INSETS_5; 3198 Component contents = GuiUtils.topCenter(new JLabel(msg), 3199 GuiUtils.doLayout(components, 3, 3200 GuiUtils.WT_NYN, GuiUtils.WT_N)); 3201 if ( !GuiUtils.showOkCancelDialog(null, "Select input", contents, 3202 null, fields)) { 3203 return null; 3204 } 3205 List values = new ArrayList(); 3206 for (int i = 0; i < userOperands.size(); i++) { 3207 DataOperand operand = (DataOperand) userOperands.get(i); 3208 String description = operand.getDescription(); 3209 DerivedDataChoice formula = operand.getDataChoice(); 3210 String label = operand.getLabel(); 3211 Object field = fields.get(i); 3212 Object value = null; 3213 Object cacheValue = null; 3214 3215 if (formula != null) { 3216 description = formula.toString(); 3217 } 3218 3219 if (field instanceof JTextComponent) { 3220 value = ((JTextComponent) field).getText().trim(); 3221 } else if (field instanceof JCheckBox) { 3222 value = new Boolean(((JCheckBox)field).isSelected()); 3223 } else if (field instanceof JComboBox) { 3224 value = ((JComboBox) field).getSelectedItem(); 3225 } else if (field instanceof LatLonWidget) { 3226 LatLonWidget llw = (LatLonWidget) field; 3227 value = new LatLonPointImpl(llw.getLat(), llw.getLon()); 3228 cacheValue = llw.getLat() + ";" + llw.getLon(); 3229 } else { 3230 throw new IllegalArgumentException("Unknown field type:" 3231 + field.getClass().getName()); 3232 } 3233 if (cacheValue == null) { 3234 cacheValue = value; 3235 } 3236 JCheckBox cbx = (JCheckBox)persistentCbxs.get(i); 3237 String fieldType = operand.getProperty("type"); 3238 if (fieldType == null) { 3239 fieldType = "text"; 3240 } 3241 3242 Object cacheKey = Misc.newList(description, label, fieldType); 3243 operandCache.put(cacheKey, cacheValue); 3244 values.add(new UserOperandValue(value, cbx.isSelected())); 3245 } 3246 getStore().putEncodedFile("operandcache.xml", operandCache); 3247 return values; 3248 } 3249 3250 private boolean didTabs = false; 3251 private boolean didNewWindow = false; 3252 3253 public void makeDefaultLayoutMenu(final JMenu menu) { 3254 if (menu == null) 3255 throw new NullPointerException("Must provide a non-null default layout menu"); 3256 3257 menu.removeAll(); 3258 JMenuItem saveLayout = new JMenuItem("Save"); 3259 McVGuiUtils.setMenuImage(saveLayout, Constants.ICON_DEFAULTLAYOUTADD_SMALL); 3260 saveLayout.setToolTipText("Save as default layout"); 3261 saveLayout.addActionListener(e -> ((McIDASV)idv).doSaveAsDefaultLayout()); 3262 3263 JMenuItem removeLayout = new JMenuItem("Remove"); 3264 McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL); 3265 removeLayout.setToolTipText("Remove saved default layout"); 3266 removeLayout.addActionListener(e -> idv.doClearDefaults()); 3267 3268 removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout()); 3269 3270 menu.add(saveLayout); 3271 menu.add(removeLayout); 3272 } 3273 3274 /** 3275 * Bundles any compatible {@link ViewManager} states into 3276 * {@link JMenuItem JMenuItem} and adds said menu items to {@code menu}. 3277 * Incompatible states are ignored. 3278 * 3279 * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"} 3280 * menu--apologies) associates a {@literal "view state"} and an 3281 * {@link ObjectListener}. The {@code ObjectListener} uses this associated 3282 * view state to attempt reinitialization of {@code vm}. 3283 * 3284 * <p>Override reasoning: 3285 * <ul> 3286 * <li> 3287 * terminology ({@literal "views"} rather than {@literal "viewpoints"}). 3288 * </li> 3289 * <li> 3290 * use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to 3291 * properly detect the {@literal "no saved views"} case. 3292 * </li> 3293 * </ul> 3294 * 3295 * @param menu Menu to populate. Should not be {@code null}. 3296 * @param vm {@code ViewManager} that might get reinitialized. 3297 * Should not be {@code null}. 3298 * 3299 * @see ViewManager#initWith(ViewManager, boolean) 3300 * @see ViewManager#initWith(ViewState) 3301 */ 3302 @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) { 3303 List<TwoFacedObject> vmStates = 3304 filterVMMStatesWithVM(vm, getVMManager().getVMState()); 3305 if (vmStates.isEmpty()) { 3306 JMenuItem item = new JMenuItem(Msg.msg("No Saved Views")); 3307 item.setEnabled(false); 3308 menu.add(item); 3309 } else { 3310 JMenu deleteMenu = new JMenu("Delete"); 3311 makeDeleteViewsMenu(deleteMenu); 3312 menu.add(deleteMenu); 3313 } 3314 3315 for (TwoFacedObject tfo : vmStates) { 3316 JMenuItem mi = new JMenuItem(tfo.getLabel().toString()); 3317 menu.add(mi); 3318 mi.addActionListener(new ObjectListener(tfo.getId()) { 3319 public void actionPerformed(final ActionEvent e) { 3320 if (vm == null) { 3321 return; 3322 } 3323 3324 if (theObject instanceof ViewManager) { 3325 vm.initWith((ViewManager)theObject, true); 3326 } else if (theObject instanceof ViewState) { 3327 try { 3328 vm.initWith((ViewState)theObject); 3329 } catch (Throwable ex) { 3330 logException("Initializing view with ViewState", ex); 3331 } 3332 } else { 3333 LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName()); 3334 } 3335 } 3336 }); 3337 } 3338 3339 // the "3" ensures that the "save viewpoint" menu item, the separator, 3340 // and the "delete" menu item are fixed at the top. 3341 new MenuScroller(menu, menu, 125, 3); 3342 } 3343 3344 /** 3345 * Overridden by McIDAS-V to add menu scrolling functionality to the 3346 * {@literal "delete"} submenu. 3347 * 3348 * @param menu {@literal "Delete"} submenu. 3349 */ 3350 @Override public void makeDeleteViewsMenu(JMenu menu) { 3351 super.makeDeleteViewsMenu(menu); 3352 new MenuScroller(menu, menu, 125); 3353 } 3354 3355 /** 3356 * Returns a list of {@link TwoFacedObject}s that are known to be 3357 * compatible with {@code vm}. 3358 * 3359 * <p>This method is currently capable of dealing with 3360 * {@link TwoFacedObject TwoFacedObjects} and 3361 * {@link ViewState ViewStates} within {@code states}. Any other types are 3362 * ignored. 3363 * 3364 * @param vm {@link ViewManager} to use for compatibility tests. 3365 * {@code null} is allowed. 3366 * @param states Collection of objects to test against {@code vm}. 3367 * {@code null} is allowed. 3368 * 3369 * @return Either a {@link List} of compatible {@literal "view states"} 3370 * or an empty {@code List}. 3371 * 3372 * @see ViewManager#isCompatibleWith(ViewManager) 3373 * @see ViewManager#isCompatibleWith(ViewState) 3374 * @see #makeViewStateMenu(JMenu, ViewManager) 3375 */ 3376 public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) { 3377 if ((vm == null) || (states == null) || states.isEmpty()) { 3378 return Collections.emptyList(); 3379 } 3380 3381 List<TwoFacedObject> validStates = new ArrayList<>(states.size()); 3382 for (Object obj : states) { 3383 TwoFacedObject tfo = null; 3384 if (obj instanceof TwoFacedObject) { 3385 tfo = (TwoFacedObject)obj; 3386 if (vm.isCompatibleWith((ViewManager)tfo.getId())) { 3387 continue; 3388 } 3389 } else if (obj instanceof ViewState) { 3390 if (!vm.isCompatibleWith((ViewState)obj)) { 3391 continue; 3392 } 3393 tfo = new TwoFacedObject(((ViewState)obj).getName(), obj); 3394 } else { 3395 LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName()); 3396 continue; 3397 } 3398 validStates.add(tfo); 3399 } 3400 return validStates; 3401 } 3402 3403 /** 3404 * Overridden to build a custom Display menu. 3405 * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu) 3406 */ 3407 @Override protected void initializeDisplayMenu(JMenu displayMenu) { 3408 JMenu m; 3409 JMenuItem mi; 3410 3411 // Get the list of possible standalone control descriptors 3412 Hashtable controlsHash = new Hashtable(); 3413 List controlDescriptors = getStandAloneControlDescriptors(); 3414 for (int i = 0; i < controlDescriptors.size(); i++) { 3415 ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i); 3416 String cdLabel = cd.getLabel(); 3417 if (cdLabel.equals("Range Rings")) { 3418 controlsHash.put(cdLabel, cd); 3419 } else if (cdLabel.equals("Range and Bearing")) { 3420 controlsHash.put(cdLabel, cd); 3421 } else if (cdLabel.equals("Location Indicator")) { 3422 controlsHash.put(cdLabel, cd); 3423 } else if (cdLabel.equals("Drawing Control")) { 3424 controlsHash.put(cdLabel, cd); 3425 } else if (cdLabel.equals("Transect Drawing Control")) { 3426 controlsHash.put(cdLabel, cd); 3427 } 3428 } 3429 3430 // Build the menu 3431 ControlDescriptor cd; 3432 3433 mi = new JMenuItem("Create Layer from Data Source..."); 3434 mi.addActionListener(ae -> showDashboard("Data Sources")); 3435 displayMenu.add(mi); 3436 3437 mi = new JMenuItem("Layer Controls..."); 3438 mi.addActionListener(ae -> showDashboard("Layer Controls")); 3439 displayMenu.add(mi); 3440 3441 displayMenu.addSeparator(); 3442 3443 cd = (ControlDescriptor)controlsHash.get("Range Rings"); 3444 mi = makeControlDescriptorItem(cd); 3445 mi.setText("Add Range Rings"); 3446 displayMenu.add(mi); 3447 3448 cd = (ControlDescriptor)controlsHash.get("Range and Bearing"); 3449 mi = makeControlDescriptorItem(cd); 3450 McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL); 3451 mi.setText("Add Range and Bearing"); 3452 displayMenu.add(mi); 3453 3454 displayMenu.addSeparator(); 3455 3456 cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control"); 3457 mi = makeControlDescriptorItem(cd); 3458 mi.setText("Draw Transect..."); 3459 displayMenu.add(mi); 3460 3461 cd = (ControlDescriptor)controlsHash.get("Drawing Control"); 3462 mi = makeControlDescriptorItem(cd); 3463 mi.setText("Draw Freely..."); 3464 displayMenu.add(mi); 3465 3466 displayMenu.addSeparator(); 3467 3468 cd = (ControlDescriptor)controlsHash.get("Location Indicator"); 3469 mi = makeControlDescriptorItem(cd); 3470 McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL); 3471 mi.setText("Add Location Indicator"); 3472 displayMenu.add(mi); 3473 3474 ControlDescriptor locationDescriptor = idv.getControlDescriptor("locationcontrol"); 3475 if (locationDescriptor != null) { 3476 List stations = idv.getLocationList(); 3477 ObjectListener listener = new ObjectListener(locationDescriptor) { 3478 public void actionPerformed(ActionEvent ae, Object obj) { 3479 addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject); 3480 } 3481 }; 3482 List menuItems = NamedStationTable.makeMenuItems(stations, listener); 3483 displayMenu.add(MenuUtil.makeMenu("Plot Location Labels", menuItems)); 3484 } 3485 3486 displayMenu.addSeparator(); 3487 3488 mi = new JMenuItem("Add Background Image"); 3489 McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL); 3490 mi.addActionListener(ae -> getIdv().doMakeBackgroundImage()); 3491 displayMenu.add(mi); 3492 3493 mi = new JMenuItem("Reset Map Layer to Defaults"); 3494 mi.addActionListener(ae -> { 3495 // TODO: Call IdvUIManager.addDefaultMap()... should be made private 3496// addDefaultMap(); 3497 ControlDescriptor mapDescriptor = idv.getControlDescriptor("mapdisplay"); 3498 if (mapDescriptor == null) { 3499 return; 3500 } 3501 String attrs = "initializeAsDefault=true;displayName=Default Background Maps;"; 3502 idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null); 3503 }); 3504 displayMenu.add(mi); 3505 Msg.translateTree(displayMenu); 3506 } 3507 3508 /** 3509 * Get the window title from the skin 3510 * 3511 * @param index the skin index 3512 * 3513 * @return the title 3514 */ 3515 private String getWindowTitleFromSkin(final int index) { 3516 if (!skinToTitle.containsKey(index)) { 3517 IdvResourceManager mngr = getResourceManager(); 3518 XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN); 3519 List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true); 3520 String title = getStateManager().getTitle(); 3521 if (!names.isEmpty()) { 3522 title = title + " - " + StringUtil.join(" - ", names); 3523 } 3524 skinToTitle.put(index, title); 3525 } 3526 return skinToTitle.get(index); 3527 } 3528 3529 @SuppressWarnings("unchecked") 3530 @Override public Hashtable getMenuIds() { 3531 return menuIds; 3532 } 3533 3534 @SuppressWarnings("unchecked") 3535 @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) { 3536 Hashtable<String, JMenuItem> menuMap = new Hashtable<>(); 3537 JMenuBar menuBar = new JMenuBar(); 3538 final IdvResourceManager mngr = getResourceManager(); 3539 XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR); 3540 Hashtable<String, ImageIcon> actionIcons = new Hashtable<>(); 3541 3542 for (int i = 0; i < xrc.size(); i++) { 3543 GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons); 3544 } 3545 3546 menuIds = new Hashtable<>(menuMap); 3547 3548 // Ensure that the "help" menu is the last menu. 3549 JMenuItem helpMenu = menuMap.get(MENU_HELP); 3550 if (helpMenu != null) { 3551 menuBar.remove(helpMenu); 3552 menuBar.add(helpMenu); 3553 } 3554 3555 //TODO: Perhaps we will put the different skins in the menu? 3556 JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY); 3557 if (newDisplayMenu != null) { 3558 MenuUtil.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false)); 3559 } 3560 3561// final JMenu publishMenu = menuMap.get(MENU_PUBLISH); 3562// if (publishMenu != null) { 3563// if (!getPublishManager().isPublishingEnabled()) 3564// publishMenu.getParent().remove(publishMenu); 3565// else 3566// getPublishManager().initMenu(publishMenu); 3567// } 3568 3569 for (Entry<String, JMenuItem> e : menuMap.entrySet()) { 3570 if (!(e.getValue() instanceof JMenu)) { 3571 continue; 3572 } 3573 String menuId = e.getKey(); 3574 JMenu menu = (JMenu)e.getValue(); 3575 menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow)); 3576 } 3577 return menuBar; 3578 } 3579 3580 private final ActionListener makeMenuBarActionListener() { 3581 final IdvResourceManager mngr = getResourceManager(); 3582 return ae -> { 3583 XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN); 3584 int skinIndex = ((Integer)ae.getSource()).intValue(); 3585 createNewWindow(null, true, getWindowTitleFromSkin(skinIndex), 3586 skins.get(skinIndex).toString(), 3587 skins.getRoot(skinIndex, false), true, null); 3588 }; 3589 } 3590 3591 private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) { 3592 return new MenuListener() { 3593 public void menuCanceled(final MenuEvent e) { } 3594 public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); } 3595 public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); } 3596 }; 3597 } 3598 3599 /** 3600 * Handle mouse clicks that occur within the toolbar. 3601 */ 3602 private class PopupListener extends MouseAdapter { 3603 3604 private JPopupMenu popup; 3605 3606 public PopupListener(JPopupMenu p) { 3607 popup = p; 3608 } 3609 3610 // handle right clicks on os x and linux 3611 public void mousePressed(MouseEvent e) { 3612 if (e.isPopupTrigger()) { 3613 popup.show(e.getComponent(), e.getX(), e.getY()); 3614 } 3615 } 3616 3617 // Windows doesn't seem to trigger mousePressed() for right clicks, but 3618 // never fear; mouseReleased() does the job. 3619 public void mouseReleased(MouseEvent e) { 3620 if (e.isPopupTrigger()) { 3621 popup.show(e.getComponent(), e.getX(), e.getY()); 3622 } 3623 } 3624 } 3625 3626 /** 3627 * Handle (polymorphically) the {@link DataControlDialog}. 3628 * 3629 * This dialog is used to either select a display control to create 3630 * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}. 3631 * 3632 * @param dcd The dialog 3633 */ 3634 public void processDialog(DataControlDialog dcd) { 3635 int estimatedMB = getEstimatedMegabytes(dcd); 3636 if (estimatedMB > 0) { 3637 double totalMem = Runtime.getRuntime().maxMemory(); 3638 double highMem = Runtime.getRuntime().totalMemory(); 3639 double freeMem = Runtime.getRuntime().freeMemory(); 3640 double usedMem = (highMem - freeMem); 3641 int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f); 3642 int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f); 3643 if (percentOfAvailable > 95) { 3644 String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>"; 3645 message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>"; 3646 message += "Data load cancelled.</html>"; 3647 JComponent msgLabel = new JLabel(message); 3648 GuiUtils.showDialog("Data Size", msgLabel); 3649 return; 3650 } else if (percentOfAvailable >= 75) { 3651 String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>"; 3652 message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>"; 3653 message += "Continue loading data?</html>"; 3654 JComponent msgLabel = new JLabel(message); 3655 if (!GuiUtils.askOkCancel("Data Size", msgLabel)) { 3656 return; 3657 } 3658 } 3659 } 3660 super.processDialog(dcd); 3661 } 3662 3663 /** 3664 * Estimate the number of megabytes that will be used by this data selection 3665 * 3666 * @param dcd Data control dialog containing the data selection whose size 3667 * we'd like to estimate. Cannot be {@code null}. 3668 * 3669 * @return Rough estimate of the size (in megabytes) of the 3670 * {@code DataSelection} within {@code dcd}. 3671 */ 3672 protected int getEstimatedMegabytes(DataControlDialog dcd) { 3673 int estimatedMB = 0; 3674 DataChoice dataChoice = dcd.getDataChoice(); 3675 if (dataChoice != null) { 3676 Object[] selectedControls = dcd.getSelectedControls(); 3677 for (int i = 0; i < selectedControls.length; i++) { 3678 ControlDescriptor cd = (ControlDescriptor) selectedControls[i]; 3679 3680 //Check if the data selection is ok 3681 if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) { 3682 continue; 3683 } 3684 3685 DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels()); 3686 3687 // Get the size in pixels of the requested image 3688 Object gotSize = dataSelection.getProperty("SIZE"); 3689 if (gotSize == null) { 3690 continue; 3691 } 3692 List<String> dims = StringUtil.split(gotSize, " ", false, false); 3693 int myLines = -1; 3694 int myElements = -1; 3695 if (dims.size() == 2) { 3696 try { 3697 myLines = Integer.parseInt(dims.get(0)); 3698 myElements = Integer.parseInt(dims.get(1)); 3699 } 3700 catch (Exception e) { } 3701 } 3702 3703 // Get the count of times requested 3704 int timeCount = 1; 3705 DataSelectionWidget dsw = dcd.getDataSelectionWidget(); 3706 List times = dsw.getSelectedDateTimes(); 3707 List timesAll = dsw.getAllDateTimes(); 3708 if ((times != null) && !times.isEmpty()) { 3709 timeCount = times.size(); 3710 } else if ((timesAll != null) && !timesAll.isEmpty()) { 3711 timeCount = timesAll.size(); 3712 } 3713 3714 // Total number of pixels 3715 // Assumed lines x elements x times x 4bytes 3716 // Empirically seems to be taking *twice* that (64bit fields??) 3717 float totalPixels = (float)myLines * (float)myElements * (float)timeCount; 3718 float totalBytes = totalPixels * 4 * 2; 3719 estimatedMB += Math.round(totalBytes / 1024f / 1024f); 3720 3721 int additionalMB = 0; 3722 // Empirical tests show that textures are not affecting 3723 // required memory... comment out for now 3724 /* 3725 int textureDimensions = 2048; 3726 int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f); 3727 int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f); 3728 int additionalMB = textureCount * mbPerTexture * timeCount; 3729 */ 3730 estimatedMB += additionalMB; 3731 } 3732 } 3733 return estimatedMB; 3734 } 3735 3736 /** 3737 * Represents a SavedBundle as a tree. 3738 */ 3739 private class BundleTreeNode { 3740 3741 private String name; 3742 3743 private SavedBundle bundle; 3744 3745 private List<BundleTreeNode> kids; 3746 3747 /** 3748 * This constructor is used to build a node that is considered a 3749 * {@literal "parent"}. 3750 * 3751 * These nodes only have child nodes, no {@code SavedBundles}. This 3752 * was done so that distinguishing between bundles and bundle 3753 * subcategories would be easy. 3754 * 3755 * @param name The name of this node. For a parent node with 3756 * {@literal "Toolbar>cat"} as the path, the name parameter would 3757 * contain only {@literal "cat"}. 3758 */ 3759 public BundleTreeNode(String name) { 3760 this(name, null); 3761 } 3762 3763 /** 3764 * Nodes constructed using this constructor can only ever be child 3765 * nodes. 3766 * 3767 * @param name The name of the SavedBundle. 3768 * @param bundle A reference to the SavedBundle. 3769 */ 3770 public BundleTreeNode(String name, SavedBundle bundle) { 3771 this.name = name; 3772 this.bundle = bundle; 3773 kids = new LinkedList<>(); 3774 } 3775 3776 /** 3777 * @param child The node to be added to the current node. 3778 */ 3779 public void addChild(BundleTreeNode child) { 3780 kids.add(child); 3781 } 3782 3783 /** 3784 * @return Returns all child nodes of this node. 3785 */ 3786 public List<BundleTreeNode> getChildren() { 3787 return kids; 3788 } 3789 3790 /** 3791 * @return Return the SavedBundle associated with this node (if any). 3792 */ 3793 public SavedBundle getBundle() { 3794 return bundle; 3795 } 3796 3797 /** 3798 * @return The name of this node. 3799 */ 3800 public String getName() { 3801 return name; 3802 } 3803 } 3804 3805 /** 3806 * A type of {@code HttpFormEntry} that supports line wrapping for 3807 * text area entries. 3808 * 3809 * @see HttpFormEntry 3810 */ 3811 private static class FormEntry extends HttpFormEntry { 3812 /** Initial contents of this entry. */ 3813 private String value = ""; 3814 3815 /** Whether or not the JTextArea should wrap lines. */ 3816 private boolean wrap = true; 3817 3818 /** Entry type. Used to remain compatible with the IDV. */ 3819 private int type = HttpFormEntry.TYPE_AREA; 3820 3821 /** Number of rows in the JTextArea. */ 3822 private int rows = 5; 3823 3824 /** Number of columns in the JTextArea. */ 3825 private int cols = 30; 3826 3827 /** GUI representation of this entry. */ 3828 private JTextArea component = new JTextArea(value, rows, cols); 3829 3830 /** 3831 * Required to keep Java happy. 3832 */ 3833 public FormEntry() { 3834 super(HttpFormEntry.TYPE_AREA, "form_data[description]", 3835 "Description:"); 3836 } 3837 3838 /** 3839 * Using this constructor allows McIDAS-V to control whether or not a 3840 * HttpFormEntry performs line wrapping for JTextArea components. 3841 * 3842 * @param wrap Whether or not line wrapping should be enabled. 3843 * @param type Type of this entry 3844 * @param name Name 3845 * @param label Label 3846 * @param value Initial value 3847 * @param rows Number of rows. 3848 * @param cols Number of columns. 3849 * @param required Whether or not the entry will be required. 3850 */ 3851 public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) { 3852 super(type, name, label, value, rows, cols, required); 3853 this.type = type; 3854 this.rows = rows; 3855 this.cols = cols; 3856 this.wrap = wrap; 3857 } 3858 3859 /** 3860 * Overrides the IDV method so that the McIDAS-V support request form 3861 * will wrap lines in the "Description" field. 3862 * 3863 * @param guiComps List to which this instance should be added. 3864 */ 3865 @SuppressWarnings("unchecked") 3866 @Override public void addToGui(List guiComps) { 3867 if (type == HttpFormEntry.TYPE_AREA) { 3868 Dimension minSize = new Dimension(500, 200); 3869 guiComps.add(LayoutUtil.top(GuiUtils.rLabel(getLabel()))); 3870 component.setLineWrap(wrap); 3871 component.setWrapStyleWord(wrap); 3872 JScrollPane sp = new JScrollPane(component); 3873 sp.setPreferredSize(minSize); 3874 sp.setMinimumSize(minSize); 3875 guiComps.add(sp); 3876 } else { 3877 super.addToGui(guiComps); 3878 } 3879 } 3880 3881 /** 3882 * Since the IDV doesn't provide a getComponent for 3883 * {@code addToGui}, we must make our {@code component} field 3884 * local to this class. 3885 * Hijacks any value requests so that the local {@code component} 3886 * field is queried, not the IDV's. 3887 * 3888 * @return Contents of form. 3889 */ 3890 @Override public String getValue() { 3891 if (type != HttpFormEntry.TYPE_AREA) { 3892 return super.getValue(); 3893 } 3894 return component.getText(); 3895 } 3896 3897 /** 3898 * Hijacks any requests to set the {@code component} field's text. 3899 */ 3900 @Override public void setValue(final String newValue) { 3901 if (type == HttpFormEntry.TYPE_AREA) { 3902 component.setText(newValue); 3903 } else { 3904 super.setValue(newValue); 3905 } 3906 } 3907 } 3908 3909 /** 3910 * A {@code ToolbarStyle} is a representation of the way icons associated 3911 * with current toolbar actions should be displayed. This notion is so far 3912 * limited to the sizing of icons, but that may change. 3913 */ 3914 public enum ToolbarStyle { 3915 /** 3916 * Represents the current toolbar actions as large icons. Currently, 3917 * {@literal "large"} is defined as {@code 32 x 32} pixels. 3918 */ 3919 LARGE("Large Icons", "action.icons.large", 32), 3920 3921 /** 3922 * Represents the current toolbar actions as medium icons. Currently, 3923 * {@literal "medium"} is defined as {@code 22 x 22} pixels. 3924 */ 3925 MEDIUM("Medium Icons", "action.icons.medium", 22), 3926 3927 /** 3928 * Represents the current toolbar actions as small icons. Currently, 3929 * {@literal "small"} is defined as {@code 16 x 16} pixels. 3930 */ 3931 SMALL("Small Icons", "action.icons.small", 16); 3932 3933 /** Label to use in the toolbar customization popup menu. */ 3934 private final String label; 3935 3936 /** Signals that the user selected a specific icon size. */ 3937 private final String action; 3938 3939 /** Icon dimensions. Each icon should be {@code size * size}. */ 3940 private final int size; 3941 3942 /** 3943 * {@link #size} in {@link String} form, merely for use with the IDV's 3944 * preference functionality. 3945 */ 3946 private final String sizeAsString; 3947 3948 /** 3949 * Initializes a toolbar style. 3950 * 3951 * @param label Label used in the toolbar popup menu. 3952 * @param action Command that signals the user selected this toolbar 3953 * style. 3954 * @param size Dimensions of the icons. 3955 * 3956 * @throws NullPointerException if {@code label} or {@code action} are 3957 * null. 3958 * 3959 * @throws IllegalArgumentException if {@code size} is not positive. 3960 */ 3961 ToolbarStyle(final String label, final String action, final int size) { 3962 requireNonNull(label, "Label cannot be null."); 3963 requireNonNull(action, "Action cannot be null."); 3964 3965 if (size <= 0) { 3966 throw new IllegalArgumentException("Size must be a positive integer"); 3967 } 3968 3969 this.label = label; 3970 this.action = action; 3971 this.size = size; 3972 this.sizeAsString = Integer.toString(size); 3973 } 3974 3975 /** 3976 * Returns the label to use as a brief description of this style. 3977 * 3978 * @return Description of style (suitable for a label). 3979 */ 3980 public String getLabel() { 3981 return label; 3982 } 3983 3984 /** 3985 * Returns the action command associated with this style. 3986 * 3987 * @return This style's {@literal "action command"}. 3988 */ 3989 public String getAction() { 3990 return action; 3991 } 3992 3993 /** 3994 * Returns the dimensions of icons used in this style. 3995 * 3996 * @return Dimensions of this style's icons. 3997 */ 3998 public int getSize() { 3999 return size; 4000 } 4001 4002 /** 4003 * Returns {@link #size} as a {@link String} to make cooperating with 4004 * the IDV preferences code easier. 4005 * 4006 * @return String representation of this style's icon dimensions. 4007 */ 4008 public String getSizeAsString() { 4009 return sizeAsString; 4010 } 4011 4012 /** 4013 * Returns a brief description of this {@code ToolbarStyle}. 4014 * 4015 * <p>A typical example: 4016 * {@code [ToolbarStyle@1337: label="Large Icons", size=32]} 4017 * 4018 * <p>Note that the format and details provided are subject to change. 4019 * 4020 * @return String representation of this {@code ToolbarStyle} instance. 4021 */ 4022 public String toString() { 4023 return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 4024 hashCode(), label, size); 4025 } 4026 4027 /** 4028 * Convenience method for build the toolbar customization popup menu. 4029 * 4030 * @param manager {@link UIManager} that will be listening for action 4031 * commands. 4032 * 4033 * @return Menu item that has {@code manager} listening for 4034 * {@link #action}. 4035 */ 4036 protected JMenuItem buildMenuItem(final UIManager manager) { 4037 JMenuItem item = new JRadioButtonMenuItem(label); 4038 item.setActionCommand(action); 4039 item.addActionListener(manager); 4040 return item; 4041 } 4042 } 4043 4044 /** 4045 * Represents what McIDAS-V {@literal "knows"} about IDV actions. 4046 */ 4047 protected enum ActionAttribute { 4048 4049 /** 4050 * Unique identifier for an IDV action. Required attribute. 4051 * 4052 * @see IdvUIManager#ATTR_ID 4053 */ 4054 ID(ATTR_ID), 4055 4056 /** 4057 * Path to an icon for this action. Currently required. Note that 4058 * McIDAS-V differs from the IDV in that actions must support different 4059 * icon sizes. This is implemented in McIDAS-V by simply having the value 4060 * of this path be a valid {@literal "format string"}, 4061 * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"} 4062 * 4063 * <p>The upshot is that this value <b>will not be a valid path in 4064 * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 4065 * {@link IdvAction#getIconForStyle}. 4066 * 4067 * @see IdvUIManager#ATTR_IMAGE 4068 * @see IdvAction#getRawIconPath() 4069 * @see IdvAction#getMenuIcon() 4070 * @see IdvAction#getIconForStyle 4071 */ 4072 ICON(ATTR_IMAGE), 4073 4074 /** 4075 * Brief description of a IDV action. Required attribute. 4076 * @see IdvUIManager#ATTR_DESCRIPTION 4077 */ 4078 DESCRIPTION(ATTR_DESCRIPTION), 4079 4080 /** 4081 * Allows actions to be clustered into arbitrary groups. Currently 4082 * optional; defaults to {@literal "General"}. 4083 * @see IdvUIManager#ATTR_GROUP 4084 */ 4085 GROUP(ATTR_GROUP, "General"), 4086 4087 /** 4088 * Actual method call used to invoke a given IDV action. Required 4089 * attribute. 4090 * @see IdvUIManager#ATTR_ACTION 4091 */ 4092 ACTION(ATTR_ACTION); 4093 4094 /** 4095 * A blank {@link String} if this is a required attribute, or a 4096 * {@code String} value to use in case this attribute has not been 4097 * specified by a given IDV action. 4098 */ 4099 private final String defaultValue; 4100 4101 /** 4102 * String representation of this attribute as used by the IDV. 4103 * @see #asIdvString() 4104 */ 4105 private final String idvString; 4106 4107 /** Whether or not this attribute is required. */ 4108 private final boolean required; 4109 4110 /** 4111 * Creates a constant that represents a required IDV action attribute. 4112 * 4113 * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}. 4114 * 4115 * @throws NullPointerException if {@code idvString} is {@code null}. 4116 */ 4117 ActionAttribute(final String idvString) { 4118 requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String"); 4119 4120 this.idvString = idvString; 4121 this.defaultValue = ""; 4122 this.required = true; 4123 } 4124 4125 /** 4126 * Creates a constant that represents an optional IDV action attribute. 4127 * 4128 * @param idvString Corresponding IDV attribute {@link String}. 4129 * Cannot be {@code null}. 4130 * @param defValue Default value for actions that do not have this 4131 * attribute. Cannot be {@code null} or an empty {@code String}. 4132 * 4133 * @throws NullPointerException if either {@code idvString} or 4134 * {@code defValue} is {@code null}. 4135 * @throws IllegalArgumentException if {@code defValue} is an empty 4136 * {@code String}. 4137 * 4138 */ 4139 ActionAttribute(final String idvString, final String defValue) { 4140 requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String"); 4141 Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString()); 4142 Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString()); 4143 4144 this.idvString = idvString; 4145 this.defaultValue = defValue; 4146 this.required = (defaultValue.equals("")); 4147 } 4148 4149 /** 4150 * @return The {@link String} representation of this attribute, as is 4151 * used by the IDV. 4152 * 4153 * @see IdvUIManager#ATTR_ACTION 4154 * @see IdvUIManager#ATTR_DESCRIPTION 4155 * @see IdvUIManager#ATTR_GROUP 4156 * @see IdvUIManager#ATTR_ID 4157 * @see IdvUIManager#ATTR_IMAGE 4158 */ 4159 public String asIdvString() { return idvString; } 4160 4161 /** 4162 * @return {@literal "Default value"} for this attribute. 4163 * Blank {@link String}s imply that the attribute is required (and 4164 * thus lacks a true default value). 4165 */ 4166 public String defaultValue() { return defaultValue; } 4167 4168 /** 4169 * @return Whether or not this attribute is a required attribute for 4170 * valid {@link IdvAction}s. 4171 */ 4172 public boolean isRequired() { return required; } 4173 } 4174 4175 /** 4176 * Represents the set of known {@link IdvAction IdvActions} in an idiom 4177 * that can be easily used by both the IDV and McIDAS-V. 4178 */ 4179 // TODO(jon:101): use Sets instead of maps and whatnot 4180 // TODO(jon:103): create an invalid IdvAction 4181 public static final class IdvActions { 4182 4183 /** Maps {@literal "id"} values to {@link IdvAction IdvActions}. */ 4184 private final Map<String, IdvAction> idToAction = 4185 new ConcurrentHashMap<>(); 4186 4187 /** 4188 * Collects {@link IdvAction IdvActions} {@literal "under"} common 4189 * group values. 4190 */ 4191 // TODO(jon:102): this should probably become concurrency-friendly. 4192 private final Map<String, Set<IdvAction>> groupToActions = 4193 new LinkedHashMap<>(); 4194 4195 /** 4196 * Creates an object that represents the application's 4197 * {@link IdvAction IdvActions}. 4198 * 4199 * @param idv Reference to the IDV {@literal "god"} object. 4200 * Cannot be {@code null}. 4201 * @param collectionId IDV resource collection that contains our 4202 * actions. Cannot be {@code null}. 4203 * 4204 * @throws NullPointerException if {@code idv} or {@code collectionId} 4205 * is {@code null}. 4206 */ 4207 public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) { 4208 requireNonNull(idv, "Cannot provide a null IDV reference"); 4209 requireNonNull(collectionId, "Cannot build actions from a null collection id"); 4210 4211 // TODO(jon): benchmark use of xpath 4212 String query = "//action[@id and @image and @description and @action]"; 4213 for (Element e : elements(idv, collectionId, query)) { 4214 IdvAction a = new IdvAction(e); 4215 String id = a.getAttribute(ActionAttribute.ID); 4216 idToAction.put(id, a); 4217 String group = a.getAttribute(ActionAttribute.GROUP); 4218 if (!groupToActions.containsKey(group)) { 4219 groupToActions.put(group, new LinkedHashSet<>()); 4220 } 4221 Set<IdvAction> groupedIds = groupToActions.get(group); 4222 groupedIds.add(a); 4223 } 4224 } 4225 4226 /** 4227 * Attempts to return the {@link IdvAction} associated with the given 4228 * {@code actionId}. 4229 * 4230 * @param actionId Identifier to use in the search. Cannot be 4231 * {@code null}. 4232 * 4233 * @return Either the {@code IdvAction} that matches {@code actionId} 4234 * or {@code null} if there was no match. 4235 * 4236 * @throws NullPointerException if {@code actionId} is {@code null}. 4237 */ 4238 // TODO(jon:103) here 4239 public IdvAction getAction(final String actionId) { 4240 requireNonNull(actionId, "Null action identifiers are not allowed"); 4241 return idToAction.get(actionId); 4242 } 4243 4244 /** 4245 * Searches for the action associated with {@code actionId} and 4246 * returns the value associated with the given {@link ActionAttribute}. 4247 * 4248 * @param actionId Identifier to search for. Cannot be {@code null}. 4249 * @param attr Attribute whose value is desired. Cannot be {@code null}. 4250 * 4251 * @return Either the desired attribute value of the desired action, 4252 * or {@code null} if {@code actionId} has no associated action. 4253 * 4254 * @throws NullPointerException if either {@code actionId} or 4255 * {@code attr} is {@code null}. 4256 */ 4257 // TODO(jon:103) here 4258 public String getAttributeForAction(final String actionId, final ActionAttribute attr) { 4259 requireNonNull(actionId, "Null action identifiers are not allowed"); 4260 requireNonNull(attr, "Actions cannot have values associated with a null attribute"); 4261 IdvAction action = idToAction.get(actionId); 4262 if (action == null) { 4263 return null; 4264 } 4265 return action.getAttribute(attr); 4266 } 4267 4268 /** 4269 * Attempts to return the XML {@link Element} that 4270 * {@literal "represents"} the action associated with {@code actionId}. 4271 * 4272 * @param actionId Identifier whose XML element is desired. 4273 * Cannot be {@code null}. 4274 * 4275 * @return Either the XML element associated with {@code actionId} or 4276 * {@code null}. 4277 * 4278 * @throws NullPointerException if {@code actionId} is {@code null}. 4279 * 4280 * @see IdvAction#originalElement 4281 */ 4282 // TODO(jon:103) here 4283 public Element getElementForAction(final String actionId) { 4284 requireNonNull(actionId, "Cannot search for a null action identifier"); 4285 IdvAction action = idToAction.get(actionId); 4286 if (action == null) { 4287 return null; 4288 } 4289 return action.getElement(); 4290 } 4291 4292 /** 4293 * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and 4294 * {@link ToolbarStyle}. 4295 * 4296 * @param actionId ID of the action whose {@literal "styled"} icon is 4297 * desired. Cannot be {@code null}. 4298 * @param style Desired {@code Icon} style. Cannot be {@code null}. 4299 * 4300 * @return Either the {@code Icon} associated with {@code actionId} 4301 * and {@code style}, or {@code null}. 4302 * 4303 * @throws NullPointerException if either {@code actionId} or 4304 * {@code style} is {@code null}. 4305 */ 4306 // TODO(jon:103) here 4307 public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) { 4308 requireNonNull(actionId, "Cannot get an icon for a null action identifier"); 4309 requireNonNull(style, "Cannot get an icon for a null ToolbarStyle"); 4310 IdvAction a = idToAction.get(actionId); 4311 if (a == null) { 4312 return null; 4313 } 4314 return a.getIconForStyle(style); 4315 } 4316 4317 // TODO(jon:105): replace with something better 4318 public List<String> getAttributes(final ActionAttribute attr) { 4319 requireNonNull(attr, "Actions cannot have null attributes"); 4320 List<String> attributeList = arrList(idToAction.size()); 4321 for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) { 4322 attributeList.add(entry.getValue().getAttribute(attr)); 4323 } 4324 return attributeList; 4325 } 4326 4327 /** 4328 * @return List of all known {@code IdvAction}s. 4329 */ 4330 public List<IdvAction> getAllActions() { 4331 return arrList(idToAction.values()); 4332 } 4333 4334 /** 4335 * @return List of all known action groupings. 4336 * 4337 * @see ActionAttribute#GROUP 4338 * @see #getActionsForGroup(String) 4339 */ 4340 public List<String> getAllGroups() { 4341 return arrList(groupToActions.keySet()); 4342 } 4343 4344 /** 4345 * Returns the {@link Set} of {@link IdvAction}s associated with the 4346 * given {@code group}. 4347 * 4348 * @param group Group whose associated actions you want. Cannot be 4349 * {@code null}. 4350 * 4351 * @return Collection of {@code IdvAction}s associated with 4352 * {@code group}. A blank collection is returned if there are no actions 4353 * associated with {@code group}. 4354 * 4355 * @throws NullPointerException if {@code group} is {@code null}. 4356 * 4357 * @see ActionAttribute#GROUP 4358 * @see #getAllGroups() 4359 */ 4360 public Set<IdvAction> getActionsForGroup(final String group) { 4361 requireNonNull(group, "Actions cannot be associated with a null group"); 4362 if (!groupToActions.containsKey(group)) { 4363 return Collections.emptySet(); 4364 } 4365 return groupToActions.get(group); 4366 } 4367 4368 /** 4369 * Returns a summary of the known IDV actions. Please note that this 4370 * format is subject to change, and is not intended for serialization. 4371 * 4372 * @return String that looks like 4373 * {@code [IdvActions@HASHCODE: actions=...]}. 4374 */ 4375 @Override public String toString() { 4376 return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction); 4377 } 4378 } 4379 4380 /** 4381 * Represents an individual IDV action. Should be fairly adaptable to 4382 * unforeseen changes from Unidata? 4383 */ 4384 // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right? 4385 public static final class IdvAction { 4386 4387 /** The XML {@link Element} that represents this IDV action. */ 4388 private final Element originalElement; 4389 4390 /** Mapping of (known) XML attributes to values for this individual action. */ 4391 private final Map<ActionAttribute, String> attributes; 4392 4393 /** 4394 * Simple {@literal "cache"} for the different icons this action has 4395 * displayed. This is {@literal "lazy"}, so the cache does not contain 4396 * icons for {@link ToolbarStyle}s that haven't been used. 4397 */ 4398 private final Map<ToolbarStyle, Icon> iconCache = 4399 new ConcurrentHashMap<>(); 4400 4401 /** 4402 * Creates a representation of an IDV action using a given 4403 * {@link Element}. 4404 * 4405 * @param element XML representation of an IDV action. 4406 * Cannot be {@code null}. 4407 * 4408 * @throws NullPointerException if {@code element} is {@code null}. 4409 * @throws IllegalArgumentException if {@code element} is not a valid 4410 * IDV action. 4411 * 4412 * @see UIManager#isValidIdvAction(Element) 4413 */ 4414 public IdvAction(final Element element) { 4415 requireNonNull(element, "Cannot build an action from a null element"); 4416 // TODO(jon:107): need a way to diagnose what's wrong with the action? 4417 Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes"); 4418 originalElement = element; 4419 attributes = actionElementToMap(element); 4420 } 4421 4422 /** 4423 * @return Returns the {@literal "raw"} path to the icon associated 4424 * with this action. Remember that this is actually a 4425 * {@literal "format string"} and should not be considered a valid path! 4426 * 4427 * @see #getIconForStyle 4428 */ 4429 public String getRawIconPath() { 4430 return attributes.get(ActionAttribute.ICON); 4431 } 4432 4433 /** 4434 * @return Returns the {@link Icon} associated with 4435 * {@link ToolbarStyle#SMALL}. 4436 */ 4437 public Icon getMenuIcon() { 4438 return getIconForStyle(ToolbarStyle.SMALL); 4439 } 4440 4441 /** 4442 * Returns the {@link Icon} associated with this action and the given 4443 * {@link ToolbarStyle}. 4444 * 4445 * @param style {@literal "Style"} of the {@code Icon} to be returned. 4446 * Cannot be {@code null}. 4447 * 4448 * @return This action's {@code Icon} with {@code style} 4449 * {@literal "applied."} 4450 * 4451 * @see ActionAttribute#ICON 4452 * @see #iconCache 4453 */ 4454 public Icon getIconForStyle(final ToolbarStyle style) { 4455 requireNonNull(style, "Cannot build an icon for a null ToolbarStyle"); 4456 if (!iconCache.containsKey(style)) { 4457 String styledPath = String.format(getRawIconPath(), style.getSize()); 4458 URL tmp = getClass().getResource(styledPath); 4459 iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp))); 4460 } 4461 return iconCache.get(style); 4462 } 4463 4464 /** 4465 * @return Returns the identifier of this {@code IdvAction}. 4466 */ 4467 public String getId() { 4468 return getAttribute(ActionAttribute.ID); 4469 } 4470 4471 /** 4472 * Representation of this {@code IdvAction} as an 4473 * {@literal "IDV action call"}. 4474 * 4475 * @return String that is suitable to hand off to the IDV for execution. 4476 */ 4477 public String getCommand() { 4478 return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')"; 4479 } 4480 4481 /** 4482 * Returns the value associated with a given {@link ActionAttribute} 4483 * for this action. 4484 * 4485 * @param attr ActionAttribute whose value you want. 4486 * Cannot be {@code null}. 4487 * 4488 * @return Value associated with {@code attr}. 4489 * 4490 * @throws NullPointerException if {@code attr} is {@code null}. 4491 */ 4492 public String getAttribute(final ActionAttribute attr) { 4493 requireNonNull(attr, "No values can be associated with a null ActionAttribute"); 4494 return attributes.get(attr); 4495 } 4496 4497 /** 4498 * @return The XML {@link Element} used to create this {@code IdvAction}. 4499 */ 4500 // TODO(jon:104): any way to copy this element? if so, this can become an immutable class! 4501 public Element getElement() { 4502 return originalElement; 4503 } 4504 4505 /** 4506 * Returns a brief description of this action. Please note that the 4507 * format is subject to change and is not intended for serialization. 4508 * 4509 * @return String that looks like 4510 * {@code [IdvAction@HASHCODE: attributes=...]}. 4511 */ 4512 @Override public String toString() { 4513 return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes); 4514 } 4515 } 4516}