001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.GraphicsEnvironment;
012import java.awt.GridBagLayout;
013import java.awt.event.ActionEvent;
014import java.awt.event.InputEvent;
015import java.awt.event.KeyEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.beans.PropertyChangeEvent;
019import java.beans.PropertyChangeListener;
020import java.lang.Character.UnicodeBlock;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Optional;
030import java.util.concurrent.TimeUnit;
031
032import javax.swing.AbstractAction;
033import javax.swing.Action;
034import javax.swing.BorderFactory;
035import javax.swing.Icon;
036import javax.swing.JButton;
037import javax.swing.JComponent;
038import javax.swing.JOptionPane;
039import javax.swing.JPanel;
040import javax.swing.JTabbedPane;
041import javax.swing.KeyStroke;
042
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.data.APIDataSet;
045import org.openstreetmap.josm.data.Version;
046import org.openstreetmap.josm.data.osm.Changeset;
047import org.openstreetmap.josm.data.osm.DataSet;
048import org.openstreetmap.josm.data.osm.OsmPrimitive;
049import org.openstreetmap.josm.gui.ExtendedDialog;
050import org.openstreetmap.josm.gui.HelpAwareOptionPane;
051import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
052import org.openstreetmap.josm.gui.help.HelpUtil;
053import org.openstreetmap.josm.gui.util.GuiHelper;
054import org.openstreetmap.josm.gui.util.MultiLineFlowLayout;
055import org.openstreetmap.josm.gui.util.WindowGeometry;
056import org.openstreetmap.josm.io.OsmApi;
057import org.openstreetmap.josm.io.UploadStrategy;
058import org.openstreetmap.josm.io.UploadStrategySpecification;
059import org.openstreetmap.josm.spi.preferences.Config;
060import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
061import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
062import org.openstreetmap.josm.spi.preferences.Setting;
063import org.openstreetmap.josm.tools.GBC;
064import org.openstreetmap.josm.tools.ImageOverlay;
065import org.openstreetmap.josm.tools.ImageProvider;
066import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
067import org.openstreetmap.josm.tools.InputMapUtils;
068import org.openstreetmap.josm.tools.Utils;
069
070/**
071 * This is a dialog for entering upload options like the parameters for
072 * the upload changeset and the strategy for opening/closing a changeset.
073 * @since 2025
074 */
075public class UploadDialog extends AbstractUploadDialog implements PropertyChangeListener, PreferenceChangedListener {
076    /** the unique instance of the upload dialog */
077    private static UploadDialog uploadDialog;
078
079    /** list of custom components that can be added by plugins at JOSM startup */
080    private static final Collection<Component> customComponents = new ArrayList<>();
081
082    /** the "created_by" changeset OSM key */
083    private static final String CREATED_BY = "created_by";
084
085    /** the panel with the objects to upload */
086    private UploadedObjectsSummaryPanel pnlUploadedObjects;
087    /** the panel to select the changeset used */
088    private ChangesetManagementPanel pnlChangesetManagement;
089
090    private BasicUploadSettingsPanel pnlBasicUploadSettings;
091
092    private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel;
093
094    /** checkbox for selecting whether an atomic upload is to be used  */
095    private TagSettingsPanel pnlTagSettings;
096    /** the tabbed pane used below of the list of primitives  */
097    private JTabbedPane tpConfigPanels;
098    /** the upload button */
099    private JButton btnUpload;
100
101    /** the changeset comment model keeping the state of the changeset comment */
102    private final transient ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel();
103    private final transient ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel();
104    private final transient ChangesetReviewModel changesetReviewModel = new ChangesetReviewModel();
105
106    private transient DataSet dataSet;
107
108    /**
109     * Constructs a new {@code UploadDialog}.
110     */
111    public UploadDialog() {
112        super(GuiHelper.getFrameForComponent(Main.parent), ModalityType.DOCUMENT_MODAL);
113        build();
114        pack();
115    }
116
117    /**
118     * Replies the unique instance of the upload dialog
119     *
120     * @return the unique instance of the upload dialog
121     */
122    public static synchronized UploadDialog getUploadDialog() {
123        if (uploadDialog == null) {
124            uploadDialog = new UploadDialog();
125        }
126        return uploadDialog;
127    }
128
129    /**
130     * builds the content panel for the upload dialog
131     *
132     * @return the content panel
133     */
134    protected JPanel buildContentPanel() {
135        JPanel pnl = new JPanel(new GridBagLayout());
136        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
137
138        // the panel with the list of uploaded objects
139        pnlUploadedObjects = new UploadedObjectsSummaryPanel();
140        pnl.add(pnlUploadedObjects, GBC.eol().fill(GBC.BOTH));
141
142        // Custom components
143        for (Component c : customComponents) {
144            pnl.add(c, GBC.eol().fill(GBC.HORIZONTAL));
145        }
146
147        // a tabbed pane with configuration panels in the lower half
148        tpConfigPanels = new CompactTabbedPane();
149
150        pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
151        tpConfigPanels.add(pnlBasicUploadSettings);
152        tpConfigPanels.setTitleAt(0, tr("Settings"));
153        tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use"));
154
155        pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
156        tpConfigPanels.add(pnlTagSettings);
157        tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
158        tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to"));
159
160        pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel);
161        tpConfigPanels.add(pnlChangesetManagement);
162        tpConfigPanels.setTitleAt(2, tr("Changesets"));
163        tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to"));
164
165        pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel();
166        tpConfigPanels.add(pnlUploadStrategySelectionPanel);
167        tpConfigPanels.setTitleAt(3, tr("Advanced"));
168        tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings"));
169
170        pnl.add(tpConfigPanels, GBC.eol().fill(GBC.HORIZONTAL));
171
172        pnl.add(buildActionPanel(), GBC.eol().fill(GBC.HORIZONTAL));
173        return pnl;
174    }
175
176    /**
177     * builds the panel with the OK and CANCEL buttons
178     *
179     * @return The panel with the OK and CANCEL buttons
180     */
181    protected JPanel buildActionPanel() {
182        JPanel pnl = new JPanel(new MultiLineFlowLayout(FlowLayout.CENTER));
183        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
184
185        // -- upload button
186        btnUpload = new JButton(new UploadAction(this));
187        pnl.add(btnUpload);
188        btnUpload.setFocusable(true);
189        InputMapUtils.enableEnter(btnUpload);
190        bindCtrlEnterToAction(getRootPane(), btnUpload.getAction());
191
192        // -- cancel button
193        CancelAction cancelAction = new CancelAction(this);
194        pnl.add(new JButton(cancelAction));
195        InputMapUtils.addEscapeAction(getRootPane(), cancelAction);
196        pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload"))));
197        HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/Upload"));
198        return pnl;
199    }
200
201    /**
202     * builds the gui
203     */
204    protected void build() {
205        setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl()));
206        setContentPane(buildContentPanel());
207
208        addWindowListener(new WindowEventHandler());
209
210        // make sure the configuration panels listen to each other changes
211        //
212        pnlChangesetManagement.addPropertyChangeListener(this);
213        pnlChangesetManagement.addPropertyChangeListener(
214                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
215        );
216        pnlChangesetManagement.addPropertyChangeListener(this);
217        pnlUploadedObjects.addPropertyChangeListener(
218                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
219        );
220        pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
221        pnlUploadStrategySelectionPanel.addPropertyChangeListener(
222                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
223        );
224
225        // users can click on either of two links in the upload parameter
226        // summary handler. This installs the handler for these two events.
227        // We simply select the appropriate tab in the tabbed pane with the configuration dialogs.
228        //
229        pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener(
230                new ConfigurationParameterRequestHandler() {
231                    @Override
232                    public void handleUploadStrategyConfigurationRequest() {
233                        tpConfigPanels.setSelectedIndex(3);
234                    }
235
236                    @Override
237                    public void handleChangesetConfigurationRequest() {
238                        tpConfigPanels.setSelectedIndex(2);
239                    }
240                }
241        );
242
243        pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers(
244                new AbstractAction() {
245                    @Override
246                    public void actionPerformed(ActionEvent e) {
247                        btnUpload.requestFocusInWindow();
248                    }
249                }
250        );
251
252        setMinimumSize(new Dimension(600, 350));
253
254        Config.getPref().addPreferenceChangeListener(this);
255    }
256
257    /**
258     * Sets the collection of primitives to upload
259     *
260     * @param toUpload the dataset with the objects to upload. If null, assumes the empty
261     * set of objects to upload
262     *
263     */
264    public void setUploadedPrimitives(APIDataSet toUpload) {
265        if (toUpload == null) {
266            List<OsmPrimitive> emptyList = Collections.emptyList();
267            pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList);
268            return;
269        }
270        pnlUploadedObjects.setUploadedPrimitives(
271                toUpload.getPrimitivesToAdd(),
272                toUpload.getPrimitivesToUpdate(),
273                toUpload.getPrimitivesToDelete()
274        );
275    }
276
277    /**
278     * Sets the tags for this upload based on (later items overwrite earlier ones):
279     * <ul>
280     * <li>previous "source" and "comment" input</li>
281     * <li>the tags set in the dataset (see {@link DataSet#getChangeSetTags()})</li>
282     * <li>the tags from the selected open changeset</li>
283     * <li>the JOSM user agent (see {@link Version#getAgentString(boolean)})</li>
284     * </ul>
285     *
286     * @param dataSet to obtain the tags set in the dataset
287     */
288    public void setChangesetTags(DataSet dataSet) {
289        final Map<String, String> tags = new HashMap<>();
290
291        // obtain from previous input
292        tags.put("source", getLastChangesetSourceFromHistory());
293        tags.put("comment", getLastChangesetCommentFromHistory());
294
295        // obtain from dataset
296        if (dataSet != null) {
297            tags.putAll(dataSet.getChangeSetTags());
298        }
299        this.dataSet = dataSet;
300
301        // obtain from selected open changeset
302        if (pnlChangesetManagement.getSelectedChangeset() != null) {
303            tags.putAll(pnlChangesetManagement.getSelectedChangeset().getKeys());
304        }
305
306        // set/adapt created_by
307        final String agent = Version.getInstance().getAgentString(false);
308        final String createdBy = tags.get(CREATED_BY);
309        if (createdBy == null || createdBy.isEmpty()) {
310            tags.put(CREATED_BY, agent);
311        } else if (!createdBy.contains(agent)) {
312            tags.put(CREATED_BY, createdBy + ';' + agent);
313        }
314
315        // remove empty values
316        final Iterator<String> it = tags.keySet().iterator();
317        while (it.hasNext()) {
318            final String v = tags.get(it.next());
319            if (v == null || v.isEmpty()) {
320                it.remove();
321            }
322        }
323
324        pnlTagSettings.initFromTags(tags);
325        pnlTagSettings.tableChanged(null);
326    }
327
328    @Override
329    public void rememberUserInput() {
330        pnlBasicUploadSettings.rememberUserInput();
331        pnlUploadStrategySelectionPanel.rememberUserInput();
332    }
333
334    /**
335     * Initializes the panel for user input
336     */
337    public void startUserInput() {
338        tpConfigPanels.setSelectedIndex(0);
339        pnlBasicUploadSettings.startUserInput();
340        pnlTagSettings.startUserInput();
341        pnlUploadStrategySelectionPanel.initFromPreferences();
342        UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
343        pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
344        pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
345        pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload());
346    }
347
348    /**
349     * Replies the current changeset
350     *
351     * @return the current changeset
352     */
353    public Changeset getChangeset() {
354        Changeset cs = Optional.ofNullable(pnlChangesetManagement.getSelectedChangeset()).orElseGet(Changeset::new);
355        cs.setKeys(pnlTagSettings.getTags(false));
356        return cs;
357    }
358
359    /**
360     * Sets the changeset to be used in the next upload
361     *
362     * @param cs the changeset
363     */
364    public void setSelectedChangesetForNextUpload(Changeset cs) {
365        pnlChangesetManagement.setSelectedChangesetForNextUpload(cs);
366    }
367
368    @Override
369    public UploadStrategySpecification getUploadStrategySpecification() {
370        UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification();
371        spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
372        return spec;
373    }
374
375    @Override
376    public String getUploadComment() {
377        return changesetCommentModel.getComment();
378    }
379
380    @Override
381    public String getUploadSource() {
382        return changesetSourceModel.getComment();
383    }
384
385    @Override
386    public void setVisible(boolean visible) {
387        if (visible) {
388            new WindowGeometry(
389                    getClass().getName() + ".geometry",
390                    WindowGeometry.centerInWindow(
391                            Main.parent,
392                            new Dimension(400, 600)
393                    )
394            ).applySafe(this);
395            startUserInput();
396        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
397            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
398        }
399        super.setVisible(visible);
400    }
401
402    /**
403     * Adds a custom component to this dialog.
404     * Custom components added at JOSM startup are displayed between the objects list and the properties tab pane.
405     * @param c The custom component to add. If {@code null}, this method does nothing.
406     * @return {@code true} if the collection of custom components changed as a result of the call
407     * @since 5842
408     */
409    public static boolean addCustomComponent(Component c) {
410        if (c != null) {
411            return customComponents.add(c);
412        }
413        return false;
414    }
415
416    static final class CompactTabbedPane extends JTabbedPane {
417        @Override
418        public Dimension getPreferredSize() {
419            // make sure the tabbed pane never grabs more space than necessary
420            return super.getMinimumSize();
421        }
422    }
423
424    /**
425     * Handles an upload.
426     */
427    static class UploadAction extends AbstractAction {
428
429        private final transient IUploadDialog dialog;
430
431        UploadAction(IUploadDialog dialog) {
432            this.dialog = dialog;
433            putValue(NAME, tr("Upload Changes"));
434            new ImageProvider("upload").getResource().attachImageIcon(this, true);
435            putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives"));
436        }
437
438        /**
439         * Displays a warning message indicating that the upload comment is empty/short.
440         * @return true if the user wants to revisit, false if they want to continue
441         */
442        protected boolean warnUploadComment() {
443            return warnUploadTag(
444                    tr("Please revise upload comment"),
445                    tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" +
446                            "This is technically allowed, but please consider that many users who are<br />" +
447                            "watching changes in their area depend on meaningful changeset comments<br />" +
448                            "to understand what is going on!<br /><br />" +
449                            "If you spend a minute now to explain your change, you will make life<br />" +
450                            "easier for many other mappers."),
451                    "upload_comment_is_empty_or_very_short"
452            );
453        }
454
455        /**
456         * Displays a warning message indicating that no changeset source is given.
457         * @return true if the user wants to revisit, false if they want to continue
458         */
459        protected boolean warnUploadSource() {
460            return warnUploadTag(
461                    tr("Please specify a changeset source"),
462                    tr("You did not specify a source for your changes.<br />" +
463                            "It is technically allowed, but this information helps<br />" +
464                            "other users to understand the origins of the data.<br /><br />" +
465                            "If you spend a minute now to explain your change, you will make life<br />" +
466                            "easier for many other mappers."),
467                    "upload_source_is_empty"
468            );
469        }
470
471        protected boolean warnUploadTag(final String title, final String message, final String togglePref) {
472            String[] buttonTexts = new String[] {tr("Revise"), tr("Cancel"), tr("Continue as is")};
473            Icon[] buttonIcons = new Icon[] {
474                    new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).get(),
475                    new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(),
476                    new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay(
477                            new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()};
478            String[] tooltips = new String[] {
479                    tr("Return to the previous dialog to enter a more descriptive comment"),
480                    tr("Cancel and return to the previous dialog"),
481                    tr("Ignore this hint and upload anyway")};
482
483            if (GraphicsEnvironment.isHeadless()) {
484                return false;
485            }
486
487            ExtendedDialog dlg = new ExtendedDialog((Component) dialog, title, buttonTexts) {
488                @Override
489                public void setupDialog() {
490                    super.setupDialog();
491                    bindCtrlEnterToAction(getRootPane(), buttons.get(buttons.size() - 1).getAction());
492                }
493            };
494            dlg.setContent("<html>" + message + "</html>");
495            dlg.setButtonIcons(buttonIcons);
496            dlg.setToolTipTexts(tooltips);
497            dlg.setIcon(JOptionPane.WARNING_MESSAGE);
498            dlg.toggleEnable(togglePref);
499            dlg.setCancelButton(1, 2);
500            return dlg.showDialog().getValue() != 3;
501        }
502
503        protected void warnIllegalChunkSize() {
504            HelpAwareOptionPane.showOptionDialog(
505                    (Component) dialog,
506                    tr("Please enter a valid chunk size first"),
507                    tr("Illegal chunk size"),
508                    JOptionPane.ERROR_MESSAGE,
509                    ht("/Dialog/Upload#IllegalChunkSize")
510            );
511        }
512
513        static boolean isUploadCommentTooShort(String comment) {
514            String s = comment.trim();
515            boolean result = true;
516            if (!s.isEmpty()) {
517                UnicodeBlock block = Character.UnicodeBlock.of(s.charAt(0));
518                if (block != null && block.toString().contains("CJK")) {
519                    result = s.length() < 4;
520                } else {
521                    result = s.length() < 10;
522                }
523            }
524            return result;
525        }
526
527        @Override
528        public void actionPerformed(ActionEvent e) {
529            if (isUploadCommentTooShort(dialog.getUploadComment()) && warnUploadComment()) {
530                // abort for missing comment
531                dialog.handleMissingComment();
532                return;
533            }
534            if (dialog.getUploadSource().trim().isEmpty() && warnUploadSource()) {
535                // abort for missing changeset source
536                dialog.handleMissingSource();
537                return;
538            }
539
540            /* test for empty tags in the changeset metadata and proceed only after user's confirmation.
541             * though, accept if key and value are empty (cf. xor). */
542            List<String> emptyChangesetTags = new ArrayList<>();
543            for (final Entry<String, String> i : dialog.getTags(true).entrySet()) {
544                final boolean isKeyEmpty = i.getKey() == null || i.getKey().trim().isEmpty();
545                final boolean isValueEmpty = i.getValue() == null || i.getValue().trim().isEmpty();
546                final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey());
547                if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) {
548                    emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue()));
549                }
550            }
551            if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(
552                    Main.parent,
553                    trn(
554                            "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>",
555                            "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>",
556                            emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)),
557                    tr("Empty metadata"),
558                    JOptionPane.OK_CANCEL_OPTION,
559                    JOptionPane.WARNING_MESSAGE
560            )) {
561                dialog.handleMissingComment();
562                return;
563            }
564
565            UploadStrategySpecification strategy = dialog.getUploadStrategySpecification();
566            if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)
567                    && strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
568                warnIllegalChunkSize();
569                dialog.handleIllegalChunkSize();
570                return;
571            }
572            if (dialog instanceof AbstractUploadDialog) {
573                ((AbstractUploadDialog) dialog).setCanceled(false);
574                ((AbstractUploadDialog) dialog).setVisible(false);
575            }
576        }
577    }
578
579    /**
580     * Action for canceling the dialog.
581     */
582    static class CancelAction extends AbstractAction {
583
584        private final transient IUploadDialog dialog;
585
586        CancelAction(IUploadDialog dialog) {
587            this.dialog = dialog;
588            putValue(NAME, tr("Cancel"));
589            new ImageProvider("cancel").getResource().attachImageIcon(this, true);
590            putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing"));
591        }
592
593        @Override
594        public void actionPerformed(ActionEvent e) {
595            if (dialog instanceof AbstractUploadDialog) {
596                ((AbstractUploadDialog) dialog).setCanceled(true);
597                ((AbstractUploadDialog) dialog).setVisible(false);
598            }
599        }
600    }
601
602    /**
603     * Listens to window closing events and processes them as cancel events.
604     * Listens to window open events and initializes user input
605     */
606    class WindowEventHandler extends WindowAdapter {
607        private boolean activatedOnce;
608
609        @Override
610        public void windowClosing(WindowEvent e) {
611            setCanceled(true);
612        }
613
614        @Override
615        public void windowActivated(WindowEvent e) {
616            if (!activatedOnce && tpConfigPanels.getSelectedIndex() == 0) {
617                pnlBasicUploadSettings.initEditingOfUploadComment();
618                activatedOnce = true;
619            }
620        }
621    }
622
623    /* -------------------------------------------------------------------------- */
624    /* Interface PropertyChangeListener                                           */
625    /* -------------------------------------------------------------------------- */
626    @Override
627    public void propertyChange(PropertyChangeEvent evt) {
628        if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) {
629            Changeset cs = (Changeset) evt.getNewValue();
630            setChangesetTags(dataSet);
631            if (cs == null) {
632                tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
633            } else {
634                tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId()));
635            }
636        }
637    }
638
639    /* -------------------------------------------------------------------------- */
640    /* Interface PreferenceChangedListener                                        */
641    /* -------------------------------------------------------------------------- */
642    @Override
643    public void preferenceChanged(PreferenceChangeEvent e) {
644        if (e.getKey() == null || !"osm-server.url".equals(e.getKey()))
645            return;
646        final Setting<?> newValue = e.getNewValue();
647        final String url;
648        if (newValue == null || newValue.getValue() == null) {
649            url = OsmApi.getOsmApi().getBaseUrl();
650        } else {
651            url = newValue.getValue().toString();
652        }
653        setTitle(tr("Upload to ''{0}''", url));
654    }
655
656    private static String getLastChangesetTagFromHistory(String historyKey, List<String> def) {
657        Collection<String> history = Config.getPref().getList(historyKey, def);
658        int age = (int) (System.currentTimeMillis() / 1000 - Config.getPref().getInt(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0));
659        if (history != null && age < Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_MAX_AGE_KEY, TimeUnit.HOURS.toMillis(4))
660                && !history.isEmpty()) {
661            return history.iterator().next();
662        } else {
663            return null;
664        }
665    }
666
667    /**
668     * Returns the last changeset comment from history.
669     * @return the last changeset comment from history
670     */
671    public String getLastChangesetCommentFromHistory() {
672        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY, new ArrayList<String>());
673    }
674
675    /**
676     * Returns the last changeset source from history.
677     * @return the last changeset source from history
678     */
679    public String getLastChangesetSourceFromHistory() {
680        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources());
681    }
682
683    @Override
684    public Map<String, String> getTags(boolean keepEmpty) {
685        return pnlTagSettings.getTags(keepEmpty);
686    }
687
688    @Override
689    public void handleMissingComment() {
690        tpConfigPanels.setSelectedIndex(0);
691        pnlBasicUploadSettings.initEditingOfUploadComment();
692    }
693
694    @Override
695    public void handleMissingSource() {
696        tpConfigPanels.setSelectedIndex(0);
697        pnlBasicUploadSettings.initEditingOfUploadSource();
698    }
699
700    @Override
701    public void handleIllegalChunkSize() {
702        tpConfigPanels.setSelectedIndex(0);
703    }
704
705    private static void bindCtrlEnterToAction(JComponent component, Action actionToBind) {
706        final KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK);
707        component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(stroke, "ctrl_enter");
708        component.getActionMap().put("ctrl_enter", actionToBind);
709    }
710}