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;
014import java.util.Optional;
015
016import javax.swing.AbstractAction;
017import javax.swing.BorderFactory;
018import javax.swing.JButton;
019import javax.swing.JCheckBox;
020import javax.swing.JPanel;
021import javax.swing.SwingUtilities;
022
023import org.openstreetmap.josm.data.osm.Changeset;
024import org.openstreetmap.josm.data.osm.ChangesetCache;
025import org.openstreetmap.josm.data.osm.ChangesetCacheEvent;
026import org.openstreetmap.josm.data.osm.ChangesetCacheListener;
027import org.openstreetmap.josm.gui.MainApplication;
028import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
029import org.openstreetmap.josm.gui.widgets.JosmComboBox;
030import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
031import org.openstreetmap.josm.io.OsmTransferException;
032import org.openstreetmap.josm.spi.preferences.Config;
033import org.openstreetmap.josm.tools.ImageProvider;
034
035/**
036 * ChangesetManagementPanel allows to configure changeset to be used in the next 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 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 chose 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 ItemListener, ChangesetCacheListener {
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 JosmComboBox<Changeset> cbOpenChangesets;
054    private JosmComboBoxModel<Changeset> model;
055    private JCheckBox cbCloseAfterUpload;
056    private JButton btnClose;
057
058    /**
059     * Constructs a new {@code ChangesetManagementPanel}.
060     *
061     * @since 18283 (signature)
062     */
063    public ChangesetManagementPanel() {
064        build();
065    }
066
067    /**
068     * Initializes this life cycle of the panel.
069     *
070     * @since 18283
071     */
072    public void initLifeCycle() {
073        refreshChangesets();
074    }
075
076    /**
077     * Returns the model in use.
078     * @return the model
079     */
080    public JosmComboBoxModel<Changeset> getModel() {
081        return model;
082    }
083
084    /**
085     * builds the GUI
086     */
087    protected void build() {
088        setLayout(new GridBagLayout());
089        setBorder(BorderFactory.createTitledBorder(tr("Please select a changeset:")));
090
091        GridBagConstraints gc = new GridBagConstraints();
092        gc.gridx = 0;
093        gc.gridy = 0;
094        gc.weightx = 1.0;
095        gc.weighty = 0.0;
096        gc.fill = GridBagConstraints.HORIZONTAL;
097        gc.anchor = GridBagConstraints.NORTHWEST;
098        gc.insets = new Insets(3, 3, 3, 3);
099
100        gc.gridwidth = 3;
101        add(new JMultilineLabel(tr(
102            "Please select which changeset the data shall be uploaded to and whether to close that changeset after the next upload."
103            )), gc);
104
105        gc.gridwidth = 1;
106        gc.gridy++;
107        model = new JosmComboBoxModel<>();
108        cbOpenChangesets = new JosmComboBox<>(model);
109        cbOpenChangesets.setToolTipText(tr("Select a changeset"));
110        cbOpenChangesets.setRenderer(new ChangesetCellRenderer());
111        Dimension d = cbOpenChangesets.getPreferredSize();
112        d.width = 200;
113        cbOpenChangesets.setPreferredSize(d);
114        d.width = 100;
115        cbOpenChangesets.setMinimumSize(d);
116        add(cbOpenChangesets, gc);
117        int h = cbOpenChangesets.getPreferredSize().height;
118        Dimension prefSize = new Dimension(h, h);
119
120        gc.gridx++;
121        gc.weightx = 0.0;
122        JButton btnRefresh = new JButton(new RefreshAction());
123        btnRefresh.setPreferredSize(prefSize);
124        btnRefresh.setMinimumSize(prefSize);
125        add(btnRefresh, gc);
126
127        gc.gridx++;
128        CloseChangesetAction closeChangesetAction = new CloseChangesetAction();
129        btnClose = new JButton(closeChangesetAction);
130        btnClose.setPreferredSize(prefSize);
131        btnClose.setMinimumSize(prefSize);
132        add(btnClose, gc);
133
134        gc.gridy++;
135        gc.gridx = 0;
136        gc.gridwidth = 3;
137        gc.weightx = 1.0;
138        cbCloseAfterUpload = new JCheckBox(tr("Close changeset after upload"));
139        cbCloseAfterUpload.setToolTipText(tr("Select to close the changeset after the next upload"));
140        add(cbCloseAfterUpload, gc);
141
142        cbOpenChangesets.addItemListener(this);
143        cbOpenChangesets.addItemListener(closeChangesetAction);
144
145        cbCloseAfterUpload.setSelected(Config.getPref().getBoolean("upload.changeset.close", true));
146        cbCloseAfterUpload.addItemListener(new CloseAfterUploadItemStateListener());
147
148        ChangesetCache.getInstance().addChangesetCacheListener(this);
149    }
150
151    /**
152     * Sets the changeset to be used in the next upload
153     * <p>
154     * Note: The changeset may be a new changeset that was automatically opened because the old
155     * changeset overflowed.  In that case it was already added to the changeset cache and the
156     * combobox.
157     *
158     * @param cs the changeset
159     * @see UploadPrimitivesTask#handleChangesetFullResponse
160     */
161    public void setSelectedChangesetForNextUpload(Changeset cs) {
162        model.setSelectedItem(cs);
163    }
164
165    /**
166     * Returns the currently selected changeset or an empty new one.
167     *
168     * @return the currently selected changeset
169     */
170    public Changeset getSelectedChangeset() {
171        return Optional.ofNullable((Changeset) model.getSelectedItem()).orElse(new Changeset());
172    }
173
174    /**
175     * Determines if the user has chosen to close the changeset after the next upload.
176     * @return {@code true} if the user has chosen to close the changeset after the next upload
177     */
178    public boolean isCloseChangesetAfterUpload() {
179        return cbCloseAfterUpload.isSelected();
180    }
181
182    /**
183     * Listens to changes in the selected changeset and fires property change events.
184     */
185    @Override
186    public void itemStateChanged(ItemEvent e) {
187        firePropertyChange(SELECTED_CHANGESET_PROP, null, model.getSelectedItem());
188    }
189
190    /**
191     * Listens to changes in "close after upload" flag and fires property change events.
192     */
193    class CloseAfterUploadItemStateListener implements ItemListener {
194        @Override
195        public void itemStateChanged(ItemEvent e) {
196            if (e.getItemSelectable() != cbCloseAfterUpload)
197                return;
198            switch(e.getStateChange()) {
199            case ItemEvent.SELECTED:
200                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, false, true);
201                Config.getPref().putBoolean("upload.changeset.close", true);
202                break;
203            case ItemEvent.DESELECTED:
204                firePropertyChange(CLOSE_CHANGESET_AFTER_UPLOAD, true, false);
205                Config.getPref().putBoolean("upload.changeset.close", false);
206                break;
207            default: // Do nothing
208            }
209        }
210    }
211
212    /**
213     * Refreshes the list of open changesets
214     */
215    class RefreshAction extends AbstractAction {
216        RefreshAction() {
217            putValue(SHORT_DESCRIPTION, tr("Load the list of your open changesets from the server"));
218            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true);
219        }
220
221        @Override
222        public void actionPerformed(ActionEvent e) {
223            MainApplication.worker.submit(new DownloadOpenChangesetsTask(ChangesetManagementPanel.this));
224        }
225    }
226
227    /**
228     * Closes the currently selected changeset
229     */
230    class CloseChangesetAction extends AbstractAction implements ItemListener {
231        CloseChangesetAction() {
232            new ImageProvider("closechangeset").getResource().attachImageIcon(this, true);
233            putValue(SHORT_DESCRIPTION, tr("Close the currently selected open changeset"));
234            refreshEnabledState();
235        }
236
237        @Override
238        public void actionPerformed(ActionEvent e) {
239            Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
240            if (cs == null) return;
241            MainApplication.worker.submit(new CloseChangesetTask(Collections.singletonList(cs)));
242        }
243
244        protected void refreshEnabledState() {
245            setEnabled(!getSelectedChangeset().isNew());
246        }
247
248        @Override
249        public void itemStateChanged(ItemEvent e) {
250            refreshEnabledState();
251        }
252    }
253
254    /**
255     * Refreshes the changesets combobox form the server.
256     * <p>
257     * Note: This calls into {@link #refreshCombo} through {@link #changesetCacheUpdated}
258     *
259     * @see ChangesetCache#refreshChangesetsFromServer
260     */
261    protected void refreshChangesets() {
262        try {
263            ChangesetCache.getInstance().refreshChangesetsFromServer();
264        } catch (OsmTransferException e) {
265            return;
266        }
267    }
268
269    private void refreshCombo() {
270        Changeset selected = (Changeset) cbOpenChangesets.getSelectedItem();
271        model.removeAllElements();
272        model.addElement(new Changeset());
273        model.addAllElements(ChangesetCache.getInstance().getOpenChangesetsForCurrentUser());
274        cbOpenChangesets.setSelectedItem(selected != null && model.getIndexOf(selected) != -1 ? selected : model.getElementAt(0));
275    }
276
277    @Override
278    public void changesetCacheUpdated(ChangesetCacheEvent event) {
279        // This listener might have been called by a background task.
280        SwingUtilities.invokeLater(() -> refreshCombo());
281    }
282}