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    static final String SELECTED_CHANGESET_PROP = ChangesetManagementPanel.class.getName() + ".selectedChangeset";
051    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
059    /**
060     * Constructs a new {@code ChangesetManagementPanel}.
061     *
062     * @param changesetCommentModel the changeset comment model. Must not be null.
063     * @throws IllegalArgumentException if {@code changesetCommentModel} is null
064     */
065    public ChangesetManagementPanel(ChangesetCommentModel changesetCommentModel) {
066        CheckParameterUtil.ensureParameterNotNull(changesetCommentModel, "changesetCommentModel");
067        build();
068        refreshGUI();
069    }
070
071    /**
072     * builds the GUI
073     */
074    protected void build() {
075        setLayout(new GridBagLayout());
076        GridBagConstraints gc = new GridBagConstraints();
077        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
078
079        ButtonGroup bgUseNewOrExisting = new ButtonGroup();
080
081        gc.gridwidth = 4;
082        gc.gridx = 0;
083        gc.gridy = 0;
084        gc.fill = GridBagConstraints.HORIZONTAL;
085        gc.weightx = 1.0;
086        gc.weighty = 0.0;
087        gc.insets = new Insets(0, 0, 5, 0);
088        add(new JMultilineLabel(
089                tr("Please decide what changeset the data is uploaded to and whether to close the changeset after the next upload.")), gc);
090
091        gc.gridwidth = 4;
092        gc.gridy = 1;
093        gc.fill = GridBagConstraints.HORIZONTAL;
094        gc.weightx = 1.0;
095        gc.weighty = 0.0;
096        gc.insets = new Insets(0, 0, 0, 0);
097        gc.anchor = GridBagConstraints.FIRST_LINE_START;
098        rbUseNew = new JRadioButton(tr("Upload to a new changeset"));
099        rbUseNew.setToolTipText(tr("Open a new changeset and use it in the next upload"));
100        bgUseNewOrExisting.add(rbUseNew);
101        add(rbUseNew, gc);
102
103        gc.gridx = 0;
104        gc.gridy = 2;
105        gc.gridwidth = 1;
106        gc.weightx = 0.0;
107        gc.fill = GridBagConstraints.HORIZONTAL;
108        rbExisting = new JRadioButton(tr("Upload to an existing changeset"));
109        rbExisting.setToolTipText(tr("Upload data to an already existing and open changeset"));
110        bgUseNewOrExisting.add(rbExisting);
111        add(rbExisting, gc);
112
113        gc.gridx = 1;
114        gc.gridy = 2;
115        gc.gridwidth = 1;
116        gc.weightx = 1.0;
117        model = new OpenChangesetComboBoxModel();
118        ChangesetCache.getInstance().addChangesetCacheListener(model);
119        cbOpenChangesets = new JosmComboBox<>(model);
120        cbOpenChangesets.setToolTipText(tr("Select an open changeset"));
121        cbOpenChangesets.setRenderer(new ChangesetCellRenderer());
122        cbOpenChangesets.addItemListener(new ChangesetListItemStateListener());
123        Dimension d = cbOpenChangesets.getPreferredSize();
124        d.width = 200;
125        cbOpenChangesets.setPreferredSize(d);
126        d.width = 100;
127        cbOpenChangesets.setMinimumSize(d);
128        model.addListDataListener(this);
129        add(cbOpenChangesets, gc);
130
131        gc.gridx = 2;
132        gc.gridy = 2;
133        gc.weightx = 0.0;
134        gc.gridwidth = 1;
135        gc.weightx = 0.0;
136        JButton btnRefresh = new JButton(new RefreshAction());
137        btnRefresh.setMargin(new Insets(0, 0, 0, 0));
138        add(btnRefresh, gc);
139
140        gc.gridx = 3;
141        gc.gridy = 2;
142        gc.gridwidth = 1;
143        CloseChangesetAction closeChangesetAction = new CloseChangesetAction();
144        JButton btnClose = new JButton(closeChangesetAction);
145        btnClose.setMargin(new Insets(0, 0, 0, 0));
146        cbOpenChangesets.addItemListener(closeChangesetAction);
147        rbExisting.addItemListener(closeChangesetAction);
148        add(btnClose, gc);
149
150        gc.gridx = 0;
151        gc.gridy = 3;
152        gc.gridwidth = 4;
153        gc.weightx = 1.0;
154        cbCloseAfterUpload = new JCheckBox(tr("Close changeset after upload"));
155        cbCloseAfterUpload.setToolTipText(tr("Select to close the changeset after the next upload"));
156        add(cbCloseAfterUpload, gc);
157        cbCloseAfterUpload.setSelected(Main.pref.getBoolean("upload.changeset.close", true));
158        cbCloseAfterUpload.addItemListener(new CloseAfterUploadItemStateListener());
159
160        gc.gridx = 0;
161        gc.gridy = 5;
162        gc.gridwidth = 4;
163        gc.weightx = 1.0;
164        gc.weighty = 1.0;
165        gc.fill = GridBagConstraints.BOTH;
166        add(new JPanel(), gc);
167
168        rbUseNew.getModel().addItemListener(new RadioButtonHandler());
169        rbExisting.getModel().addItemListener(new RadioButtonHandler());
170    }
171
172    protected void refreshGUI() {
173        rbExisting.setEnabled(model.getSize() > 0);
174        if (model.getSize() == 0 && !rbUseNew.isSelected()) {
175            rbUseNew.setSelected(true);
176        }
177        cbOpenChangesets.setEnabled(model.getSize() > 0 && rbExisting.isSelected());
178    }
179
180    /**
181     * Sets the changeset to be used in the next upload
182     *
183     * @param cs the changeset
184     */
185    public void setSelectedChangesetForNextUpload(Changeset cs) {
186        int idx  = model.getIndexOf(cs);
187        if (idx >= 0) {
188            rbExisting.setSelected(true);
189            model.setSelectedItem(cs);
190        }
191    }
192
193    /**
194     * Replies the currently selected changeset. null, if no changeset is
195     * selected or if the user has chosen to use a new changeset.
196     *
197     * @return the currently selected changeset. null, if no changeset is
198     * selected.
199     */
200    public Changeset getSelectedChangeset() {
201        if (rbUseNew.isSelected())
202            return null;
203        return (Changeset) cbOpenChangesets.getSelectedItem();
204    }
205
206    /**
207     * Determines if the user has chosen to close the changeset after the next upload.
208     * @return {@code true} if the user has chosen to close the changeset after the next upload
209     */
210    public boolean isCloseChangesetAfterUpload() {
211        return cbCloseAfterUpload.isSelected();
212    }
213
214    /* ---------------------------------------------------------------------------- */
215    /* Interface ListDataListener                                                   */
216    /* ---------------------------------------------------------------------------- */
217    @Override
218    public void contentsChanged(ListDataEvent e) {
219        refreshGUI();
220    }
221
222    @Override
223    public void intervalAdded(ListDataEvent e) {
224        refreshGUI();
225    }
226
227    @Override
228    public void intervalRemoved(ListDataEvent e) {
229        refreshGUI();
230    }
231
232    /**
233     * Listens to changes in the selected changeset and fires property change events.
234     */
235    class ChangesetListItemStateListener implements ItemListener {
236        @Override
237        public void itemStateChanged(ItemEvent e) {
238            Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
239            if (cs == null) return;
240            if (rbExisting.isSelected()) {
241                firePropertyChange(SELECTED_CHANGESET_PROP, null, cs);
242            }
243        }
244    }
245
246    /**
247     * Listens to changes in "close after upload" flag and fires property change events.
248     */
249    class CloseAfterUploadItemStateListener implements ItemListener {
250        @Override
251        public void itemStateChanged(ItemEvent e) {
252            if (e.getItemSelectable() != cbCloseAfterUpload)
253                return;
254            switch(e.getStateChange()) {
255            case ItemEvent.SELECTED:
256                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, false, true);
257                Main.pref.put("upload.changeset.close", true);
258                break;
259            case ItemEvent.DESELECTED:
260                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, true, false);
261                Main.pref.put("upload.changeset.close", false);
262                break;
263            default: // Do nothing
264            }
265        }
266    }
267
268    /**
269     * Listens to changes in the two radio buttons rbUseNew and rbUseExisting.
270     */
271    class RadioButtonHandler implements ItemListener {
272        @Override
273        public void itemStateChanged(ItemEvent e) {
274            if (rbUseNew.isSelected()) {
275                cbOpenChangesets.setEnabled(false);
276                firePropertyChange(SELECTED_CHANGESET_PROP, null, null);
277            } else if (rbExisting.isSelected()) {
278                cbOpenChangesets.setEnabled(true);
279                if (cbOpenChangesets.getSelectedItem() == null) {
280                    model.selectFirstChangeset();
281                }
282                Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
283                if (cs == null) return;
284                firePropertyChange(SELECTED_CHANGESET_PROP, null, cs);
285            }
286        }
287    }
288
289    /**
290     * Refreshes the list of open changesets
291     *
292     */
293    class RefreshAction extends AbstractAction {
294        RefreshAction() {
295            putValue(SHORT_DESCRIPTION, tr("Load the list of your open changesets from the server"));
296            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
297        }
298
299        @Override
300        public void actionPerformed(ActionEvent e) {
301            DownloadOpenChangesetsTask task = new DownloadOpenChangesetsTask(ChangesetManagementPanel.this);
302            Main.worker.submit(task);
303        }
304    }
305
306    /**
307     * Closes the currently selected changeset
308     *
309     */
310    class CloseChangesetAction extends AbstractAction implements ItemListener {
311        CloseChangesetAction() {
312            putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
313            putValue(SHORT_DESCRIPTION, tr("Close the currently selected open changeset"));
314            refreshEnabledState();
315        }
316
317        @Override
318        public void actionPerformed(ActionEvent e) {
319            Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
320            if (cs == null) return;
321            CloseChangesetTask task = new CloseChangesetTask(Collections.singletonList(cs));
322            Main.worker.submit(task);
323        }
324
325        protected void refreshEnabledState() {
326            setEnabled(
327                    cbOpenChangesets.getModel().getSize() > 0
328                    && cbOpenChangesets.getSelectedItem() != null
329                    && rbExisting.isSelected()
330            );
331        }
332
333        @Override
334        public void itemStateChanged(ItemEvent e) {
335            refreshEnabledState();
336        }
337    }
338}