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