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 * Method to do the work of showing the Data Explorer (nee Dashboard). 2569 * 2570 * @param tabName Name of the tab that should be made active. 2571 * Cannot be {@code null}, but empty {@code String} values 2572 * will not change the active tab. 2573 */ 2574 @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows. 2575 public void showDashboard(String tabName) { 2576 if (!initDone) { 2577 return; 2578 } else if (dashboard == null) { 2579 showWaitCursor(); 2580 doMakeBasicWindows(); 2581 showNormalCursor(); 2582 String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME); 2583 for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) { 2584 if (title.equals(window.getTitle())) { 2585 dashboard = window; 2586 dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 2587 } 2588 } 2589 } else { 2590 dashboard.show(); 2591 } 2592 2593 if (tabName.isEmpty()) { 2594 return; 2595 } 2596 2597 // Dig two panels deep looking for a JTabbedPane 2598 // If you find one, try to show the requested tab name 2599 JComponent contents = dashboard.getContents(); 2600 JComponent component = (JComponent)contents.getComponent(0); 2601 JTabbedPane tPane = null; 2602 if (component instanceof JTabbedPane) { 2603 tPane = (JTabbedPane)component; 2604 } 2605 else { 2606 JComponent component2 = (JComponent)component.getComponent(0); 2607 if (component2 instanceof JTabbedPane) { 2608 tPane = (JTabbedPane)component2; 2609 } 2610 } 2611 if (tPane != null) { 2612 for (int i=0; i<tPane.getTabCount(); i++) { 2613 if (tabName.equals(tPane.getTitleAt(i))) { 2614 tPane.setSelectedIndex(i); 2615 break; 2616 } 2617 } 2618 } 2619 } 2620 2621 /** 2622 * Search through list of {@link IdvWindow IdvWindows} and return the {@literal "Data Explorer"}, 2623 * if it exists. 2624 * 2625 * @return {@literal "Data Explorer"} window or {@code null} if it does not exist. 2626 */ 2627 public IdvWindow getDashboardWindow() { 2628 IdvWindow dash = null; 2629 List<IdvWindow> windows = cast(IdvWindow.getWindows()); 2630 for (IdvWindow window : windows) { 2631 if (Constants.DATASELECTOR_NAME.equals(window.getTitle())) { 2632 dash = window; 2633 break; 2634 } 2635 } 2636 return dash; 2637 } 2638 2639 /** 2640 * Show the support request form 2641 * 2642 * @param description Default value for the description form entry 2643 * @param stackTrace The stack trace that caused this error. 2644 * @param dialog The dialog to put the gui in, if non-null. 2645 */ 2646 public void showSupportForm(final String description, 2647 final String stackTrace, final JDialog dialog) 2648 { 2649 java.awt.EventQueue.invokeLater(() -> { 2650 // TODO: mcvstatecollector should have a way to gather the 2651 // exception information.. 2652 McIDASV mcv = (McIDASV)getIdv(); 2653 new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true); 2654 }); 2655 } 2656 2657 /** 2658 * Attempts to locate and display a dashboard component using an ID. 2659 * 2660 * @param id ID of the desired component. 2661 * 2662 * @return True if {@code id} corresponds to a component. False otherwise. 2663 */ 2664 public boolean showDashboardComponent(String id) { 2665 Object comp = findComponent(id); 2666 if (comp != null) { 2667 GuiUtils.showComponentInTabs((JComponent)comp); 2668 return true; 2669 } else { 2670 super.showDashboard(); 2671 for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) { 2672 String title = makeTitle( 2673 getStateManager().getTitle(), 2674 Constants.DATASELECTOR_NAME 2675 ); 2676 if (title.equals(window.getTitle())) { 2677 dashboard = window; 2678 dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 2679 } 2680 } 2681 } 2682 return false; 2683 } 2684 2685 /** 2686 * Close and dispose of the splash window (if it has been created). 2687 */ 2688 @Override 2689 public void splashClose() { 2690 if (splash != null) { 2691 splash.doClose(); 2692 } 2693 } 2694 2695 /** 2696 * Show a message in the splash screen (if it exists) 2697 * 2698 * @param m The message to show 2699 */ 2700 @Override public void splashMsg(String m) { 2701 if (splash != null) { 2702 splash.splashMsg(m); 2703 } 2704 } 2705 2706 /** 2707 * Uses a given toolbar editor to repopulate all toolbars so that they 2708 * correspond to the user's choice of actions. 2709 * 2710 * @param tbe The toolbar editor that contains the actions the user wants. 2711 */ 2712 public void setCurrentToolbars(final McvToolbarEditor tbe) { 2713 List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries(); 2714 List<String> buttonIds = new ArrayList<>(tfos.size()); 2715 for (TwoFacedObject tfo : tfos) { 2716 if (McvToolbarEditor.isSpace(tfo)) { 2717 buttonIds.add(null); 2718 } else { 2719 buttonIds.add(TwoFacedObject.getIdString(tfo)); 2720 } 2721 } 2722 2723 cachedButtons = buttonIds; 2724 2725 for (JToolBar toolbar : toolbars) { 2726 toolbar.setVisible(false); 2727 populateToolbar(toolbar); 2728 toolbar.setVisible(true); 2729 } 2730 } 2731 2732 /** 2733 * Append a string and object to the buffer 2734 * 2735 * @param sb StringBuffer to append to 2736 * @param name Name of the object 2737 * @param value the object value 2738 */ 2739 private void append(StringBuffer sb, String name, Object value) { 2740 sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>"); 2741 } 2742 2743 private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) { 2744 JMenuItem mi = new JMenuItem(); 2745 if (cd != null) { 2746 mi = new JMenuItem(cd.getLabel()); 2747 mi.addActionListener(new ObjectListener(cd) { 2748 public void actionPerformed(ActionEvent ev) { 2749 idv.doMakeControl(new ArrayList(), 2750 (ControlDescriptor)theObject); 2751 } 2752 }); 2753 } 2754 return mi; 2755 } 2756 2757 /* (non-javadoc) 2758 * Overridden so that the toolbar will update upon saving a bundle. 2759 */ 2760 @Override public void displayTemplatesChanged() { 2761 super.displayTemplatesChanged(); 2762 for (JToolBar toolbar : toolbars) { 2763 toolbar.setVisible(false); 2764 populateToolbar(toolbar); 2765 toolbar.setVisible(true); 2766 } 2767 } 2768 2769 /** 2770 * Called when there has been any change to the favorite bundles and is 2771 * most useful for triggering an update to the {@literal "toolbar bundles"}. 2772 */ 2773 @Override public void favoriteBundlesChanged() { 2774 SwingUtilities.invokeLater(() -> { 2775 for (JToolBar toolbar : toolbars) { 2776 toolbar.setVisible(false); 2777 populateToolbar(toolbar); 2778 toolbar.setVisible(true); 2779 } 2780 }); 2781 } 2782 2783 /** 2784 * Show the support request form in a non-swing thread. We do this because we cannot 2785 * call the HttpFormEntry.showUI from a swing thread 2786 * 2787 * @param description Default value for the description form entry 2788 * @param stackTrace The stack trace that caused this error. 2789 * @param dialog The dialog to put the gui in, if non-null. 2790 */ 2791 2792 private void showSupportFormInThread(String description, 2793 String stackTrace, JDialog dialog) { 2794 List<HttpFormEntry> entries = new ArrayList<>(); 2795 2796 StringBuffer extra = new StringBuffer("<h3>McIDAS-V</h3>\n"); 2797 Hashtable<String, String> table = 2798 ((StateManager)getStateManager()).getVersionInfo(); 2799 append(extra, "mcv.version.general", table.get("mcv.version.general")); 2800 append(extra, "mcv.version.build", table.get("mcv.version.build")); 2801 append(extra, "idv.version.general", table.get("idv.version.general")); 2802 append(extra, "idv.version.build", table.get("idv.version.build")); 2803 2804 extra.append("<h3>OS</h3>\n"); 2805 append(extra, "os.name", System.getProperty("os.name")); 2806 append(extra, "os.arch", System.getProperty("os.arch")); 2807 append(extra, "os.version", System.getProperty("os.version")); 2808 2809 extra.append("<h3>Java</h3>\n"); 2810 append(extra, "java.vendor", System.getProperty("java.vendor")); 2811 append(extra, "java.version", System.getProperty("java.version")); 2812 append(extra, "java.home", System.getProperty("java.home")); 2813 2814 StringBuffer javaInfo = new StringBuffer(); 2815 javaInfo.append("Java: home: " + System.getProperty("java.home")); 2816 javaInfo.append(" version: " + System.getProperty("java.version")); 2817 2818 Class c = null; 2819 try { 2820 c = Class.forName("javax.media.j3d.VirtualUniverse"); 2821 Method method = Misc.findMethod(c, "getProperties", 2822 new Class[] {}); 2823 if (method == null) { 2824 javaInfo.append("j3d <1.3"); 2825 } else { 2826 try { 2827 Map m = (Map)method.invoke(c, new Object[] {}); 2828 javaInfo.append(" j3d:" + m.get("j3d.version")); 2829 append(extra, "j3d.version", m.get("j3d.version")); 2830 append(extra, "j3d.vendor", m.get("j3d.vendor")); 2831 append(extra, "j3d.renderer", m.get("j3d.renderer")); 2832 } catch (Exception exc) { 2833 javaInfo.append(" j3d:" + "unknown"); 2834 } 2835 } 2836 } catch (ClassNotFoundException exc) { 2837 append(extra, "j3d", "none"); 2838 } 2839 2840 boolean persistCC = getStore().get("mcv.supportreq.cc", true); 2841 2842 JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC); 2843 ccMyself.addActionListener(e -> { 2844 JCheckBox cb = (JCheckBox)e.getSource(); 2845 getStore().put("mcv.supportreq.cc", cb.isSelected()); 2846 }); 2847 2848 boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true); 2849 2850 HttpFormEntry descriptionEntry; 2851 HttpFormEntry nameEntry; 2852 HttpFormEntry emailEntry; 2853 HttpFormEntry orgEntry; 2854 2855 entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2856 "form_data[fromName]", "Name:", 2857 getStore().get(PROP_HELP_NAME, (String) null))); 2858 entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2859 "form_data[email]", "Your Email:", 2860 getStore().get(PROP_HELP_EMAIL, (String) null))); 2861 entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2862 "form_data[organization]", "Organization:", 2863 getStore().get(PROP_HELP_ORG, (String) null))); 2864 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT, 2865 "form_data[subject]", "Subject:")); 2866 2867 entries.add( 2868 new HttpFormEntry( 2869 HttpFormEntry.TYPE_LABEL, "", 2870 "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>")); 2871 entries.add(descriptionEntry = 2872 new FormEntry(doWrap, HttpFormEntry.TYPE_AREA, 2873 "form_data[description]", "Description:", 2874 description, 5, 30, true)); 2875 2876 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE, 2877 "form_data[att_two]", "Attachment 1:", "", 2878 false)); 2879 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE, 2880 "form_data[att_three]", "Attachment 2:", "", 2881 false)); 2882 2883 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2884 "form_data[submit]", "", "Send Email")); 2885 2886 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2887 "form_data[p_version]", "", 2888 getStateManager().getVersion() 2889 + " build date:" 2890 + getStateManager().getBuildDate())); 2891 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2892 "form_data[opsys]", "", 2893 System.getProperty("os.name"))); 2894 entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2895 "form_data[hardware]", "", 2896 javaInfo.toString())); 2897 2898 JLabel topLabel = 2899 new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>"); 2900 2901 JCheckBox includeBundleCbx = 2902 new JCheckBox("Include Current State as Bundle", false); 2903 2904 List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself); 2905 2906 boolean alreadyHaveDialog = true; 2907 if (dialog == null) { 2908 // NOTE: if the dialog is modeless you can leave alreadyHaveDialog 2909 // alone. If the dialog is modal you need to set alreadyHaveDialog 2910 // to false. 2911 // If alreadyHaveDialog is false with a modeless dialog, the later 2912 // call to HttpFormEntry.showUI will return false and break out of 2913 // the while loop without talking to the HTTP server. 2914 dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(), 2915 "Support Request Form", false); 2916// alreadyHaveDialog = false; 2917 } 2918 2919 JLabel statusLabel = GuiUtils.cLabel(" "); 2920 JComponent bottom = LayoutUtil.vbox(LayoutUtil.leftVbox(checkboxes), statusLabel); 2921 2922 while (true) { 2923 //Show form. Check if user pressed cancel. 2924 statusLabel.setText(" "); 2925 if ( !HttpFormEntry.showUI(entries, LayoutUtil.inset(topLabel, 10), 2926 bottom, dialog, alreadyHaveDialog)) { 2927 break; 2928 } 2929 statusLabel.setText("Posting support request..."); 2930 2931 //Save persistent state 2932 getStore().put(PROP_HELP_NAME, nameEntry.getValue()); 2933 getStore().put(PROP_HELP_ORG, orgEntry.getValue()); 2934 getStore().put(PROP_HELP_EMAIL, emailEntry.getValue()); 2935 getStore().save(); 2936 2937 List<HttpFormEntry> entriesToPost = 2938 new ArrayList<>(entries); 2939 2940 if ((stackTrace != null) && (stackTrace.length() > 0)) { 2941 entriesToPost.remove(descriptionEntry); 2942 String newDescription = 2943 descriptionEntry.getValue() 2944 + "\n\n******************\nStack trace:\n" + stackTrace; 2945 entriesToPost.add( 2946 new HttpFormEntry( 2947 HttpFormEntry.TYPE_HIDDEN, "form_data[description]", 2948 "Description:", newDescription, 5, 30, true)); 2949 } 2950 2951 try { 2952 extra.append(idv.getPluginManager().getPluginHtml()); 2953 extra.append(getResourceManager().getHtmlView()); 2954 2955 entriesToPost.add(new HttpFormEntry("form_data[att_extra]", 2956 "extra.html", extra.toString().getBytes())); 2957 2958 if (includeBundleCbx.isSelected()) { 2959 entriesToPost.add( 2960 new HttpFormEntry( 2961 "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV, 2962 idv.getPersistenceManager().getBundleXml( 2963 true).getBytes())); 2964 } 2965 entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 2966 "form_data[cc_user]", "", 2967 Boolean.toString(getStore().get("mcv.supportreq.cc", true)))); 2968 2969 String[] results = 2970 HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL); 2971 2972 if (results[0] != null) { 2973 GuiUtils.showHtmlDialog( 2974 results[0], "Support Request Response - Error", 2975 "Support Request Response - Error", null, true); 2976 continue; 2977 } 2978 String html = results[1]; 2979 if (html.toLowerCase().indexOf("your email has been sent") 2980 >= 0) { 2981 LogUtil.userMessage("Your support request has been sent"); 2982 break; 2983 } else if (html.toLowerCase().indexOf("required fields") 2984 >= 0) { 2985 LogUtil.userErrorMessage( 2986 "<html>There was a problem submitting your request. <br>Is your email correct?</html>"); 2987 } else { 2988 GuiUtils.showHtmlDialog( 2989 html, "Unknown Support Request Response", 2990 "Unknown Support Request Response", null, true); 2991 System.err.println(html.toLowerCase()); 2992 } 2993 } catch (Exception exc) { 2994 LogUtil.logException("Doing support request form", exc); 2995 } 2996 } 2997 dialog.dispose(); 2998 } 2999 3000 @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 3001 List viewManagers, Element skinRoot) 3002 { 3003 return new McIDASVXmlUi(window, viewManagers, idv, skinRoot); 3004 } 3005 3006 /** 3007 * DeInitialize the given menu before it is shown 3008 * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu) 3009 */ 3010 @Override 3011 protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) { 3012 super.handleMenuDeSelected(id, menu, idvWindow); 3013 } 3014 3015 /** 3016 * Initialize the given menu before it is shown 3017 * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu) 3018 */ 3019 @Override 3020 protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) { 3021 if (id.equals(MENU_NEWVIEWS)) { 3022 ViewManager last = getVMManager().getLastActiveViewManager(); 3023 menu.removeAll(); 3024 makeViewStateMenu(menu, last); 3025 } else if (id.equals("bundles")) { 3026 menu.removeAll(); 3027 makeBundleMenu(menu); 3028 } else if (id.equals(MENU_NEWDISPLAY_TAB)) { 3029 menu.removeAll(); 3030 doMakeNewDisplayMenu(menu, false); 3031 } else if (id.equals(MENU_NEWDISPLAY)) { 3032 menu.removeAll(); 3033 doMakeNewDisplayMenu(menu, true); 3034 } else if (id.equals("menu.tools.projections.deletesaved")) { 3035 menu.removeAll(); 3036 makeDeleteViewsMenu(menu); 3037 } else if (id.equals("file.default.layout")) { 3038 makeDefaultLayoutMenu(menu); 3039 } else if (id.equals("tools.formulas")) { 3040 menu.removeAll(); 3041 makeFormulasMenu(menu); 3042 } else { 3043 super.handleMenuSelected(id, menu, idvWindow); 3044 } 3045 } 3046 3047 /** A cache of the operand name to value for the user choices */ 3048 private Map operandCache; 3049 3050 @Override public List selectUserChoices(String msg, List userOperands) { 3051 if (operandCache == null) { 3052 operandCache = 3053 (Hashtable) getStore().getEncodedFile("operandcache.xml"); 3054 if (operandCache == null) { 3055 operandCache = new Hashtable(); 3056 } 3057 } 3058 List fields = new ArrayList(); 3059 List components = new ArrayList(); 3060 List persistentCbxs = new ArrayList(); 3061 components.add(new JLabel("Property")); 3062 components.add(new JLabel("Value")); 3063 components.add(new JLabel("Save in Bundle")); 3064 for (int i = 0; i < userOperands.size(); i++) { 3065 DataOperand operand = (DataOperand)userOperands.get(i); 3066 String fieldType = operand.getProperty("type"); 3067 if (fieldType == null) { 3068 fieldType = FIELDTYPE_TEXT; 3069 } 3070 DerivedDataChoice formula = operand.getDataChoice(); 3071 String description = operand.getDescription(); 3072 if (formula != null) { 3073 description = formula.toString(); 3074 } 3075 3076 String label = operand.getLabel(); 3077 Object dflt = operand.getUserDefault(); 3078 Object cacheKeyNewStyle = Misc.newList(description, label, fieldType); 3079 Object cacheKey = Misc.newList(label, fieldType); 3080 3081 Object cachedOperand = null; 3082 boolean oldStyle = operandCache.containsKey(cacheKey); 3083 boolean newStyle = operandCache.containsKey(cacheKeyNewStyle); 3084 3085 // if new style, always use that and ignore old style 3086 // if no new style, proceed as before. 3087 if (newStyle) { 3088 cachedOperand = operandCache.get(cacheKeyNewStyle); 3089 } else if (oldStyle) { 3090 cachedOperand = operandCache.get(cacheKey); 3091 } 3092 3093 if (cachedOperand != null) { 3094 dflt = cachedOperand; 3095 } 3096 3097 JCheckBox cbx = new JCheckBox("", operand.isPersistent()); 3098 persistentCbxs.add(cbx); 3099 JComponent field = null; 3100 JComponent fieldComp = null; 3101 if (fieldType.equals(FIELDTYPE_TEXT)) { 3102 String rowString = operand.getProperty("rows"); 3103 if (rowString == null) { 3104 rowString = "1"; 3105 } 3106 int rows = Integer.parseInt(rowString); 3107 if (rows == 1) { 3108 field = new JTextField((dflt != null) 3109 ? dflt.toString() 3110 : "", 15); 3111 } else { 3112 field = new JTextArea((dflt != null) 3113 ? dflt.toString() 3114 : "", rows, 15); 3115 fieldComp = GuiUtils.makeScrollPane(field, 200, 100); 3116 } 3117 } else if (fieldType.equals(FIELDTYPE_BOOLEAN)) { 3118 field = new JCheckBox("", ((dflt != null) 3119 ? new Boolean( 3120 dflt.toString()).booleanValue() 3121 : true)); 3122 } else if (fieldType.equals(FIELDTYPE_CHOICE)) { 3123 String choices = operand.getProperty("choices"); 3124 if (choices == null) { 3125 throw new IllegalArgumentException( 3126 "No 'choices' attribute defined for operand: " 3127 + operand); 3128 } 3129 List l = StringUtil.split(choices, ";", true, true); 3130 field = new JComboBox(new Vector(l)); 3131 if ((dflt != null) && l.contains(dflt)) { 3132 ((JComboBox) field).setSelectedItem(dflt); 3133 } 3134 } else if (fieldType.equals(FIELDTYPE_FILE)) { 3135 JTextField fileFld = new JTextField(((dflt != null) 3136 ? dflt.toString() 3137 : ""), 30); 3138 field = fileFld; 3139 String patterns = operand.getProperty("filepattern"); 3140 List filters = null; 3141 if (patterns != null) { 3142 filters = new ArrayList(); 3143 List toks = StringUtil.split(patterns, ";", true, true); 3144 for (int tokIdx = 0; tokIdx < toks.size(); tokIdx++) { 3145 String tok = (String) toks.get(tokIdx); 3146 List subToks = StringUtil.split(tok, ":", true, true); 3147 if (subToks.size() == 2) { 3148 filters.add( 3149 new PatternFileFilter( 3150 (String)subToks.get(0), 3151 (String)subToks.get(1))); 3152 } else { 3153 filters.add(new PatternFileFilter(tok, tok)); 3154 } 3155 } 3156 } 3157 fieldComp = GuiUtils.centerRight(GuiUtils.hfill(fileFld), 3158 GuiUtils.makeFileBrowseButton(fileFld, filters)); 3159 } else if (fieldType.equals(FIELDTYPE_LOCATION)) { 3160 List l = ((dflt != null) 3161 ? StringUtil.split(dflt.toString(), ";", true, true) 3162 : (List) new ArrayList()); 3163 final LatLonWidget llw = new LatLonWidget(); 3164 field = llw; 3165 if (l.size() == 2) { 3166 llw.setLat(Misc.decodeLatLon(l.get(0).toString())); 3167 llw.setLon(Misc.decodeLatLon(l.get(1).toString())); 3168 } 3169 final JButton centerPopupBtn = 3170 GuiUtils.getImageButton("/auxdata/ui/icons/Map16.gif", 3171 getClass()); 3172 centerPopupBtn.setToolTipText("Center on current displays"); 3173 centerPopupBtn.addActionListener(ae -> popupCenterMenu(centerPopupBtn, llw)); 3174 JComponent centerPopup = GuiUtils.inset(centerPopupBtn, 3175 new Insets(0, 0, 0, 4)); 3176 fieldComp = GuiUtils.hbox(llw, centerPopup); 3177 } else if (fieldType.equals(FIELDTYPE_AREA)) { 3178 //TODO: 3179 } else { 3180 throw new IllegalArgumentException("Unknown type: " 3181 + fieldType + " for operand: " + operand); 3182 } 3183 3184 fields.add(field); 3185 label = StringUtil.replace(label, "_", " "); 3186 components.add(GuiUtils.rLabel(label)); 3187 components.add((fieldComp != null) 3188 ? fieldComp 3189 : field); 3190 components.add(cbx); 3191 } 3192 // GuiUtils.tmpColFills = new int[] { GridBagConstraints.HORIZONTAL, 3193 // GridBagConstraints.NONE, 3194 // GridBagConstraints.NONE }; 3195 GuiUtils.tmpInsets = GuiUtils.INSETS_5; 3196 Component contents = GuiUtils.topCenter(new JLabel(msg), 3197 GuiUtils.doLayout(components, 3, 3198 GuiUtils.WT_NYN, GuiUtils.WT_N)); 3199 if ( !GuiUtils.showOkCancelDialog(null, "Select input", contents, 3200 null, fields)) { 3201 return null; 3202 } 3203 List values = new ArrayList(); 3204 for (int i = 0; i < userOperands.size(); i++) { 3205 DataOperand operand = (DataOperand) userOperands.get(i); 3206 String description = operand.getDescription(); 3207 DerivedDataChoice formula = operand.getDataChoice(); 3208 String label = operand.getLabel(); 3209 Object field = fields.get(i); 3210 Object value = null; 3211 Object cacheValue = null; 3212 3213 if (formula != null) { 3214 description = formula.toString(); 3215 } 3216 3217 if (field instanceof JTextComponent) { 3218 value = ((JTextComponent) field).getText().trim(); 3219 } else if (field instanceof JCheckBox) { 3220 value = Boolean.valueOf(((JCheckBox)field).isSelected()); 3221 } else if (field instanceof JComboBox) { 3222 value = ((JComboBox) field).getSelectedItem(); 3223 } else if (field instanceof LatLonWidget) { 3224 LatLonWidget llw = (LatLonWidget) field; 3225 value = new LatLonPointImpl(llw.getLat(), llw.getLon()); 3226 cacheValue = llw.getLat() + ";" + llw.getLon(); 3227 } else { 3228 throw new IllegalArgumentException("Unknown field type:" 3229 + field.getClass().getName()); 3230 } 3231 if (cacheValue == null) { 3232 cacheValue = value; 3233 } 3234 JCheckBox cbx = (JCheckBox)persistentCbxs.get(i); 3235 String fieldType = operand.getProperty("type"); 3236 if (fieldType == null) { 3237 fieldType = "text"; 3238 } 3239 3240 Object cacheKey = Misc.newList(description, label, fieldType); 3241 operandCache.put(cacheKey, cacheValue); 3242 values.add(new UserOperandValue(value, cbx.isSelected())); 3243 } 3244 getStore().putEncodedFile("operandcache.xml", operandCache); 3245 return values; 3246 } 3247 3248 private boolean didTabs = false; 3249 private boolean didNewWindow = false; 3250 3251 public void makeDefaultLayoutMenu(final JMenu menu) { 3252 if (menu == null) 3253 throw new NullPointerException("Must provide a non-null default layout menu"); 3254 3255 menu.removeAll(); 3256 JMenuItem saveLayout = new JMenuItem("Save"); 3257 McVGuiUtils.setMenuImage(saveLayout, Constants.ICON_DEFAULTLAYOUTADD_SMALL); 3258 saveLayout.setToolTipText("Save as default layout"); 3259 saveLayout.addActionListener(e -> ((McIDASV)idv).doSaveAsDefaultLayout()); 3260 3261 JMenuItem removeLayout = new JMenuItem("Remove"); 3262 McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL); 3263 removeLayout.setToolTipText("Remove saved default layout"); 3264 removeLayout.addActionListener(e -> idv.doClearDefaults()); 3265 3266 removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout()); 3267 3268 menu.add(saveLayout); 3269 menu.add(removeLayout); 3270 } 3271 3272 /** 3273 * Bundles any compatible {@link ViewManager} states into 3274 * {@link JMenuItem JMenuItem} and adds said menu items to {@code menu}. 3275 * Incompatible states are ignored. 3276 * 3277 * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"} 3278 * menu--apologies) associates a {@literal "view state"} and an 3279 * {@link ObjectListener}. The {@code ObjectListener} uses this associated 3280 * view state to attempt reinitialization of {@code vm}. 3281 * 3282 * <p>Override reasoning: 3283 * <ul> 3284 * <li> 3285 * terminology ({@literal "views"} rather than {@literal "viewpoints"}). 3286 * </li> 3287 * <li> 3288 * use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to 3289 * properly detect the {@literal "no saved views"} case. 3290 * </li> 3291 * </ul> 3292 * 3293 * @param menu Menu to populate. Should not be {@code null}. 3294 * @param vm {@code ViewManager} that might get reinitialized. 3295 * Should not be {@code null}. 3296 * 3297 * @see ViewManager#initWith(ViewManager, boolean) 3298 * @see ViewManager#initWith(ViewState) 3299 */ 3300 @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) { 3301 List<TwoFacedObject> vmStates = 3302 filterVMMStatesWithVM(vm, getVMManager().getVMState()); 3303 if (vmStates.isEmpty()) { 3304 JMenuItem item = new JMenuItem(Msg.msg("No Saved Views")); 3305 item.setEnabled(false); 3306 menu.add(item); 3307 } else { 3308 JMenu deleteMenu = new JMenu("Delete"); 3309 makeDeleteViewsMenu(deleteMenu); 3310 menu.add(deleteMenu); 3311 } 3312 3313 // McIDAS Inquiry #2803-3141 3314 ArrayList<TwoFacedObject> notInDirs = new ArrayList<>(); 3315 HashMap<String, JMenu> dirs = new HashMap<>(); 3316 3317 for (int i = 0; i < vmStates.size(); i++) { 3318 TwoFacedObject tfo = vmStates.get(i); 3319 3320 if (tfo.getLabel().toString().contains(">")) { 3321 String dirStr = tfo.getLabel().toString(); 3322 String[] dirAsArray = dirStr.split(">"); 3323 3324 if (dirAsArray.length > 2) { 3325 String tgt = ""; 3326 for (int split = 1; split < dirAsArray.length; split++) { 3327 tgt += dirAsArray[split] + ">"; 3328 } 3329 dirAsArray[1] = tgt; 3330 } 3331 3332 JMenu tmp; 3333 if (dirs.containsKey(dirAsArray[0])) { 3334 tmp = dirs.get(dirAsArray[0]); 3335 } else { 3336 tmp = new JMenu(dirAsArray[0]); 3337 } 3338 JMenuItem mi = new JMenuItem(dirAsArray[1]); 3339 tmp.add(mi); 3340 dirs.put(dirAsArray[0], tmp); 3341 3342 mi.addActionListener(new ObjectListener(tfo.getId()) { 3343 public void actionPerformed(final ActionEvent e) { 3344 if (vm == null) { 3345 return; 3346 } 3347 if (theObject instanceof ViewManager) { 3348 vm.initWith((ViewManager)theObject, true); 3349 } else if (theObject instanceof ViewState) { 3350 try { 3351 vm.initWith((ViewState)theObject); 3352 } catch (Throwable ex) { 3353 logException("Initializing view with ViewState", ex); 3354 } 3355 } else { 3356 LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName()); 3357 } 3358 } 3359 }); 3360 3361 } else { 3362 notInDirs.add(tfo); 3363 } 3364 } 3365 3366 ArrayList<JMenu> folders = new ArrayList<>(); 3367 3368 for (Map.Entry<String, JMenu> entry : dirs.entrySet()) { 3369 JMenu jmu = entry.getValue(); 3370 ArrayList<JMenuItem> itms = new ArrayList<>(); 3371 3372 for (int i = 0; i < jmu.getItemCount(); i++) { 3373 itms.add(jmu.getItem(i)); 3374 } 3375 3376 Collections.sort(itms, new Comparator<JMenuItem>() { 3377 @Override 3378 public int compare(JMenuItem o1, JMenuItem o2) { 3379 return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase())); 3380 } 3381 }); 3382 3383 JMenu out = new JMenu(jmu.getText()); 3384 3385 for (int i = 0; i < itms.size(); i++) { 3386 out.add(itms.get(i)); 3387 } 3388 3389 folders.add(out); 3390 } 3391 3392 Collections.sort(folders, new Comparator<JMenu>() { 3393 @Override 3394 public int compare(JMenu o1, JMenu o2) { 3395 return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase())); 3396 } 3397 }); 3398 3399 for (int i = 0; i < folders.size(); i++) { 3400 menu.add(folders.get(i)); 3401 } 3402 3403 if (folders.size() > 0) { 3404 menu.addSeparator(); 3405 } 3406 3407 Collections.sort(notInDirs, new Comparator<TwoFacedObject>() { 3408 @Override 3409 public int compare(TwoFacedObject a, TwoFacedObject b) { 3410 return (a.getLabel().toString().toLowerCase().compareTo(b.getLabel().toString().toLowerCase())); 3411 } 3412 }); 3413 3414 for (TwoFacedObject tfo : notInDirs) { 3415 JMenuItem mi = new JMenuItem(tfo.getLabel().toString()); 3416 menu.add(mi); 3417 mi.addActionListener(new ObjectListener(tfo.getId()) { 3418 public void actionPerformed(final ActionEvent e) { 3419 if (vm == null) { 3420 return; 3421 } 3422 3423 if (theObject instanceof ViewManager) { 3424 vm.initWith((ViewManager)theObject, true); 3425 } else if (theObject instanceof ViewState) { 3426 try { 3427 vm.initWith((ViewState)theObject); 3428 } catch (Throwable ex) { 3429 logException("Initializing view with ViewState", ex); 3430 } 3431 } else { 3432 LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName()); 3433 } 3434 } 3435 }); 3436 } 3437 3438 // the "3" ensures that the "save viewpoint" menu item, the separator, 3439 // and the "delete" menu item are fixed at the top. 3440 new MenuScroller(menu, menu, 125, 3); 3441 } 3442 3443 /** 3444 * Overridden by McIDAS-V to add menu scrolling functionality to the 3445 * {@literal "delete"} submenu. 3446 * 3447 * @param menu {@literal "Delete"} submenu. 3448 */ 3449 @Override public void makeDeleteViewsMenu(JMenu menu) { 3450 super.makeDeleteViewsMenu(menu); 3451 new MenuScroller(menu, menu, 125); 3452 } 3453 3454 /** 3455 * Returns a list of {@link TwoFacedObject}s that are known to be 3456 * compatible with {@code vm}. 3457 * 3458 * <p>This method is currently capable of dealing with 3459 * {@link TwoFacedObject TwoFacedObjects} and 3460 * {@link ViewState ViewStates} within {@code states}. Any other types are 3461 * ignored. 3462 * 3463 * @param vm {@link ViewManager} to use for compatibility tests. 3464 * {@code null} is allowed. 3465 * @param states Collection of objects to test against {@code vm}. 3466 * {@code null} is allowed. 3467 * 3468 * @return Either a {@link List} of compatible {@literal "view states"} 3469 * or an empty {@code List}. 3470 * 3471 * @see ViewManager#isCompatibleWith(ViewManager) 3472 * @see ViewManager#isCompatibleWith(ViewState) 3473 * @see #makeViewStateMenu(JMenu, ViewManager) 3474 */ 3475 public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) { 3476 if ((vm == null) || (states == null) || states.isEmpty()) { 3477 return Collections.emptyList(); 3478 } 3479 3480 List<TwoFacedObject> validStates = new ArrayList<>(states.size()); 3481 for (Object obj : states) { 3482 TwoFacedObject tfo = null; 3483 if (obj instanceof TwoFacedObject) { 3484 tfo = (TwoFacedObject)obj; 3485 if (vm.isCompatibleWith((ViewManager)tfo.getId())) { 3486 continue; 3487 } 3488 } else if (obj instanceof ViewState) { 3489 if (!vm.isCompatibleWith((ViewState)obj)) { 3490 continue; 3491 } 3492 tfo = new TwoFacedObject(((ViewState)obj).getName(), obj); 3493 } else { 3494 LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName()); 3495 continue; 3496 } 3497 validStates.add(tfo); 3498 } 3499 return validStates; 3500 } 3501 3502 /** 3503 * Overridden to build a custom Display menu. 3504 * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu) 3505 */ 3506 @Override protected void initializeDisplayMenu(JMenu displayMenu) { 3507 JMenu m; 3508 JMenuItem mi; 3509 3510 // Get the list of possible standalone control descriptors 3511 Hashtable controlsHash = new Hashtable(); 3512 List controlDescriptors = getStandAloneControlDescriptors(); 3513 for (int i = 0; i < controlDescriptors.size(); i++) { 3514 ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i); 3515 String cdLabel = cd.getLabel(); 3516 if (cdLabel.equals("Range Rings")) { 3517 controlsHash.put(cdLabel, cd); 3518 } else if (cdLabel.equals("Range and Bearing")) { 3519 controlsHash.put(cdLabel, cd); 3520 } else if (cdLabel.equals("Location Indicator")) { 3521 controlsHash.put(cdLabel, cd); 3522 } else if (cdLabel.equals("Drawing Control")) { 3523 controlsHash.put(cdLabel, cd); 3524 } else if (cdLabel.equals("Transect Drawing Control")) { 3525 controlsHash.put(cdLabel, cd); 3526 } 3527 } 3528 3529 // Build the menu 3530 ControlDescriptor cd; 3531 3532 mi = new JMenuItem("Create Layer from Data Source..."); 3533 mi.addActionListener(ae -> showDashboard("Data Sources")); 3534 displayMenu.add(mi); 3535 3536 mi = new JMenuItem("Layer Controls..."); 3537 mi.addActionListener(ae -> showDashboard("Layer Controls")); 3538 displayMenu.add(mi); 3539 3540 displayMenu.addSeparator(); 3541 3542 cd = (ControlDescriptor)controlsHash.get("Range Rings"); 3543 mi = makeControlDescriptorItem(cd); 3544 mi.setText("Add Range Rings"); 3545 displayMenu.add(mi); 3546 3547 cd = (ControlDescriptor)controlsHash.get("Range and Bearing"); 3548 mi = makeControlDescriptorItem(cd); 3549 McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL); 3550 mi.setText("Add Range and Bearing"); 3551 displayMenu.add(mi); 3552 3553 displayMenu.addSeparator(); 3554 3555 cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control"); 3556 mi = makeControlDescriptorItem(cd); 3557 mi.setText("Draw Transect..."); 3558 displayMenu.add(mi); 3559 3560 cd = (ControlDescriptor)controlsHash.get("Drawing Control"); 3561 mi = makeControlDescriptorItem(cd); 3562 mi.setText("Draw Freely..."); 3563 displayMenu.add(mi); 3564 3565 displayMenu.addSeparator(); 3566 3567 cd = (ControlDescriptor)controlsHash.get("Location Indicator"); 3568 mi = makeControlDescriptorItem(cd); 3569 McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL); 3570 mi.setText("Add Location Indicator"); 3571 displayMenu.add(mi); 3572 3573 ControlDescriptor locationDescriptor = idv.getControlDescriptor("locationcontrol"); 3574 if (locationDescriptor != null) { 3575 List stations = idv.getLocationList(); 3576 ObjectListener listener = new ObjectListener(locationDescriptor) { 3577 public void actionPerformed(ActionEvent ae, Object obj) { 3578 addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject); 3579 } 3580 }; 3581 List menuItems = NamedStationTable.makeMenuItems(stations, listener); 3582 displayMenu.add(MenuUtil.makeMenu("Plot Location Labels", menuItems)); 3583 } 3584 3585 displayMenu.addSeparator(); 3586 3587 mi = new JMenuItem("Add Background Image"); 3588 McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL); 3589 mi.addActionListener(ae -> getIdv().doMakeBackgroundImage()); 3590 displayMenu.add(mi); 3591 3592 mi = new JMenuItem("Reset Map Layer to Defaults"); 3593 mi.addActionListener(ae -> { 3594 // TODO: Call IdvUIManager.addDefaultMap()... should be made private 3595// addDefaultMap(); 3596 ControlDescriptor mapDescriptor = idv.getControlDescriptor("mapdisplay"); 3597 if (mapDescriptor == null) { 3598 return; 3599 } 3600 String attrs = "initializeAsDefault=true;displayName=Default Background Maps;"; 3601 idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null); 3602 }); 3603 displayMenu.add(mi); 3604 Msg.translateTree(displayMenu); 3605 } 3606 3607 /** 3608 * Get the window title from the skin 3609 * 3610 * @param index the skin index 3611 * 3612 * @return the title 3613 */ 3614 private String getWindowTitleFromSkin(final int index) { 3615 if (!skinToTitle.containsKey(index)) { 3616 IdvResourceManager mngr = getResourceManager(); 3617 XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN); 3618 List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true); 3619 String title = getStateManager().getTitle(); 3620 if (!names.isEmpty()) { 3621 title = title + " - " + StringUtil.join(" - ", names); 3622 } 3623 skinToTitle.put(index, title); 3624 } 3625 return skinToTitle.get(index); 3626 } 3627 3628 @SuppressWarnings("unchecked") 3629 @Override public Hashtable getMenuIds() { 3630 return menuIds; 3631 } 3632 3633 @SuppressWarnings("unchecked") 3634 @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) { 3635 Hashtable<String, JMenuItem> menuMap = new Hashtable<>(); 3636 JMenuBar menuBar = new JMenuBar(); 3637 final IdvResourceManager mngr = getResourceManager(); 3638 XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR); 3639 Hashtable<String, ImageIcon> actionIcons = new Hashtable<>(); 3640 3641 for (int i = 0; i < xrc.size(); i++) { 3642 GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons); 3643 } 3644 3645 menuIds = new Hashtable<>(menuMap); 3646 3647 // Ensure that the "help" menu is the last menu. 3648 JMenuItem helpMenu = menuMap.get(MENU_HELP); 3649 if (helpMenu != null) { 3650 menuBar.remove(helpMenu); 3651 menuBar.add(helpMenu); 3652 } 3653 3654 //TODO: Perhaps we will put the different skins in the menu? 3655 JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY); 3656 if (newDisplayMenu != null) { 3657 MenuUtil.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false)); 3658 } 3659 3660// final JMenu publishMenu = menuMap.get(MENU_PUBLISH); 3661// if (publishMenu != null) { 3662// if (!getPublishManager().isPublishingEnabled()) 3663// publishMenu.getParent().remove(publishMenu); 3664// else 3665// getPublishManager().initMenu(publishMenu); 3666// } 3667 3668 for (Entry<String, JMenuItem> e : menuMap.entrySet()) { 3669 if (!(e.getValue() instanceof JMenu)) { 3670 continue; 3671 } 3672 String menuId = e.getKey(); 3673 JMenu menu = (JMenu)e.getValue(); 3674 menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow)); 3675 } 3676 return menuBar; 3677 } 3678 3679 private final ActionListener makeMenuBarActionListener() { 3680 final IdvResourceManager mngr = getResourceManager(); 3681 return ae -> { 3682 XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN); 3683 int skinIndex = ((Integer)ae.getSource()).intValue(); 3684 createNewWindow(null, true, getWindowTitleFromSkin(skinIndex), 3685 skins.get(skinIndex).toString(), 3686 skins.getRoot(skinIndex, false), true, null); 3687 }; 3688 } 3689 3690 private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) { 3691 return new MenuListener() { 3692 public void menuCanceled(final MenuEvent e) { } 3693 public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); } 3694 public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); } 3695 }; 3696 } 3697 3698 /** 3699 * Handle mouse clicks that occur within the toolbar. 3700 */ 3701 private class PopupListener extends MouseAdapter { 3702 3703 private JPopupMenu popup; 3704 3705 public PopupListener(JPopupMenu p) { 3706 popup = p; 3707 } 3708 3709 // handle right clicks on os x and linux 3710 public void mousePressed(MouseEvent e) { 3711 if (e.isPopupTrigger()) { 3712 popup.show(e.getComponent(), e.getX(), e.getY()); 3713 } 3714 } 3715 3716 // Windows doesn't seem to trigger mousePressed() for right clicks, but 3717 // never fear; mouseReleased() does the job. 3718 public void mouseReleased(MouseEvent e) { 3719 if (e.isPopupTrigger()) { 3720 popup.show(e.getComponent(), e.getX(), e.getY()); 3721 } 3722 } 3723 } 3724 3725 /** 3726 * Handle (polymorphically) the {@link DataControlDialog}. 3727 * 3728 * This dialog is used to either select a display control to create 3729 * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}. 3730 * 3731 * @param dcd The dialog 3732 */ 3733 public void processDialog(DataControlDialog dcd) { 3734 int estimatedMB = getEstimatedMegabytes(dcd); 3735 if (estimatedMB > 0) { 3736 double totalMem = Runtime.getRuntime().maxMemory(); 3737 double highMem = Runtime.getRuntime().totalMemory(); 3738 double freeMem = Runtime.getRuntime().freeMemory(); 3739 double usedMem = (highMem - freeMem); 3740 int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f); 3741 int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f); 3742 if (percentOfAvailable > 95) { 3743 String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>"; 3744 message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>"; 3745 message += "Data load cancelled.</html>"; 3746 JComponent msgLabel = new JLabel(message); 3747 GuiUtils.showDialog("Data Size", msgLabel); 3748 return; 3749 } else if (percentOfAvailable >= 75) { 3750 String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>"; 3751 message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>"; 3752 message += "Continue loading data?</html>"; 3753 JComponent msgLabel = new JLabel(message); 3754 if (!GuiUtils.askOkCancel("Data Size", msgLabel)) { 3755 return; 3756 } 3757 } 3758 } 3759 super.processDialog(dcd); 3760 } 3761 3762 /** 3763 * Estimate the number of megabytes that will be used by this data selection 3764 * 3765 * @param dcd Data control dialog containing the data selection whose size 3766 * we'd like to estimate. Cannot be {@code null}. 3767 * 3768 * @return Rough estimate of the size (in megabytes) of the 3769 * {@code DataSelection} within {@code dcd}. 3770 */ 3771 protected int getEstimatedMegabytes(DataControlDialog dcd) { 3772 int estimatedMB = 0; 3773 DataChoice dataChoice = dcd.getDataChoice(); 3774 if (dataChoice != null) { 3775 Object[] selectedControls = dcd.getSelectedControls(); 3776 for (int i = 0; i < selectedControls.length; i++) { 3777 ControlDescriptor cd = (ControlDescriptor) selectedControls[i]; 3778 3779 //Check if the data selection is ok 3780 if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) { 3781 continue; 3782 } 3783 3784 DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels()); 3785 3786 // Get the size in pixels of the requested image 3787 Object gotSize = dataSelection.getProperty("SIZE"); 3788 if (gotSize == null) { 3789 continue; 3790 } 3791 List<String> dims = StringUtil.split(gotSize, " ", false, false); 3792 int myLines = -1; 3793 int myElements = -1; 3794 if (dims.size() == 2) { 3795 try { 3796 myLines = Integer.parseInt(dims.get(0)); 3797 myElements = Integer.parseInt(dims.get(1)); 3798 } 3799 catch (Exception e) { } 3800 } 3801 3802 // Get the count of times requested 3803 int timeCount = 1; 3804 DataSelectionWidget dsw = dcd.getDataSelectionWidget(); 3805 List times = dsw.getSelectedDateTimes(); 3806 List timesAll = dsw.getAllDateTimes(); 3807 if ((times != null) && !times.isEmpty()) { 3808 timeCount = times.size(); 3809 } else if ((timesAll != null) && !timesAll.isEmpty()) { 3810 timeCount = timesAll.size(); 3811 } 3812 3813 // Total number of pixels 3814 // Assumed lines x elements x times x 4bytes 3815 // Empirically seems to be taking *twice* that (64bit fields??) 3816 float totalPixels = (float)myLines * (float)myElements * (float)timeCount; 3817 float totalBytes = totalPixels * 4 * 2; 3818 estimatedMB += Math.round(totalBytes / 1024f / 1024f); 3819 3820 int additionalMB = 0; 3821 // Empirical tests show that textures are not affecting 3822 // required memory... comment out for now 3823 /* 3824 int textureDimensions = 2048; 3825 int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f); 3826 int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f); 3827 int additionalMB = textureCount * mbPerTexture * timeCount; 3828 */ 3829 estimatedMB += additionalMB; 3830 } 3831 } 3832 return estimatedMB; 3833 } 3834 3835 /** 3836 * Represents a SavedBundle as a tree. 3837 */ 3838 private class BundleTreeNode { 3839 3840 private String name; 3841 3842 private SavedBundle bundle; 3843 3844 private List<BundleTreeNode> kids; 3845 3846 /** 3847 * This constructor is used to build a node that is considered a 3848 * {@literal "parent"}. 3849 * 3850 * These nodes only have child nodes, no {@code SavedBundles}. This 3851 * was done so that distinguishing between bundles and bundle 3852 * subcategories would be easy. 3853 * 3854 * @param name The name of this node. For a parent node with 3855 * {@literal "Toolbar>cat"} as the path, the name parameter would 3856 * contain only {@literal "cat"}. 3857 */ 3858 public BundleTreeNode(String name) { 3859 this(name, null); 3860 } 3861 3862 /** 3863 * Nodes constructed using this constructor can only ever be child 3864 * nodes. 3865 * 3866 * @param name The name of the SavedBundle. 3867 * @param bundle A reference to the SavedBundle. 3868 */ 3869 public BundleTreeNode(String name, SavedBundle bundle) { 3870 this.name = name; 3871 this.bundle = bundle; 3872 kids = new LinkedList<>(); 3873 } 3874 3875 /** 3876 * @param child The node to be added to the current node. 3877 */ 3878 public void addChild(BundleTreeNode child) { 3879 kids.add(child); 3880 } 3881 3882 /** 3883 * @return Returns all child nodes of this node. 3884 */ 3885 public List<BundleTreeNode> getChildren() { 3886 return kids; 3887 } 3888 3889 /** 3890 * @return Return the SavedBundle associated with this node (if any). 3891 */ 3892 public SavedBundle getBundle() { 3893 return bundle; 3894 } 3895 3896 /** 3897 * @return The name of this node. 3898 */ 3899 public String getName() { 3900 return name; 3901 } 3902 } 3903 3904 /** 3905 * A type of {@code HttpFormEntry} that supports line wrapping for 3906 * text area entries. 3907 * 3908 * @see HttpFormEntry 3909 */ 3910 private static class FormEntry extends HttpFormEntry { 3911 /** Initial contents of this entry. */ 3912 private String value = ""; 3913 3914 /** Whether or not the JTextArea should wrap lines. */ 3915 private boolean wrap = true; 3916 3917 /** Entry type. Used to remain compatible with the IDV. */ 3918 private int type = HttpFormEntry.TYPE_AREA; 3919 3920 /** Number of rows in the JTextArea. */ 3921 private int rows = 5; 3922 3923 /** Number of columns in the JTextArea. */ 3924 private int cols = 30; 3925 3926 /** GUI representation of this entry. */ 3927 private JTextArea component = new JTextArea(value, rows, cols); 3928 3929 /** 3930 * Required to keep Java happy. 3931 */ 3932 public FormEntry() { 3933 super(HttpFormEntry.TYPE_AREA, "form_data[description]", 3934 "Description:"); 3935 } 3936 3937 /** 3938 * Using this constructor allows McIDAS-V to control whether or not a 3939 * HttpFormEntry performs line wrapping for JTextArea components. 3940 * 3941 * @param wrap Whether or not line wrapping should be enabled. 3942 * @param type Type of this entry 3943 * @param name Name 3944 * @param label Label 3945 * @param value Initial value 3946 * @param rows Number of rows. 3947 * @param cols Number of columns. 3948 * @param required Whether or not the entry will be required. 3949 */ 3950 public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) { 3951 super(type, name, label, value, rows, cols, required); 3952 this.type = type; 3953 this.rows = rows; 3954 this.cols = cols; 3955 this.wrap = wrap; 3956 } 3957 3958 /** 3959 * Overrides the IDV method so that the McIDAS-V support request form 3960 * will wrap lines in the "Description" field. 3961 * 3962 * @param guiComps List to which this instance should be added. 3963 */ 3964 @SuppressWarnings("unchecked") 3965 @Override public void addToGui(List guiComps) { 3966 if (type == HttpFormEntry.TYPE_AREA) { 3967 Dimension minSize = new Dimension(500, 200); 3968 guiComps.add(LayoutUtil.top(GuiUtils.rLabel(getLabel()))); 3969 component.setLineWrap(wrap); 3970 component.setWrapStyleWord(wrap); 3971 JScrollPane sp = new JScrollPane(component); 3972 sp.setPreferredSize(minSize); 3973 sp.setMinimumSize(minSize); 3974 guiComps.add(sp); 3975 } else { 3976 super.addToGui(guiComps); 3977 } 3978 } 3979 3980 /** 3981 * Since the IDV doesn't provide a getComponent for 3982 * {@code addToGui}, we must make our {@code component} field 3983 * local to this class. 3984 * Hijacks any value requests so that the local {@code component} 3985 * field is queried, not the IDV's. 3986 * 3987 * @return Contents of form. 3988 */ 3989 @Override public String getValue() { 3990 if (type != HttpFormEntry.TYPE_AREA) { 3991 return super.getValue(); 3992 } 3993 return component.getText(); 3994 } 3995 3996 /** 3997 * Hijacks any requests to set the {@code component} field's text. 3998 */ 3999 @Override public void setValue(final String newValue) { 4000 if (type == HttpFormEntry.TYPE_AREA) { 4001 component.setText(newValue); 4002 } else { 4003 super.setValue(newValue); 4004 } 4005 } 4006 } 4007 4008 /** 4009 * A {@code ToolbarStyle} is a representation of the way icons associated 4010 * with current toolbar actions should be displayed. This notion is so far 4011 * limited to the sizing of icons, but that may change. 4012 */ 4013 public enum ToolbarStyle { 4014 /** 4015 * Represents the current toolbar actions as large icons. Currently, 4016 * {@literal "large"} is defined as {@code 32 x 32} pixels. 4017 */ 4018 LARGE("Large Icons", "action.icons.large", 32), 4019 4020 /** 4021 * Represents the current toolbar actions as medium icons. Currently, 4022 * {@literal "medium"} is defined as {@code 22 x 22} pixels. 4023 */ 4024 MEDIUM("Medium Icons", "action.icons.medium", 22), 4025 4026 /** 4027 * Represents the current toolbar actions as small icons. Currently, 4028 * {@literal "small"} is defined as {@code 16 x 16} pixels. 4029 */ 4030 SMALL("Small Icons", "action.icons.small", 16); 4031 4032 /** Label to use in the toolbar customization popup menu. */ 4033 private final String label; 4034 4035 /** Signals that the user selected a specific icon size. */ 4036 private final String action; 4037 4038 /** Icon dimensions. Each icon should be {@code size * size}. */ 4039 private final int size; 4040 4041 /** 4042 * {@link #size} in {@link String} form, merely for use with the IDV's 4043 * preference functionality. 4044 */ 4045 private final String sizeAsString; 4046 4047 /** 4048 * Initializes a toolbar style. 4049 * 4050 * @param label Label used in the toolbar popup menu. 4051 * @param action Command that signals the user selected this toolbar 4052 * style. 4053 * @param size Dimensions of the icons. 4054 * 4055 * @throws NullPointerException if {@code label} or {@code action} are 4056 * null. 4057 * 4058 * @throws IllegalArgumentException if {@code size} is not positive. 4059 */ 4060 ToolbarStyle(final String label, final String action, final int size) { 4061 requireNonNull(label, "Label cannot be null."); 4062 requireNonNull(action, "Action cannot be null."); 4063 4064 if (size <= 0) { 4065 throw new IllegalArgumentException("Size must be a positive integer"); 4066 } 4067 4068 this.label = label; 4069 this.action = action; 4070 this.size = size; 4071 this.sizeAsString = Integer.toString(size); 4072 } 4073 4074 /** 4075 * Returns the label to use as a brief description of this style. 4076 * 4077 * @return Description of style (suitable for a label). 4078 */ 4079 public String getLabel() { 4080 return label; 4081 } 4082 4083 /** 4084 * Returns the action command associated with this style. 4085 * 4086 * @return This style's {@literal "action command"}. 4087 */ 4088 public String getAction() { 4089 return action; 4090 } 4091 4092 /** 4093 * Returns the dimensions of icons used in this style. 4094 * 4095 * @return Dimensions of this style's icons. 4096 */ 4097 public int getSize() { 4098 return size; 4099 } 4100 4101 /** 4102 * Returns {@link #size} as a {@link String} to make cooperating with 4103 * the IDV preferences code easier. 4104 * 4105 * @return String representation of this style's icon dimensions. 4106 */ 4107 public String getSizeAsString() { 4108 return sizeAsString; 4109 } 4110 4111 /** 4112 * Returns a brief description of this {@code ToolbarStyle}. 4113 * 4114 * <p>A typical example: 4115 * {@code [ToolbarStyle@1337: label="Large Icons", size=32]} 4116 * 4117 * <p>Note that the format and details provided are subject to change. 4118 * 4119 * @return String representation of this {@code ToolbarStyle} instance. 4120 */ 4121 public String toString() { 4122 return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 4123 hashCode(), label, size); 4124 } 4125 4126 /** 4127 * Convenience method for build the toolbar customization popup menu. 4128 * 4129 * @param manager {@link UIManager} that will be listening for action 4130 * commands. 4131 * 4132 * @return Menu item that has {@code manager} listening for 4133 * {@link #action}. 4134 */ 4135 protected JMenuItem buildMenuItem(final UIManager manager) { 4136 JMenuItem item = new JRadioButtonMenuItem(label); 4137 item.setActionCommand(action); 4138 item.addActionListener(manager); 4139 return item; 4140 } 4141 } 4142 4143 /** 4144 * Represents what McIDAS-V {@literal "knows"} about IDV actions. 4145 */ 4146 protected enum ActionAttribute { 4147 4148 /** 4149 * Unique identifier for an IDV action. Required attribute. 4150 * 4151 * @see IdvUIManager#ATTR_ID 4152 */ 4153 ID(ATTR_ID), 4154 4155 /** 4156 * Path to an icon for this action. Currently required. Note that 4157 * McIDAS-V differs from the IDV in that actions must support different 4158 * icon sizes. This is implemented in McIDAS-V by simply having the value 4159 * of this path be a valid {@literal "format string"}, 4160 * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"} 4161 * 4162 * <p>The upshot is that this value <b>will not be a valid path in 4163 * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 4164 * {@link IdvAction#getIconForStyle}. 4165 * 4166 * @see IdvUIManager#ATTR_IMAGE 4167 * @see IdvAction#getRawIconPath() 4168 * @see IdvAction#getMenuIcon() 4169 * @see IdvAction#getIconForStyle 4170 */ 4171 ICON(ATTR_IMAGE), 4172 4173 /** 4174 * Brief description of a IDV action. Required attribute. 4175 * @see IdvUIManager#ATTR_DESCRIPTION 4176 */ 4177 DESCRIPTION(ATTR_DESCRIPTION), 4178 4179 /** 4180 * Allows actions to be clustered into arbitrary groups. Currently 4181 * optional; defaults to {@literal "General"}. 4182 * @see IdvUIManager#ATTR_GROUP 4183 */ 4184 GROUP(ATTR_GROUP, "General"), 4185 4186 /** 4187 * Actual method call used to invoke a given IDV action. Required 4188 * attribute. 4189 * @see IdvUIManager#ATTR_ACTION 4190 */ 4191 ACTION(ATTR_ACTION); 4192 4193 /** 4194 * A blank {@link String} if this is a required attribute, or a 4195 * {@code String} value to use in case this attribute has not been 4196 * specified by a given IDV action. 4197 */ 4198 private final String defaultValue; 4199 4200 /** 4201 * String representation of this attribute as used by the IDV. 4202 * @see #asIdvString() 4203 */ 4204 private final String idvString; 4205 4206 /** Whether or not this attribute is required. */ 4207 private final boolean required; 4208 4209 /** 4210 * Creates a constant that represents a required IDV action attribute. 4211 * 4212 * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}. 4213 * 4214 * @throws NullPointerException if {@code idvString} is {@code null}. 4215 */ 4216 ActionAttribute(final String idvString) { 4217 requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String"); 4218 4219 this.idvString = idvString; 4220 this.defaultValue = ""; 4221 this.required = true; 4222 } 4223 4224 /** 4225 * Creates a constant that represents an optional IDV action attribute. 4226 * 4227 * @param idvString Corresponding IDV attribute {@link String}. 4228 * Cannot be {@code null}. 4229 * @param defValue Default value for actions that do not have this 4230 * attribute. Cannot be {@code null} or an empty {@code String}. 4231 * 4232 * @throws NullPointerException if either {@code idvString} or 4233 * {@code defValue} is {@code null}. 4234 * @throws IllegalArgumentException if {@code defValue} is an empty 4235 * {@code String}. 4236 * 4237 */ 4238 ActionAttribute(final String idvString, final String defValue) { 4239 requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String"); 4240 Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString()); 4241 Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString()); 4242 4243 this.idvString = idvString; 4244 this.defaultValue = defValue; 4245 this.required = (defaultValue.equals("")); 4246 } 4247 4248 /** 4249 * @return The {@link String} representation of this attribute, as is 4250 * used by the IDV. 4251 * 4252 * @see IdvUIManager#ATTR_ACTION 4253 * @see IdvUIManager#ATTR_DESCRIPTION 4254 * @see IdvUIManager#ATTR_GROUP 4255 * @see IdvUIManager#ATTR_ID 4256 * @see IdvUIManager#ATTR_IMAGE 4257 */ 4258 public String asIdvString() { return idvString; } 4259 4260 /** 4261 * @return {@literal "Default value"} for this attribute. 4262 * Blank {@link String}s imply that the attribute is required (and 4263 * thus lacks a true default value). 4264 */ 4265 public String defaultValue() { return defaultValue; } 4266 4267 /** 4268 * @return Whether or not this attribute is a required attribute for 4269 * valid {@link IdvAction}s. 4270 */ 4271 public boolean isRequired() { return required; } 4272 } 4273 4274 /** 4275 * Represents the set of known {@link IdvAction IdvActions} in an idiom 4276 * that can be easily used by both the IDV and McIDAS-V. 4277 */ 4278 // TODO(jon:101): use Sets instead of maps and whatnot 4279 // TODO(jon:103): create an invalid IdvAction 4280 public static final class IdvActions { 4281 4282 /** Maps {@literal "id"} values to {@link IdvAction IdvActions}. */ 4283 private final Map<String, IdvAction> idToAction = 4284 new ConcurrentHashMap<>(); 4285 4286 /** 4287 * Collects {@link IdvAction IdvActions} {@literal "under"} common 4288 * group values. 4289 */ 4290 // TODO(jon:102): this should probably become concurrency-friendly. 4291 private final Map<String, Set<IdvAction>> groupToActions = 4292 new LinkedHashMap<>(); 4293 4294 /** 4295 * Creates an object that represents the application's 4296 * {@link IdvAction IdvActions}. 4297 * 4298 * @param idv Reference to the IDV {@literal "god"} object. 4299 * Cannot be {@code null}. 4300 * @param collectionId IDV resource collection that contains our 4301 * actions. Cannot be {@code null}. 4302 * 4303 * @throws NullPointerException if {@code idv} or {@code collectionId} 4304 * is {@code null}. 4305 */ 4306 public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) { 4307 requireNonNull(idv, "Cannot provide a null IDV reference"); 4308 requireNonNull(collectionId, "Cannot build actions from a null collection id"); 4309 4310 // TODO(jon): benchmark use of xpath 4311 String query = "//action[@id and @image and @description and @action]"; 4312 for (Element e : elements(idv, collectionId, query)) { 4313 IdvAction a = new IdvAction(e); 4314 String id = a.getAttribute(ActionAttribute.ID); 4315 idToAction.put(id, a); 4316 String group = a.getAttribute(ActionAttribute.GROUP); 4317 if (!groupToActions.containsKey(group)) { 4318 groupToActions.put(group, new LinkedHashSet<>()); 4319 } 4320 Set<IdvAction> groupedIds = groupToActions.get(group); 4321 groupedIds.add(a); 4322 } 4323 } 4324 4325 /** 4326 * Attempts to return the {@link IdvAction} associated with the given 4327 * {@code actionId}. 4328 * 4329 * @param actionId Identifier to use in the search. Cannot be 4330 * {@code null}. 4331 * 4332 * @return Either the {@code IdvAction} that matches {@code actionId} 4333 * or {@code null} if there was no match. 4334 * 4335 * @throws NullPointerException if {@code actionId} is {@code null}. 4336 */ 4337 // TODO(jon:103) here 4338 public IdvAction getAction(final String actionId) { 4339 requireNonNull(actionId, "Null action identifiers are not allowed"); 4340 return idToAction.get(actionId); 4341 } 4342 4343 /** 4344 * Searches for the action associated with {@code actionId} and 4345 * returns the value associated with the given {@link ActionAttribute}. 4346 * 4347 * @param actionId Identifier to search for. Cannot be {@code null}. 4348 * @param attr Attribute whose value is desired. Cannot be {@code null}. 4349 * 4350 * @return Either the desired attribute value of the desired action, 4351 * or {@code null} if {@code actionId} has no associated action. 4352 * 4353 * @throws NullPointerException if either {@code actionId} or 4354 * {@code attr} is {@code null}. 4355 */ 4356 // TODO(jon:103) here 4357 public String getAttributeForAction(final String actionId, final ActionAttribute attr) { 4358 requireNonNull(actionId, "Null action identifiers are not allowed"); 4359 requireNonNull(attr, "Actions cannot have values associated with a null attribute"); 4360 IdvAction action = idToAction.get(actionId); 4361 if (action == null) { 4362 return null; 4363 } 4364 return action.getAttribute(attr); 4365 } 4366 4367 /** 4368 * Attempts to return the XML {@link Element} that 4369 * {@literal "represents"} the action associated with {@code actionId}. 4370 * 4371 * @param actionId Identifier whose XML element is desired. 4372 * Cannot be {@code null}. 4373 * 4374 * @return Either the XML element associated with {@code actionId} or 4375 * {@code null}. 4376 * 4377 * @throws NullPointerException if {@code actionId} is {@code null}. 4378 * 4379 * @see IdvAction#originalElement 4380 */ 4381 // TODO(jon:103) here 4382 public Element getElementForAction(final String actionId) { 4383 requireNonNull(actionId, "Cannot search for a null action identifier"); 4384 IdvAction action = idToAction.get(actionId); 4385 if (action == null) { 4386 return null; 4387 } 4388 return action.getElement(); 4389 } 4390 4391 /** 4392 * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and 4393 * {@link ToolbarStyle}. 4394 * 4395 * @param actionId ID of the action whose {@literal "styled"} icon is 4396 * desired. Cannot be {@code null}. 4397 * @param style Desired {@code Icon} style. Cannot be {@code null}. 4398 * 4399 * @return Either the {@code Icon} associated with {@code actionId} 4400 * and {@code style}, or {@code null}. 4401 * 4402 * @throws NullPointerException if either {@code actionId} or 4403 * {@code style} is {@code null}. 4404 */ 4405 // TODO(jon:103) here 4406 public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) { 4407 requireNonNull(actionId, "Cannot get an icon for a null action identifier"); 4408 requireNonNull(style, "Cannot get an icon for a null ToolbarStyle"); 4409 IdvAction a = idToAction.get(actionId); 4410 if (a == null) { 4411 return null; 4412 } 4413 return a.getIconForStyle(style); 4414 } 4415 4416 // TODO(jon:105): replace with something better 4417 public List<String> getAttributes(final ActionAttribute attr) { 4418 requireNonNull(attr, "Actions cannot have null attributes"); 4419 List<String> attributeList = arrList(idToAction.size()); 4420 for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) { 4421 attributeList.add(entry.getValue().getAttribute(attr)); 4422 } 4423 return attributeList; 4424 } 4425 4426 /** 4427 * @return List of all known {@code IdvAction}s. 4428 */ 4429 public List<IdvAction> getAllActions() { 4430 return arrList(idToAction.values()); 4431 } 4432 4433 /** 4434 * @return List of all known action groupings. 4435 * 4436 * @see ActionAttribute#GROUP 4437 * @see #getActionsForGroup(String) 4438 */ 4439 public List<String> getAllGroups() { 4440 return arrList(groupToActions.keySet()); 4441 } 4442 4443 /** 4444 * Returns the {@link Set} of {@link IdvAction}s associated with the 4445 * given {@code group}. 4446 * 4447 * @param group Group whose associated actions you want. Cannot be 4448 * {@code null}. 4449 * 4450 * @return Collection of {@code IdvAction}s associated with 4451 * {@code group}. A blank collection is returned if there are no actions 4452 * associated with {@code group}. 4453 * 4454 * @throws NullPointerException if {@code group} is {@code null}. 4455 * 4456 * @see ActionAttribute#GROUP 4457 * @see #getAllGroups() 4458 */ 4459 public Set<IdvAction> getActionsForGroup(final String group) { 4460 requireNonNull(group, "Actions cannot be associated with a null group"); 4461 if (!groupToActions.containsKey(group)) { 4462 return Collections.emptySet(); 4463 } 4464 return groupToActions.get(group); 4465 } 4466 4467 /** 4468 * Returns a summary of the known IDV actions. Please note that this 4469 * format is subject to change, and is not intended for serialization. 4470 * 4471 * @return String that looks like 4472 * {@code [IdvActions@HASHCODE: actions=...]}. 4473 */ 4474 @Override public String toString() { 4475 return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction); 4476 } 4477 } 4478 4479 /** 4480 * Represents an individual IDV action. Should be fairly adaptable to 4481 * unforeseen changes from Unidata? 4482 */ 4483 // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right? 4484 public static final class IdvAction { 4485 4486 /** The XML {@link Element} that represents this IDV action. */ 4487 private final Element originalElement; 4488 4489 /** Mapping of (known) XML attributes to values for this individual action. */ 4490 private final Map<ActionAttribute, String> attributes; 4491 4492 /** 4493 * Simple {@literal "cache"} for the different icons this action has 4494 * displayed. This is {@literal "lazy"}, so the cache does not contain 4495 * icons for {@link ToolbarStyle}s that haven't been used. 4496 */ 4497 private final Map<ToolbarStyle, Icon> iconCache = 4498 new ConcurrentHashMap<>(); 4499 4500 /** 4501 * Creates a representation of an IDV action using a given 4502 * {@link Element}. 4503 * 4504 * @param element XML representation of an IDV action. 4505 * Cannot be {@code null}. 4506 * 4507 * @throws NullPointerException if {@code element} is {@code null}. 4508 * @throws IllegalArgumentException if {@code element} is not a valid 4509 * IDV action. 4510 * 4511 * @see UIManager#isValidIdvAction(Element) 4512 */ 4513 public IdvAction(final Element element) { 4514 requireNonNull(element, "Cannot build an action from a null element"); 4515 // TODO(jon:107): need a way to diagnose what's wrong with the action? 4516 Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes"); 4517 originalElement = element; 4518 attributes = actionElementToMap(element); 4519 } 4520 4521 /** 4522 * @return Returns the {@literal "raw"} path to the icon associated 4523 * with this action. Remember that this is actually a 4524 * {@literal "format string"} and should not be considered a valid path! 4525 * 4526 * @see #getIconForStyle 4527 */ 4528 public String getRawIconPath() { 4529 return attributes.get(ActionAttribute.ICON); 4530 } 4531 4532 /** 4533 * @return Returns the {@link Icon} associated with 4534 * {@link ToolbarStyle#SMALL}. 4535 */ 4536 public Icon getMenuIcon() { 4537 return getIconForStyle(ToolbarStyle.SMALL); 4538 } 4539 4540 /** 4541 * Returns the {@link Icon} associated with this action and the given 4542 * {@link ToolbarStyle}. 4543 * 4544 * @param style {@literal "Style"} of the {@code Icon} to be returned. 4545 * Cannot be {@code null}. 4546 * 4547 * @return This action's {@code Icon} with {@code style} 4548 * {@literal "applied."} 4549 * 4550 * @see ActionAttribute#ICON 4551 * @see #iconCache 4552 */ 4553 public Icon getIconForStyle(final ToolbarStyle style) { 4554 requireNonNull(style, "Cannot build an icon for a null ToolbarStyle"); 4555 if (!iconCache.containsKey(style)) { 4556 String styledPath = String.format(getRawIconPath(), style.getSize()); 4557 URL tmp = getClass().getResource(styledPath); 4558 iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp))); 4559 } 4560 return iconCache.get(style); 4561 } 4562 4563 /** 4564 * @return Returns the identifier of this {@code IdvAction}. 4565 */ 4566 public String getId() { 4567 return getAttribute(ActionAttribute.ID); 4568 } 4569 4570 /** 4571 * Representation of this {@code IdvAction} as an 4572 * {@literal "IDV action call"}. 4573 * 4574 * @return String that is suitable to hand off to the IDV for execution. 4575 */ 4576 public String getCommand() { 4577 return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')"; 4578 } 4579 4580 /** 4581 * Returns the value associated with a given {@link ActionAttribute} 4582 * for this action. 4583 * 4584 * @param attr ActionAttribute whose value you want. 4585 * Cannot be {@code null}. 4586 * 4587 * @return Value associated with {@code attr}. 4588 * 4589 * @throws NullPointerException if {@code attr} is {@code null}. 4590 */ 4591 public String getAttribute(final ActionAttribute attr) { 4592 requireNonNull(attr, "No values can be associated with a null ActionAttribute"); 4593 return attributes.get(attr); 4594 } 4595 4596 /** 4597 * @return The XML {@link Element} used to create this {@code IdvAction}. 4598 */ 4599 // TODO(jon:104): any way to copy this element? if so, this can become an immutable class! 4600 public Element getElement() { 4601 return originalElement; 4602 } 4603 4604 /** 4605 * Returns a brief description of this action. Please note that the 4606 * format is subject to change and is not intended for serialization. 4607 * 4608 * @return String that looks like 4609 * {@code [IdvAction@HASHCODE: attributes=...]}. 4610 */ 4611 @Override public String toString() { 4612 return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes); 4613 } 4614 } 4615}