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