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 * Refreshes the tile sources. 344 * @since 6364 345 */ 346 public final void refreshTileSources() { 347 if (slippyMapChooser != null) { 348 slippyMapChooser.refreshTileSources(); 349 } 350 } 351 352 /** 353 * Remembers the current settings in the download dialog. 354 */ 355 public void rememberSettings() { 356 DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex()); 357 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::rememberSettings); 358 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> DOWNLOAD_SOURCE_TAB.put(panel.getSimpleName())); 359 DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected()); 360 if (currentBounds != null) { 361 Config.getPref().put("osm-download.bounds", currentBounds.encodeAsString(";")); 362 } 363 } 364 365 /** 366 * Restores the previous settings in the download dialog. 367 */ 368 public void restoreSettings() { 369 cbStartup.setSelected(isAutorunEnabled()); 370 cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get()); 371 372 try { 373 tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get()); 374 } catch (IndexOutOfBoundsException e) { 375 Logging.trace(e); 376 tpDownloadAreaSelectors.setSelectedIndex(0); 377 } 378 379 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::restoreSettings); 380 downloadSourcesTab.setSelected(DOWNLOAD_SOURCE_TAB.get()); 381 382 if (MainApplication.isDisplayingMapView()) { 383 MapView mv = MainApplication.getMap().mapView; 384 currentBounds = new Bounds( 385 mv.getLatLon(0, mv.getHeight()), 386 mv.getLatLon(mv.getWidth(), 0) 387 ); 388 boundingBoxChanged(currentBounds, null); 389 } else { 390 Bounds bounds = getSavedDownloadBounds(); 391 if (bounds != null) { 392 currentBounds = bounds; 393 boundingBoxChanged(currentBounds, null); 394 } 395 } 396 } 397 398 /** 399 * Returns the previously saved bounding box from preferences. 400 * @return The bounding box saved in preferences if any, {@code null} otherwise. 401 * @since 6509 402 */ 403 public static Bounds getSavedDownloadBounds() { 404 String value = Config.getPref().get("osm-download.bounds"); 405 if (!value.isEmpty()) { 406 try { 407 return new Bounds(value, ";"); 408 } catch (IllegalArgumentException e) { 409 Logging.warn(e); 410 } 411 } 412 return null; 413 } 414 415 /** 416 * Automatically opens the download dialog, if autorun is enabled. 417 * @see #isAutorunEnabled 418 */ 419 public static void autostartIfNeeded() { 420 if (isAutorunEnabled()) { 421 MainApplication.getMenu().download.actionPerformed(null); 422 } 423 } 424 425 /** 426 * Returns an {@link Optional} of the currently selected download area. 427 * @return An {@link Optional} of the currently selected download area. 428 * @since 12574 Return type changed to optional 429 */ 430 public Optional<Bounds> getSelectedDownloadArea() { 431 return Optional.ofNullable(currentBounds); 432 } 433 434 @Override 435 public void setVisible(boolean visible) { 436 if (visible) { 437 btnDownloadNewLayer.setEnabled( 438 !MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).isEmpty()); 439 new WindowGeometry( 440 getClass().getName() + ".geometry", 441 WindowGeometry.centerInWindow( 442 getParent(), 443 new Dimension(1000, 600) 444 ) 445 ).applySafe(this); 446 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 447 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 448 } 449 super.setVisible(visible); 450 } 451 452 /** 453 * Replies true if the dialog was canceled 454 * 455 * @return true if the dialog was canceled 456 */ 457 public boolean isCanceled() { 458 return canceled; 459 } 460 461 /** 462 * Gets the global settings of the download dialog. 463 * @param newLayer The flag defining if a new layer must be created for the downloaded data. 464 * @return The {@link DownloadSettings} object that describes the current state of 465 * the download dialog. 466 */ 467 public DownloadSettings getDownloadSettings(boolean newLayer) { 468 return new DownloadSettings(currentBounds, newLayer, isZoomToDownloadedDataRequired()); 469 } 470 471 protected void setCanceled(boolean canceled) { 472 this.canceled = canceled; 473 } 474 475 /** 476 * Adds the download source to the download sources tab. 477 * @param downloadSource The download source to be added. 478 * @param <T> The type of the download data. 479 */ 480 protected <T> void addNewDownloadSourceTab(DownloadSource<T> downloadSource) { 481 downloadSourcesTab.addPanel(downloadSource.createPanel(this)); 482 } 483 484 /** 485 * Creates listener that removes/adds download sources from/to {@code downloadSourcesTab} 486 * depending on the current mode. 487 * @return The expert mode listener. 488 */ 489 private ExpertToggleAction.ExpertModeChangeListener getExpertModeListenerForDownloadSources() { 490 return downloadSourcesTab::updateExpert; 491 } 492 493 /** 494 * Creates a listener that reacts on tab switches for {@code downloadSourcesTab} in order 495 * to adjust proper division of the dialog according to user saved preferences or minimal size 496 * of the panel. 497 * @return A listener to adjust dialog division. 498 */ 499 private ChangeListener getDownloadSourceTabChangeListener() { 500 return ec -> downloadSourcesTab.getSelectedPanel().ifPresent( 501 panel -> dialogSplit.setPolicy(panel.getSizingPolicy())); 502 } 503 504 /** 505 * Action that is executed when the cancel button is pressed. 506 */ 507 class CancelAction extends AbstractAction { 508 CancelAction() { 509 putValue(NAME, tr("Cancel")); 510 new ImageProvider("cancel").getResource().attachImageIcon(this); 511 putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); 512 } 513 514 /** 515 * Cancels the download 516 */ 517 public void run() { 518 rememberSettings(); 519 setCanceled(true); 520 setVisible(false); 521 } 522 523 @Override 524 public void actionPerformed(ActionEvent e) { 525 Optional<AbstractDownloadSourcePanel<?>> panel = downloadSourcesTab.getSelectedPanel(); 526 run(); 527 panel.ifPresent(AbstractDownloadSourcePanel::checkCancel); 528 } 529 } 530 531 /** 532 * Action that is executed when the download button is pressed. 533 */ 534 class DownloadAction extends AbstractAction { 535 final boolean newLayer; 536 DownloadAction(boolean newLayer) { 537 this.newLayer = newLayer; 538 if (!newLayer) { 539 putValue(NAME, tr("Download")); 540 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); 541 new ImageProvider("download").getResource().attachImageIcon(this); 542 } else { 543 putValue(NAME, tr("Download as new layer")); 544 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area into a new data layer")); 545 new ImageProvider("download_new_layer").getResource().attachImageIcon(this); 546 } 547 setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API)); 548 } 549 550 /** 551 * Starts the download and closes the dialog, if all requirements for the current download source are met. 552 * Otherwise the download is not started and the dialog remains visible. 553 */ 554 public void run() { 555 rememberSettings(); 556 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> { 557 DownloadSettings downloadSettings = getDownloadSettings(newLayer); 558 if (panel.checkDownload(downloadSettings)) { 559 setCanceled(false); 560 setVisible(false); 561 panel.triggerDownload(downloadSettings); 562 } 563 }); 564 } 565 566 @Override 567 public void actionPerformed(ActionEvent e) { 568 run(); 569 } 570 } 571 572 class WindowEventHandler extends WindowAdapter { 573 @Override 574 public void windowClosing(WindowEvent e) { 575 new CancelAction().run(); 576 } 577 578 @Override 579 public void windowActivated(WindowEvent e) { 580 btnDownload.requestFocusInWindow(); 581 } 582 } 583 584 /** 585 * A special tabbed pane for {@link AbstractDownloadSourcePanel}s 586 * @author Michael Zangl 587 * @since 12706 588 */ 589 private class DownloadSourceTabs extends JTabbedPane implements DownloadSourceListener { 590 private final List<AbstractDownloadSourcePanel<?>> allPanels = new ArrayList<>(); 591 592 DownloadSourceTabs() { 593 downloadSources.forEach(this::downloadSourceAdded); 594 downloadSourcesListeners.addListener(this); 595 } 596 597 List<AbstractDownloadSourcePanel<?>> getAllPanels() { 598 return allPanels; 599 } 600 601 List<AbstractDownloadSourcePanel<?>> getVisiblePanels() { 602 return IntStream.range(0, getTabCount()) 603 .mapToObj(this::getComponentAt) 604 .map(p -> (AbstractDownloadSourcePanel<?>) p) 605 .collect(Collectors.toList()); 606 } 607 608 void setSelected(String simpleName) { 609 getVisiblePanels().stream() 610 .filter(panel -> simpleName.equals(panel.getSimpleName())) 611 .findFirst() 612 .ifPresent(this::setSelectedComponent); 613 } 614 615 void updateExpert(boolean isExpert) { 616 updateTabs(); 617 } 618 619 void addPanel(AbstractDownloadSourcePanel<?> panel) { 620 allPanels.add(panel); 621 updateTabs(); 622 } 623 624 private void updateTabs() { 625 // Not the best performance, but we don't do it often 626 removeAll(); 627 628 boolean isExpert = ExpertToggleAction.isExpert(); 629 allPanels.stream() 630 .filter(panel -> isExpert || !panel.getDownloadSource().onlyExpert()) 631 .forEach(panel -> addTab(panel.getDownloadSource().getLabel(), panel.getIcon(), panel)); 632 } 633 634 Optional<AbstractDownloadSourcePanel<?>> getSelectedPanel() { 635 return Optional.ofNullable((AbstractDownloadSourcePanel<?>) getSelectedComponent()); 636 } 637 638 @Override 639 public void insertTab(String title, Icon icon, Component component, String tip, int index) { 640 if (!(component instanceof AbstractDownloadSourcePanel)) { 641 throw new IllegalArgumentException("Can only add AbstractDownloadSourcePanels"); 642 } 643 super.insertTab(title, icon, component, tip, index); 644 } 645 646 @Override 647 public void downloadSourceAdded(DownloadSource<?> source) { 648 addPanel(source.createPanel(DownloadDialog.this)); 649 } 650 } 651 652 /** 653 * A special split pane that acts according to a {@link DownloadSourceSizingPolicy} 654 * 655 * It attempts to size the top tab content correctly. 656 * 657 * @author Michael Zangl 658 * @since 12705 659 */ 660 private static class DownloadDialogSplitPane extends JSplitPane { 661 private DownloadSourceSizingPolicy policy; 662 private final JTabbedPane topComponent; 663 /** 664 * If the height was explicitly set by the user. 665 */ 666 private boolean heightAdjustedExplicitly; 667 668 DownloadDialogSplitPane(JTabbedPane newTopComponent, Component newBottomComponent) { 669 super(VERTICAL_SPLIT, newTopComponent, newBottomComponent); 670 this.topComponent = newTopComponent; 671 672 addComponentListener(new ComponentAdapter() { 673 @Override 674 public void componentResized(ComponentEvent e) { 675 // doLayout is called automatically when the component size decreases 676 // This seems to be the only way to call doLayout when the component size increases 677 // We need this since we sometimes want to increase the top component size. 678 revalidate(); 679 } 680 }); 681 682 addPropertyChangeListener(DIVIDER_LOCATION_PROPERTY, e -> heightAdjustedExplicitly = true); 683 } 684 685 public void setPolicy(DownloadSourceSizingPolicy policy) { 686 this.policy = policy; 687 688 super.setDividerLocation(policy.getComponentHeight() + computeOffset()); 689 setDividerSize(policy.isHeightAdjustable() ? 10 : 0); 690 setEnabled(policy.isHeightAdjustable()); 691 } 692 693 @Override 694 public void doLayout() { 695 // We need to force this height before the layout manager is run. 696 // We cannot do this in the setDividerLocation, since the offset cannot be computed there. 697 int offset = computeOffset(); 698 if (policy.isHeightAdjustable() && heightAdjustedExplicitly) { 699 policy.storeHeight(Math.max(getDividerLocation() - offset, 0)); 700 } 701 // At least 30 pixel for map, if we have enough space 702 int maxValidDividerLocation = getHeight() > 150 ? getHeight() - 40 : getHeight(); 703 704 super.setDividerLocation(Math.min(policy.getComponentHeight() + offset, maxValidDividerLocation)); 705 super.doLayout(); 706 // Order is important (set this after setDividerLocation/doLayout called the listener) 707 this.heightAdjustedExplicitly = false; 708 } 709 710 /** 711 * @return The difference between the content height and the divider location 712 */ 713 private int computeOffset() { 714 Component selectedComponent = topComponent.getSelectedComponent(); 715 return topComponent.getHeight() - (selectedComponent == null ? 0 : selectedComponent.getHeight()); 716 } 717 } 718}