001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010import java.awt.event.ActionEvent;
011import java.awt.event.ItemEvent;
012import java.awt.event.ItemListener;
013import java.util.Collections;
014
015import javax.swing.AbstractAction;
016import javax.swing.BorderFactory;
017import javax.swing.ButtonGroup;
018import javax.swing.JButton;
019import javax.swing.JCheckBox;
020import javax.swing.JPanel;
021import javax.swing.JRadioButton;
022import javax.swing.event.ListDataEvent;
023import javax.swing.event.ListDataListener;
024
025import org.openstreetmap.josm.Main;
026import org.openstreetmap.josm.data.osm.Changeset;
027import org.openstreetmap.josm.data.osm.ChangesetCache;
028import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
029import org.openstreetmap.josm.gui.widgets.JosmComboBox;
030import org.openstreetmap.josm.tools.CheckParameterUtil;
031import org.openstreetmap.josm.tools.ImageProvider;
032
033/**
034 * ChangesetManagementPanel allows to configure changeset to be used in the next
035 * upload.
036 *
037 * It is displayed as one of the configuration panels in the {@link UploadDialog}.
038 *
039 * ChangesetManagementPanel is a source for {@link java.beans.PropertyChangeEvent}s. Clients can listen
040 * to
041 * <ul>
042 *   <li>{@link #SELECTED_CHANGESET_PROP}  - the new value in the property change event is
043 *   the changeset selected by the user. The value is null if the user didn't select a
044 *   a changeset or if he chosed to use a new changeset.</li>
045 *   <li> {@link #CLOSE_CHANGESET_AFTER_UPLOAD} - the new value is a boolean value indicating
046 *   whether the changeset should be closed after the next upload</li>
047 * </ul>
048 */
049public class ChangesetManagementPanel extends JPanel implements ListDataListener {
050    public static final String SELECTED_CHANGESET_PROP = ChangesetManagementPanel.class.getName() + ".selectedChangeset";
051    public static final String CLOSE_CHANGESET_AFTER_UPLOAD = ChangesetManagementPanel.class.getName() + ".closeChangesetAfterUpload";
052
053    private JRadioButton rbUseNew;
054    private JRadioButton rbExisting;
055    private JosmComboBox<Changeset> cbOpenChangesets;
056    private JCheckBox cbCloseAfterUpload;
057    private OpenChangesetComboBoxModel model;
058    private final transient ChangesetCommentModel changesetCommentModel;
059
060    /**
061     * builds the GUI
062     */
063    protected void build() {
064        setLayout(new GridBagLayout());
065        GridBagConstraints gc = new GridBagConstraints();
066        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
067
068        ButtonGroup bgUseNewOrExisting = new ButtonGroup();
069
070        gc.gridwidth = 4;
071        gc.gridx = 0;
072        gc.gridy = 0;
073        gc.fill = GridBagConstraints.HORIZONTAL;
074        gc.weightx = 1.0;
075        gc.weighty = 0.0;
076        gc.insets = new Insets(0, 0, 5, 0);
077        add(new JMultilineLabel(
078                tr("Please decide what changeset the data is uploaded to and whether to close the changeset after the next upload.")), gc);
079
080        gc.gridwidth = 4;
081        gc.gridy = 1;
082        gc.fill = GridBagConstraints.HORIZONTAL;
083        gc.weightx = 1.0;
084        gc.weighty = 0.0;
085        gc.insets = new Insets(0, 0, 0, 0);
086        gc.anchor = GridBagConstraints.FIRST_LINE_START;
087        rbUseNew = new JRadioButton(tr("Upload to a new changeset"));
088        rbUseNew.setToolTipText(tr("Open a new changeset and use it in the next upload"));
089        bgUseNewOrExisting.add(rbUseNew);
090        add(rbUseNew, gc);
091
092        gc.gridx = 0;
093        gc.gridy = 2;
094        gc.gridwidth = 1;
095        gc.weightx = 0.0;
096        gc.fill = GridBagConstraints.HORIZONTAL;
097        rbExisting = new JRadioButton(tr("Upload to an existing changeset"));
098        rbExisting.setToolTipText(tr("Upload data to an already existing and open changeset"));
099        bgUseNewOrExisting.add(rbExisting);
100        add(rbExisting, gc);
101
102        gc.gridx = 1;
103        gc.gridy = 2;
104        gc.gridwidth = 1;
105        gc.weightx = 1.0;
106        model = new OpenChangesetComboBoxModel();
107        ChangesetCache.getInstance().addChangesetCacheListener(model);
108        cbOpenChangesets = new JosmComboBox<>(model);
109        cbOpenChangesets.setToolTipText(tr("Select an open changeset"));
110        cbOpenChangesets.setRenderer(new ChangesetCellRenderer());
111        cbOpenChangesets.addItemListener(new ChangesetListItemStateListener());
112        Dimension d = cbOpenChangesets.getPreferredSize();
113        d.width = 200;
114        cbOpenChangesets.setPreferredSize(d);
115        d.width = 100;
116        cbOpenChangesets.setMinimumSize(d);
117        model.addListDataListener(this);
118        add(cbOpenChangesets, gc);
119
120        gc.gridx = 2;
121        gc.gridy = 2;
122        gc.weightx = 0.0;
123        gc.gridwidth = 1;
124        gc.weightx = 0.0;
125        JButton btnRefresh = new JButton(new RefreshAction());
126        btnRefresh.setMargin(new Insets(0, 0, 0, 0));
127        add(btnRefresh, gc);
128
129        gc.gridx = 3;
130        gc.gridy = 2;
131        gc.gridwidth = 1;
132        CloseChangesetAction closeChangesetAction = new CloseChangesetAction();
133        JButton btnClose = new JButton(closeChangesetAction);
134        btnClose.setMargin(new Insets(0, 0, 0, 0));
135        cbOpenChangesets.addItemListener(closeChangesetAction);
136        rbExisting.addItemListener(closeChangesetAction);
137        add(btnClose, gc);
138
139        gc.gridx = 0;
140        gc.gridy = 3;
141        gc.gridwidth = 4;
142        gc.weightx = 1.0;
143        cbCloseAfterUpload = new JCheckBox(tr("Close changeset after upload"));
144        cbCloseAfterUpload.setToolTipText(tr("Select to close the changeset after the next upload"));
145        add(cbCloseAfterUpload, gc);
146        cbCloseAfterUpload.setSelected(Main.pref.getBoolean("upload.changeset.close", true));
147        cbCloseAfterUpload.addItemListener(new CloseAfterUploadItemStateListener());
148
149        gc.gridx = 0;
150        gc.gridy = 5;
151        gc.gridwidth = 4;
152        gc.weightx = 1.0;
153        gc.weighty = 1.0;
154        gc.fill = GridBagConstraints.BOTH;
155        add(new JPanel(), gc);
156
157        rbUseNew.getModel().addItemListener(new RadioButtonHandler());
158        rbExisting.getModel().addItemListener(new RadioButtonHandler());
159    }
160
161    /**
162     * Creates a new panel
163     *
164     * @param changesetCommentModel the changeset comment model. Must not be null.
165     * @throws IllegalArgumentException if {@code changesetCommentModel} is null
166     */
167    public ChangesetManagementPanel(ChangesetCommentModel changesetCommentModel) {
168        CheckParameterUtil.ensureParameterNotNull(changesetCommentModel, "changesetCommentModel");
169        this.changesetCommentModel = changesetCommentModel;
170        build();
171        refreshGUI();
172    }
173
174    protected void refreshGUI() {
175        rbExisting.setEnabled(model.getSize() > 0);
176        if (model.getSize() == 0) {
177            if (!rbUseNew.isSelected()) {
178                rbUseNew.setSelected(true);
179            }
180        }
181        cbOpenChangesets.setEnabled(model.getSize() > 0 && rbExisting.isSelected());
182    }
183
184    /**
185     * Sets the changeset to be used in the next upload
186     *
187     * @param cs the changeset
188     */
189    public void setSelectedChangesetForNextUpload(Changeset cs) {
190        int idx  = model.getIndexOf(cs);
191        if (idx >= 0) {
192            rbExisting.setSelected(true);
193            model.setSelectedItem(cs);
194        }
195    }
196
197    /**
198     * Replies the currently selected changeset. null, if no changeset is
199     * selected or if the user has chosen to use a new changeset.
200     *
201     * @return the currently selected changeset. null, if no changeset is
202     * selected.
203     */
204    public Changeset getSelectedChangeset() {
205        if (rbUseNew.isSelected())
206            return null;
207        return (Changeset) cbOpenChangesets.getSelectedItem();
208    }
209
210    /**
211     * Determines if the user has chosen to close the changeset after the next upload.
212     * @return {@code true} if the user has chosen to close the changeset after the next upload
213     */
214    public boolean isCloseChangesetAfterUpload() {
215        return cbCloseAfterUpload.isSelected();
216    }
217
218    /* ---------------------------------------------------------------------------- */
219    /* Interface ListDataListener                                                   */
220    /* ---------------------------------------------------------------------------- */
221    @Override
222    public void contentsChanged(ListDataEvent e) {
223        refreshGUI();
224    }
225
226    @Override
227    public void intervalAdded(ListDataEvent e) {
228        refreshGUI();
229    }
230
231    @Override
232    public void intervalRemoved(ListDataEvent e) {
233        refreshGUI();
234    }
235
236    /**
237     * Listens to changes in the selected changeset and fires property
238     * change events.
239     *
240     */
241    class ChangesetListItemStateListener implements ItemListener {
242        @Override
243        public void itemStateChanged(ItemEvent e) {
244            Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
245            if (cs == null) return;
246            if (rbExisting.isSelected()) {
247                firePropertyChange(SELECTED_CHANGESET_PROP, null, cs);
248            }
249        }
250    }
251
252    /**
253     * Listens to changes in "close after upload" flag and fires
254     * property change events.
255     *
256     */
257    class CloseAfterUploadItemStateListener implements ItemListener {
258        @Override
259        public void itemStateChanged(ItemEvent e) {
260            if (e.getItemSelectable() != cbCloseAfterUpload)
261                return;
262            switch(e.getStateChange()) {
263            case ItemEvent.SELECTED:
264                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, false, true);
265                Main.pref.put("upload.changeset.close", true);
266                break;
267            case ItemEvent.DESELECTED:
268                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, true, false);
269                Main.pref.put("upload.changeset.close", false);
270                break;
271            }
272        }
273    }
274
275    /**
276     * Listens to changes in the two radio buttons rbUseNew and rbUseExisting.
277     *
278     */
279    class RadioButtonHandler implements ItemListener {
280        @Override
281        public void itemStateChanged(ItemEvent e) {
282            if (rbUseNew.isSelected()) {
283                cbOpenChangesets.setEnabled(false);
284                firePropertyChange(SELECTED_CHANGESET_PROP, null, null);
285            } else if (rbExisting.isSelected()) {
286                cbOpenChangesets.setEnabled(true);
287                if (cbOpenChangesets.getSelectedItem() == null) {
288                    model.selectFirstChangeset();
289                }
290                Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
291                if (cs == null) return;
292                firePropertyChange(SELECTED_CHANGESET_PROP, null, cs);
293            }
294        }
295    }
296
297    /**
298     * Refreshes the list of open changesets
299     *
300     */
301    class RefreshAction extends AbstractAction {
302        RefreshAction() {
303            putValue(SHORT_DESCRIPTION, tr("Load the list of your open changesets from the server"));
304            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
305        }
306
307        @Override
308        public void actionPerformed(ActionEvent e) {
309            DownloadOpenChangesetsTask task = new DownloadOpenChangesetsTask(ChangesetManagementPanel.this);
310            Main.worker.submit(task);
311        }
312    }
313
314    /**
315     * Closes the currently selected changeset
316     *
317     */
318    class CloseChangesetAction extends AbstractAction implements ItemListener {
319        CloseChangesetAction() {
320            putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
321            putValue(SHORT_DESCRIPTION, tr("Close the currently selected open changeset"));
322            refreshEnabledState();
323        }
324
325        @Override
326        public void actionPerformed(ActionEvent e) {
327            Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
328            if (cs == null) return;
329            CloseChangesetTask task = new CloseChangesetTask(Collections.singletonList(cs));
330            Main.worker.submit(task);
331        }
332
333        protected void refreshEnabledState() {
334            setEnabled(
335                    cbOpenChangesets.getModel().getSize() > 0
336                    && cbOpenChangesets.getSelectedItem() != null
337                    && rbExisting.isSelected()
338            );
339        }
340
341        @Override
342        public void itemStateChanged(ItemEvent e) {
343            refreshEnabledState();
344        }
345    }
346}