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