001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.download; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.BorderLayout; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.FlowLayout; 011import java.awt.GridBagLayout; 012import java.awt.event.ActionEvent; 013import java.awt.event.ComponentAdapter; 014import java.awt.event.ComponentEvent; 015import java.awt.event.InputEvent; 016import java.awt.event.KeyEvent; 017import java.awt.event.WindowAdapter; 018import java.awt.event.WindowEvent; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Optional; 022import java.util.stream.Collectors; 023import java.util.stream.IntStream; 024 025import javax.swing.AbstractAction; 026import javax.swing.Icon; 027import javax.swing.JButton; 028import javax.swing.JCheckBox; 029import javax.swing.JComponent; 030import javax.swing.JDialog; 031import javax.swing.JLabel; 032import javax.swing.JPanel; 033import javax.swing.JSplitPane; 034import javax.swing.JTabbedPane; 035import javax.swing.KeyStroke; 036import javax.swing.event.ChangeEvent; 037import javax.swing.event.ChangeListener; 038 039import org.openstreetmap.josm.actions.ExpertToggleAction; 040import org.openstreetmap.josm.data.Bounds; 041import org.openstreetmap.josm.data.preferences.BooleanProperty; 042import org.openstreetmap.josm.data.preferences.IntegerProperty; 043import org.openstreetmap.josm.data.preferences.StringProperty; 044import org.openstreetmap.josm.gui.MainApplication; 045import org.openstreetmap.josm.gui.MapView; 046import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 047import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 048import org.openstreetmap.josm.gui.help.HelpUtil; 049import org.openstreetmap.josm.gui.layer.OsmDataLayer; 050import org.openstreetmap.josm.gui.util.GuiHelper; 051import org.openstreetmap.josm.gui.util.WindowGeometry; 052import org.openstreetmap.josm.io.NetworkManager; 053import org.openstreetmap.josm.io.OnlineResource; 054import org.openstreetmap.josm.plugins.PluginHandler; 055import org.openstreetmap.josm.spi.preferences.Config; 056import org.openstreetmap.josm.tools.GBC; 057import org.openstreetmap.josm.tools.ImageProvider; 058import org.openstreetmap.josm.tools.InputMapUtils; 059import org.openstreetmap.josm.tools.JosmRuntimeException; 060import org.openstreetmap.josm.tools.ListenerList; 061import org.openstreetmap.josm.tools.Logging; 062import org.openstreetmap.josm.tools.OsmUrlToBounds; 063 064/** 065 * Dialog displayed to the user to download mapping data. 066 */ 067public class DownloadDialog extends JDialog { 068 069 private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0); 070 private static final StringProperty DOWNLOAD_SOURCE_TAB = new StringProperty("download.source.tab", OSMDownloadSource.SIMPLE_NAME); 071 private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false); 072 private static final BooleanProperty DOWNLOAD_ZOOMTODATA = new BooleanProperty("download.zoomtodata", true); 073 074 /** the unique instance of the download dialog */ 075 private static DownloadDialog instance; 076 077 /** 078 * Replies the unique instance of the download dialog 079 * 080 * @return the unique instance of the download dialog 081 */ 082 public static synchronized DownloadDialog getInstance() { 083 if (instance == null) { 084 instance = new DownloadDialog(MainApplication.getMainFrame()); 085 } 086 return instance; 087 } 088 089 private static final ListenerList<DownloadSourceListener> downloadSourcesListeners = ListenerList.create(); 090 private static final List<DownloadSource<?>> downloadSources = new ArrayList<>(); 091 static { 092 // add default download sources 093 addDownloadSource(new OSMDownloadSource()); 094 addDownloadSource(new OverpassDownloadSource()); 095 } 096 097 protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>(); 098 protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane(); 099 protected final DownloadSourceTabs downloadSourcesTab = new DownloadSourceTabs(); 100 101 protected JCheckBox cbStartup; 102 protected JCheckBox cbZoomToDownloadedData; 103 protected SlippyMapChooser slippyMapChooser; 104 protected JPanel mainPanel; 105 protected DownloadDialogSplitPane dialogSplit; 106 107 /* 108 * Keep the reference globally to avoid having it garbage collected 109 */ 110 protected final transient ExpertToggleAction.ExpertModeChangeListener expertListener = 111 getExpertModeListenerForDownloadSources(); 112 protected transient Bounds currentBounds; 113 protected boolean canceled; 114 115 protected JButton btnDownload; 116 protected JButton btnDownloadNewLayer; 117 protected JButton btnCancel; 118 protected JButton btnHelp; 119 120 /** 121 * Builds the main panel of the dialog. 122 * @return The panel of the dialog. 123 */ 124 protected final JPanel buildMainPanel() { 125 mainPanel = new JPanel(new GridBagLayout()); 126 127 // must be created before hook 128 slippyMapChooser = new SlippyMapChooser(); 129 130 // predefined download selections 131 downloadSelections.add(slippyMapChooser); 132 downloadSelections.add(new BookmarkSelection()); 133 downloadSelections.add(new BoundingBoxSelection()); 134 downloadSelections.add(new PlaceSelection()); 135 downloadSelections.add(new TileSelection()); 136 137 // add selections from plugins 138 PluginHandler.addDownloadSelection(downloadSelections); 139 140 // register all default download selections 141 for (DownloadSelection s : downloadSelections) { 142 s.addGui(this); 143 } 144 145 // allow to collapse the panes, but reserve some space for tabs 146 downloadSourcesTab.setMinimumSize(new Dimension(0, 25)); 147 tpDownloadAreaSelectors.setMinimumSize(new Dimension(0, 0)); 148 149 dialogSplit = new DownloadDialogSplitPane( 150 downloadSourcesTab, 151 tpDownloadAreaSelectors); 152 153 ChangeListener tabChangedListener = getDownloadSourceTabChangeListener(); 154 tabChangedListener.stateChanged(new ChangeEvent(downloadSourcesTab)); 155 downloadSourcesTab.addChangeListener(tabChangedListener); 156 157 mainPanel.add(dialogSplit, GBC.eol().fill()); 158 159 cbStartup = new JCheckBox(tr("Open this dialog on startup")); 160 cbStartup.setToolTipText( 161 tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" + 162 "You can open it manually from File menu or toolbar.</html>")); 163 cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected())); 164 165 cbZoomToDownloadedData = new JCheckBox(tr("Zoom to downloaded data")); 166 cbZoomToDownloadedData.setToolTipText(tr("Select to zoom to entire newly downloaded data.")); 167 168 mainPanel.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5)); 169 mainPanel.add(cbZoomToDownloadedData, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5)); 170 171 ExpertToggleAction.addVisibilitySwitcher(cbZoomToDownloadedData); 172 173 mainPanel.add(new JLabel(), GBC.eol()); // place info label at a new line 174 JLabel infoLabel = new JLabel( 175 tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom.")); 176 mainPanel.add(infoLabel, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 0)); 177 178 ExpertToggleAction.addExpertModeChangeListener(isExpert -> infoLabel.setVisible(!isExpert), true); 179 180 return mainPanel; 181 } 182 183 /** 184 * Builds the button pane of the dialog. 185 * @return The button panel of the dialog. 186 */ 187 protected final JPanel buildButtonPanel() { 188 btnDownload = new JButton(new DownloadAction(false)); 189 btnDownloadNewLayer = new JButton(new DownloadAction(true)); 190 btnCancel = new JButton(new CancelAction()); 191 btnHelp = new JButton( 192 new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString())); 193 194 JPanel pnl = new JPanel(new FlowLayout()); 195 196 pnl.add(btnDownload); 197 pnl.add(btnDownloadNewLayer); 198 pnl.add(btnCancel); 199 pnl.add(btnHelp); 200 201 InputMapUtils.enableEnter(btnDownload); 202 InputMapUtils.enableEnter(btnCancel); 203 InputMapUtils.addEscapeAction(getRootPane(), btnCancel.getAction()); 204 InputMapUtils.enableEnter(btnHelp); 205 206 InputMapUtils.addEnterActionWhenAncestor(cbStartup, btnDownload.getAction()); 207 InputMapUtils.addEnterActionWhenAncestor(cbZoomToDownloadedData, btnDownload.getAction()); 208 InputMapUtils.addCtrlEnterAction(pnl, btnDownload.getAction()); 209 210 return pnl; 211 } 212 213 /** 214 * Constructs a new {@code DownloadDialog}. 215 * @param parent the parent component 216 */ 217 public DownloadDialog(Component parent) { 218 this(parent, ht("/Action/Download")); 219 } 220 221 /** 222 * Constructs a new {@code DownloadDialog}. 223 * @param parent the parent component 224 * @param helpTopic the help topic to assign 225 */ 226 public DownloadDialog(Component parent, String helpTopic) { 227 super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL); 228 HelpUtil.setHelpContext(getRootPane(), helpTopic); 229 getContentPane().setLayout(new BorderLayout()); 230 getContentPane().add(buildMainPanel(), BorderLayout.CENTER); 231 getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH); 232 233 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 234 KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), "checkClipboardContents"); 235 236 getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() { 237 @Override 238 public void actionPerformed(ActionEvent e) { 239 String clip = ClipboardUtils.getClipboardStringContent(); 240 if (clip == null) { 241 return; 242 } 243 Bounds b = OsmUrlToBounds.parse(clip); 244 if (b != null) { 245 boundingBoxChanged(new Bounds(b), null); 246 } 247 } 248 }); 249 addWindowListener(new WindowEventHandler()); 250 ExpertToggleAction.addExpertModeChangeListener(expertListener); 251 restoreSettings(); 252 253 // if no bounding box is selected make sure it is still propagated. 254 if (currentBounds == null) { 255 boundingBoxChanged(null, null); 256 } 257 } 258 259 /** 260 * Distributes a "bounding box changed" from one DownloadSelection 261 * object to the others, so they may update or clear their input fields. Also informs 262 * download sources about the change, so they can react on it. 263 * @param b new current bounds 264 * 265 * @param eventSource - the DownloadSelection object that fired this notification. 266 */ 267 public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) { 268 this.currentBounds = b; 269 for (DownloadSelection s : downloadSelections) { 270 if (s != eventSource) { 271 s.setDownloadArea(currentBounds); 272 } 273 } 274 275 for (AbstractDownloadSourcePanel<?> ds : downloadSourcesTab.getAllPanels()) { 276 ds.boundingBoxChanged(b); 277 } 278 } 279 280 /** 281 * Starts download for the given bounding box 282 * @param b bounding box to download 283 */ 284 public void startDownload(Bounds b) { 285 this.currentBounds = b; 286 startDownload(); 287 } 288 289 /** 290 * Starts download. 291 */ 292 public void startDownload() { 293 btnDownload.doClick(); 294 } 295 296 /** 297 * Replies true if the user requires to zoom to new downloaded data 298 * 299 * @return true if the user requires to zoom to new downloaded data 300 * @since 11658 301 */ 302 public boolean isZoomToDownloadedDataRequired() { 303 return cbZoomToDownloadedData.isSelected(); 304 } 305 306 /** 307 * Determines if the dialog autorun is enabled in preferences. 308 * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise. 309 */ 310 public static boolean isAutorunEnabled() { 311 return DOWNLOAD_AUTORUN.get(); 312 } 313 314 /** 315 * Adds a new download area selector to the download dialog. 316 * 317 * @param selector the download are selector. 318 * @param displayName the display name of the selector. 319 */ 320 public void addDownloadAreaSelector(JPanel selector, String displayName) { 321 tpDownloadAreaSelectors.add(displayName, selector); 322 } 323 324 /** 325 * Adds a new download source to the download dialog if it is not added. 326 * 327 * @param downloadSource The download source to be added. 328 * @param <T> The type of the download data. 329 * @throws JosmRuntimeException If the download source is already added. Note, download sources are 330 * compared by their reference. 331 * @since 12878 332 */ 333 public static <T> void addDownloadSource(DownloadSource<T> downloadSource) { 334 if (downloadSources.contains(downloadSource)) { 335 throw new JosmRuntimeException("The download source you are trying to add already exists."); 336 } 337 338 downloadSources.add(downloadSource); 339 downloadSourcesListeners.fireEvent(l -> l.downloadSourceAdded(downloadSource)); 340 } 341 342 /** 343 * Remove a download source from the download dialog 344 * 345 * @param downloadSource The download source to be removed. 346 * @return see {@link List#remove} 347 * @since 15542 348 */ 349 public static boolean removeDownloadSource(DownloadSource<?> downloadSource) { 350 if (downloadSources.contains(downloadSource)) { 351 return downloadSources.remove(downloadSource); 352 } 353 return false; 354 } 355 356 /** 357 * Refreshes the tile sources. 358 * @since 6364 359 */ 360 public final void refreshTileSources() { 361 if (slippyMapChooser != null) { 362 slippyMapChooser.refreshTileSources(); 363 } 364 } 365 366 /** 367 * Remembers the current settings in the download dialog. 368 */ 369 public void rememberSettings() { 370 DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex()); 371 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::rememberSettings); 372 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> DOWNLOAD_SOURCE_TAB.put(panel.getSimpleName())); 373 DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected()); 374 if (currentBounds != null) { 375 Config.getPref().put("osm-download.bounds", currentBounds.encodeAsString(";")); 376 } 377 } 378 379 /** 380 * Restores the previous settings in the download dialog. 381 */ 382 public void restoreSettings() { 383 cbStartup.setSelected(isAutorunEnabled()); 384 cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get()); 385 386 try { 387 tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get()); 388 } catch (IndexOutOfBoundsException e) { 389 Logging.trace(e); 390 tpDownloadAreaSelectors.setSelectedIndex(0); 391 } 392 393 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::restoreSettings); 394 downloadSourcesTab.setSelected(DOWNLOAD_SOURCE_TAB.get()); 395 396 if (MainApplication.isDisplayingMapView()) { 397 MapView mv = MainApplication.getMap().mapView; 398 currentBounds = new Bounds( 399 mv.getLatLon(0, mv.getHeight()), 400 mv.getLatLon(mv.getWidth(), 0) 401 ); 402 boundingBoxChanged(currentBounds, null); 403 } else { 404 Bounds bounds = getSavedDownloadBounds(); 405 if (bounds != null) { 406 currentBounds = bounds; 407 boundingBoxChanged(currentBounds, null); 408 } 409 } 410 } 411 412 /** 413 * Returns the previously saved bounding box from preferences. 414 * @return The bounding box saved in preferences if any, {@code null} otherwise. 415 * @since 6509 416 */ 417 public static Bounds getSavedDownloadBounds() { 418 String value = Config.getPref().get("osm-download.bounds"); 419 if (!value.isEmpty()) { 420 try { 421 return new Bounds(value, ";"); 422 } catch (IllegalArgumentException e) { 423 Logging.warn(e); 424 } 425 } 426 return null; 427 } 428 429 /** 430 * Automatically opens the download dialog, if autorun is enabled. 431 * @see #isAutorunEnabled 432 */ 433 public static void autostartIfNeeded() { 434 if (isAutorunEnabled()) { 435 MainApplication.getMenu().download.actionPerformed(null); 436 } 437 } 438 439 /** 440 * Returns an {@link Optional} of the currently selected download area. 441 * @return An {@link Optional} of the currently selected download area. 442 * @since 12574 Return type changed to optional 443 */ 444 public Optional<Bounds> getSelectedDownloadArea() { 445 return Optional.ofNullable(currentBounds); 446 } 447 448 @Override 449 public void setVisible(boolean visible) { 450 if (visible) { 451 btnDownloadNewLayer.setEnabled( 452 !MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).isEmpty()); 453 new WindowGeometry( 454 getClass().getName() + ".geometry", 455 WindowGeometry.centerInWindow( 456 getParent(), 457 new Dimension(1000, 600) 458 ) 459 ).applySafe(this); 460 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 461 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 462 } 463 super.setVisible(visible); 464 } 465 466 /** 467 * Replies true if the dialog was canceled 468 * 469 * @return true if the dialog was canceled 470 */ 471 public boolean isCanceled() { 472 return canceled; 473 } 474 475 /** 476 * Gets the global settings of the download dialog. 477 * @param newLayer The flag defining if a new layer must be created for the downloaded data. 478 * @return The {@link DownloadSettings} object that describes the current state of 479 * the download dialog. 480 */ 481 public DownloadSettings getDownloadSettings(boolean newLayer) { 482 return new DownloadSettings(currentBounds, newLayer, isZoomToDownloadedDataRequired()); 483 } 484 485 protected void setCanceled(boolean canceled) { 486 this.canceled = canceled; 487 } 488 489 /** 490 * Adds the download source to the download sources tab. 491 * @param downloadSource The download source to be added. 492 * @param <T> The type of the download data. 493 */ 494 protected <T> void addNewDownloadSourceTab(DownloadSource<T> downloadSource) { 495 downloadSourcesTab.addPanel(downloadSource.createPanel(this)); 496 } 497 498 /** 499 * Creates listener that removes/adds download sources from/to {@code downloadSourcesTab} 500 * depending on the current mode. 501 * @return The expert mode listener. 502 */ 503 private ExpertToggleAction.ExpertModeChangeListener getExpertModeListenerForDownloadSources() { 504 return downloadSourcesTab::updateExpert; 505 } 506 507 /** 508 * Creates a listener that reacts on tab switches for {@code downloadSourcesTab} in order 509 * to adjust proper division of the dialog according to user saved preferences or minimal size 510 * of the panel. 511 * @return A listener to adjust dialog division. 512 */ 513 private ChangeListener getDownloadSourceTabChangeListener() { 514 return ec -> downloadSourcesTab.getSelectedPanel().ifPresent( 515 panel -> dialogSplit.setPolicy(panel.getSizingPolicy())); 516 } 517 518 /** 519 * Action that is executed when the cancel button is pressed. 520 */ 521 class CancelAction extends AbstractAction { 522 CancelAction() { 523 putValue(NAME, tr("Cancel")); 524 new ImageProvider("cancel").getResource().attachImageIcon(this); 525 putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); 526 } 527 528 /** 529 * Cancels the download 530 */ 531 public void run() { 532 rememberSettings(); 533 setCanceled(true); 534 setVisible(false); 535 } 536 537 @Override 538 public void actionPerformed(ActionEvent e) { 539 Optional<AbstractDownloadSourcePanel<?>> panel = downloadSourcesTab.getSelectedPanel(); 540 run(); 541 panel.ifPresent(AbstractDownloadSourcePanel::checkCancel); 542 } 543 } 544 545 /** 546 * Action that is executed when the download button is pressed. 547 */ 548 class DownloadAction extends AbstractAction { 549 final boolean newLayer; 550 DownloadAction(boolean newLayer) { 551 this.newLayer = newLayer; 552 if (!newLayer) { 553 putValue(NAME, tr("Download")); 554 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); 555 new ImageProvider("download").getResource().attachImageIcon(this); 556 } else { 557 putValue(NAME, tr("Download as new layer")); 558 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area into a new data layer")); 559 new ImageProvider("download_new_layer").getResource().attachImageIcon(this); 560 } 561 setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API)); 562 } 563 564 /** 565 * Starts the download and closes the dialog, if all requirements for the current download source are met. 566 * Otherwise the download is not started and the dialog remains visible. 567 */ 568 public void run() { 569 rememberSettings(); 570 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> { 571 DownloadSettings downloadSettings = getDownloadSettings(newLayer); 572 if (panel.checkDownload(downloadSettings)) { 573 setCanceled(false); 574 setVisible(false); 575 panel.triggerDownload(downloadSettings); 576 } 577 }); 578 } 579 580 @Override 581 public void actionPerformed(ActionEvent e) { 582 run(); 583 } 584 } 585 586 class WindowEventHandler extends WindowAdapter { 587 @Override 588 public void windowClosing(WindowEvent e) { 589 new CancelAction().run(); 590 } 591 592 @Override 593 public void windowActivated(WindowEvent e) { 594 btnDownload.requestFocusInWindow(); 595 } 596 } 597 598 /** 599 * A special tabbed pane for {@link AbstractDownloadSourcePanel}s 600 * @author Michael Zangl 601 * @since 12706 602 */ 603 private class DownloadSourceTabs extends JTabbedPane implements DownloadSourceListener { 604 private final List<AbstractDownloadSourcePanel<?>> allPanels = new ArrayList<>(); 605 606 DownloadSourceTabs() { 607 downloadSources.forEach(this::downloadSourceAdded); 608 downloadSourcesListeners.addListener(this); 609 } 610 611 List<AbstractDownloadSourcePanel<?>> getAllPanels() { 612 return allPanels; 613 } 614 615 List<AbstractDownloadSourcePanel<?>> getVisiblePanels() { 616 return IntStream.range(0, getTabCount()) 617 .mapToObj(this::getComponentAt) 618 .map(p -> (AbstractDownloadSourcePanel<?>) p) 619 .collect(Collectors.toList()); 620 } 621 622 void setSelected(String simpleName) { 623 getVisiblePanels().stream() 624 .filter(panel -> simpleName.equals(panel.getSimpleName())) 625 .findFirst() 626 .ifPresent(this::setSelectedComponent); 627 } 628 629 void updateExpert(boolean isExpert) { 630 updateTabs(); 631 } 632 633 void addPanel(AbstractDownloadSourcePanel<?> panel) { 634 allPanels.add(panel); 635 updateTabs(); 636 } 637 638 private void updateTabs() { 639 // Not the best performance, but we don't do it often 640 removeAll(); 641 642 boolean isExpert = ExpertToggleAction.isExpert(); 643 allPanels.stream() 644 .filter(panel -> isExpert || !panel.getDownloadSource().onlyExpert()) 645 .forEach(panel -> addTab(panel.getDownloadSource().getLabel(), panel.getIcon(), panel)); 646 } 647 648 Optional<AbstractDownloadSourcePanel<?>> getSelectedPanel() { 649 return Optional.ofNullable((AbstractDownloadSourcePanel<?>) getSelectedComponent()); 650 } 651 652 @Override 653 public void insertTab(String title, Icon icon, Component component, String tip, int index) { 654 if (!(component instanceof AbstractDownloadSourcePanel)) { 655 throw new IllegalArgumentException("Can only add AbstractDownloadSourcePanels"); 656 } 657 super.insertTab(title, icon, component, tip, index); 658 } 659 660 @Override 661 public void downloadSourceAdded(DownloadSource<?> source) { 662 addPanel(source.createPanel(DownloadDialog.this)); 663 } 664 } 665 666 /** 667 * A special split pane that acts according to a {@link DownloadSourceSizingPolicy} 668 * 669 * It attempts to size the top tab content correctly. 670 * 671 * @author Michael Zangl 672 * @since 12705 673 */ 674 private static class DownloadDialogSplitPane extends JSplitPane { 675 private DownloadSourceSizingPolicy policy; 676 private final JTabbedPane topComponent; 677 /** 678 * If the height was explicitly set by the user. 679 */ 680 private boolean heightAdjustedExplicitly; 681 682 DownloadDialogSplitPane(JTabbedPane newTopComponent, Component newBottomComponent) { 683 super(VERTICAL_SPLIT, newTopComponent, newBottomComponent); 684 this.topComponent = newTopComponent; 685 686 addComponentListener(new ComponentAdapter() { 687 @Override 688 public void componentResized(ComponentEvent e) { 689 // doLayout is called automatically when the component size decreases 690 // This seems to be the only way to call doLayout when the component size increases 691 // We need this since we sometimes want to increase the top component size. 692 revalidate(); 693 } 694 }); 695 696 addPropertyChangeListener(DIVIDER_LOCATION_PROPERTY, e -> heightAdjustedExplicitly = true); 697 } 698 699 public void setPolicy(DownloadSourceSizingPolicy policy) { 700 this.policy = policy; 701 702 super.setDividerLocation(policy.getComponentHeight() + computeOffset()); 703 setDividerSize(policy.isHeightAdjustable() ? 10 : 0); 704 setEnabled(policy.isHeightAdjustable()); 705 } 706 707 @Override 708 public void doLayout() { 709 // We need to force this height before the layout manager is run. 710 // We cannot do this in the setDividerLocation, since the offset cannot be computed there. 711 int offset = computeOffset(); 712 if (policy.isHeightAdjustable() && heightAdjustedExplicitly) { 713 policy.storeHeight(Math.max(getDividerLocation() - offset, 0)); 714 } 715 // At least 30 pixel for map, if we have enough space 716 int maxValidDividerLocation = getHeight() > 150 ? getHeight() - 40 : getHeight(); 717 718 super.setDividerLocation(Math.min(policy.getComponentHeight() + offset, maxValidDividerLocation)); 719 super.doLayout(); 720 // Order is important (set this after setDividerLocation/doLayout called the listener) 721 this.heightAdjustedExplicitly = false; 722 } 723 724 /** 725 * @return The difference between the content height and the divider location 726 */ 727 private int computeOffset() { 728 Component selectedComponent = topComponent.getSelectedComponent(); 729 return topComponent.getHeight() - (selectedComponent == null ? 0 : selectedComponent.getHeight()); 730 } 731 } 732}