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