001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.FlowLayout;
008import java.awt.Frame;
009import java.awt.event.ActionEvent;
010import java.awt.event.ItemEvent;
011import java.awt.event.ItemListener;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.HashSet;
017import java.util.List;
018import java.util.Set;
019import java.util.concurrent.ExecutionException;
020import java.util.concurrent.Future;
021
022import javax.swing.AbstractAction;
023import javax.swing.Action;
024import javax.swing.DefaultListSelectionModel;
025import javax.swing.JCheckBox;
026import javax.swing.JList;
027import javax.swing.JMenuItem;
028import javax.swing.JPanel;
029import javax.swing.JScrollPane;
030import javax.swing.ListSelectionModel;
031import javax.swing.SwingUtilities;
032import javax.swing.event.ListSelectionEvent;
033import javax.swing.event.ListSelectionListener;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.actions.AbstractInfoAction;
037import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask;
038import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
039import org.openstreetmap.josm.data.osm.Changeset;
040import org.openstreetmap.josm.data.osm.ChangesetCache;
041import org.openstreetmap.josm.data.osm.DataSet;
042import org.openstreetmap.josm.data.osm.OsmPrimitive;
043import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
044import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
045import org.openstreetmap.josm.gui.SideButton;
046import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager;
047import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetInSelectionListModel;
048import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListCellRenderer;
049import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListModel;
050import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetsInActiveDataLayerListModel;
051import org.openstreetmap.josm.gui.help.HelpUtil;
052import org.openstreetmap.josm.gui.io.CloseChangesetTask;
053import org.openstreetmap.josm.gui.layer.OsmDataLayer;
054import org.openstreetmap.josm.gui.util.GuiHelper;
055import org.openstreetmap.josm.gui.widgets.ListPopupMenu;
056import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
057import org.openstreetmap.josm.io.OnlineResource;
058import org.openstreetmap.josm.tools.ImageProvider;
059import org.openstreetmap.josm.tools.OpenBrowser;
060import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
061
062/**
063 * ChangesetDialog is a toggle dialog which displays the current list of changesets.
064 * It either displays
065 * <ul>
066 *   <li>the list of changesets the currently selected objects are assigned to</li>
067 *   <li>the list of changesets objects in the current data layer are assigend to</li>
068 * </ul>
069 *
070 * The dialog offers actions to download and to close changesets. It can also launch an external
071 * browser with information about a changeset. Furthermore, it can select all objects in
072 * the current data layer being assigned to a specific changeset.
073 * @since 2613
074 */
075public class ChangesetDialog extends ToggleDialog {
076    private ChangesetInSelectionListModel inSelectionModel;
077    private ChangesetsInActiveDataLayerListModel inActiveDataLayerModel;
078    private JList<Changeset> lstInSelection;
079    private JList<Changeset> lstInActiveDataLayer;
080    private JCheckBox cbInSelectionOnly;
081    private JPanel pnlList;
082
083    // the actions
084    private SelectObjectsAction selectObjectsAction;
085    private ReadChangesetsAction readChangesetAction;
086    private ShowChangesetInfoAction showChangesetInfoAction;
087    private CloseOpenChangesetsAction closeChangesetAction;
088
089    private ChangesetDialogPopup popupMenu;
090
091    protected void buildChangesetsLists() {
092        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
093        inSelectionModel = new ChangesetInSelectionListModel(selectionModel);
094
095        lstInSelection = new JList<>(inSelectionModel);
096        lstInSelection.setSelectionModel(selectionModel);
097        lstInSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
098        lstInSelection.setCellRenderer(new ChangesetListCellRenderer());
099
100        selectionModel = new DefaultListSelectionModel();
101        inActiveDataLayerModel = new ChangesetsInActiveDataLayerListModel(selectionModel);
102        lstInActiveDataLayer = new JList<>(inActiveDataLayerModel);
103        lstInActiveDataLayer.setSelectionModel(selectionModel);
104        lstInActiveDataLayer.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
105        lstInActiveDataLayer.setCellRenderer(new ChangesetListCellRenderer());
106
107        DblClickHandler dblClickHandler = new DblClickHandler();
108        lstInSelection.addMouseListener(dblClickHandler);
109        lstInActiveDataLayer.addMouseListener(dblClickHandler);
110    }
111
112    protected void registerAsListener() {
113        // let the model for changesets in the current selection listen to various events
114        ChangesetCache.getInstance().addChangesetCacheListener(inSelectionModel);
115        Main.getLayerManager().addActiveLayerChangeListener(inSelectionModel);
116        DataSet.addSelectionListener(inSelectionModel);
117
118        // let the model for changesets in the current layer listen to various
119        // events and bootstrap it's content
120        ChangesetCache.getInstance().addChangesetCacheListener(inActiveDataLayerModel);
121        Main.getLayerManager().addActiveLayerChangeListener(inActiveDataLayerModel);
122        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
123        if (editLayer != null) {
124            editLayer.data.addDataSetListener(inActiveDataLayerModel);
125            inActiveDataLayerModel.initFromDataSet(editLayer.data);
126            inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected());
127        }
128    }
129
130    protected void unregisterAsListener() {
131        // remove the list model for the current edit layer as listener
132        //
133        ChangesetCache.getInstance().removeChangesetCacheListener(inActiveDataLayerModel);
134        Main.getLayerManager().removeActiveLayerChangeListener(inActiveDataLayerModel);
135        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
136        if (editLayer != null) {
137            editLayer.data.removeDataSetListener(inActiveDataLayerModel);
138        }
139
140        // remove the list model for the changesets in the current selection as
141        // listener
142        //
143        Main.getLayerManager().removeActiveLayerChangeListener(inSelectionModel);
144        DataSet.removeSelectionListener(inSelectionModel);
145    }
146
147    @Override
148    public void showNotify() {
149        registerAsListener();
150        DatasetEventManager.getInstance().addDatasetListener(inActiveDataLayerModel, FireMode.IN_EDT);
151    }
152
153    @Override
154    public void hideNotify() {
155        unregisterAsListener();
156        DatasetEventManager.getInstance().removeDatasetListener(inActiveDataLayerModel);
157    }
158
159    protected JPanel buildFilterPanel() {
160        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
161        pnl.setBorder(null);
162        cbInSelectionOnly = new JCheckBox(tr("For selected objects only"));
163        pnl.add(cbInSelectionOnly);
164        cbInSelectionOnly.setToolTipText(tr("<html>Select to show changesets for the currently selected objects only.<br>"
165                + "Unselect to show all changesets for objects in the current data layer.</html>"));
166        cbInSelectionOnly.setSelected(Main.pref.getBoolean("changeset-dialog.for-selected-objects-only", false));
167        return pnl;
168    }
169
170    protected JPanel buildListPanel() {
171        buildChangesetsLists();
172        JPanel pnl = new JPanel(new BorderLayout());
173        if (cbInSelectionOnly.isSelected()) {
174            pnl.add(new JScrollPane(lstInSelection));
175        } else {
176            pnl.add(new JScrollPane(lstInActiveDataLayer));
177        }
178        return pnl;
179    }
180
181    protected void build() {
182        JPanel pnl = new JPanel(new BorderLayout());
183        pnl.add(buildFilterPanel(), BorderLayout.NORTH);
184        pnlList = buildListPanel();
185        pnl.add(pnlList, BorderLayout.CENTER);
186
187        cbInSelectionOnly.addItemListener(new FilterChangeHandler());
188
189        HelpUtil.setHelpContext(pnl, HelpUtil.ht("/Dialog/ChangesetList"));
190
191        // -- select objects action
192        selectObjectsAction = new SelectObjectsAction();
193        cbInSelectionOnly.addItemListener(selectObjectsAction);
194
195        // -- read changesets action
196        readChangesetAction = new ReadChangesetsAction();
197        cbInSelectionOnly.addItemListener(readChangesetAction);
198
199        // -- close changesets action
200        closeChangesetAction = new CloseOpenChangesetsAction();
201        cbInSelectionOnly.addItemListener(closeChangesetAction);
202
203        // -- show info action
204        showChangesetInfoAction = new ShowChangesetInfoAction();
205        cbInSelectionOnly.addItemListener(showChangesetInfoAction);
206
207        popupMenu = new ChangesetDialogPopup(lstInActiveDataLayer, lstInSelection);
208
209        PopupMenuLauncher popupMenuLauncher = new PopupMenuLauncher(popupMenu);
210        lstInSelection.addMouseListener(popupMenuLauncher);
211        lstInActiveDataLayer.addMouseListener(popupMenuLauncher);
212
213        createLayout(pnl, false, Arrays.asList(new SideButton[] {
214            new SideButton(selectObjectsAction, false),
215            new SideButton(readChangesetAction, false),
216            new SideButton(closeChangesetAction, false),
217            new SideButton(showChangesetInfoAction, false),
218            new SideButton(new LaunchChangesetManagerAction(), false)
219        }));
220    }
221
222    protected JList<Changeset> getCurrentChangesetList() {
223        if (cbInSelectionOnly.isSelected())
224            return lstInSelection;
225        return lstInActiveDataLayer;
226    }
227
228    protected ChangesetListModel getCurrentChangesetListModel() {
229        if (cbInSelectionOnly.isSelected())
230            return inSelectionModel;
231        return inActiveDataLayerModel;
232    }
233
234    protected void initWithCurrentData() {
235        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
236        if (editLayer != null) {
237            inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected());
238            inActiveDataLayerModel.initFromDataSet(editLayer.data);
239        }
240    }
241
242    /**
243     * Constructs a new {@code ChangesetDialog}.
244     */
245    public ChangesetDialog() {
246        super(
247                tr("Changesets"),
248                "changesetdialog",
249                tr("Open the list of changesets in the current layer."),
250                null, /* no keyboard shortcut */
251                200, /* the preferred height */
252                false /* don't show if there is no preference */
253        );
254        build();
255        initWithCurrentData();
256    }
257
258    class DblClickHandler extends MouseAdapter {
259        @Override
260        public void mouseClicked(MouseEvent e) {
261            if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() < 2)
262                return;
263            Set<Integer> sel = getCurrentChangesetListModel().getSelectedChangesetIds();
264            if (sel.isEmpty())
265                return;
266            if (Main.getLayerManager().getEditDataSet() == null)
267                return;
268            new SelectObjectsAction().selectObjectsByChangesetIds(Main.getLayerManager().getEditDataSet(), sel);
269        }
270
271    }
272
273    class FilterChangeHandler implements ItemListener {
274        @Override
275        public void itemStateChanged(ItemEvent e) {
276            Main.pref.put("changeset-dialog.for-selected-objects-only", cbInSelectionOnly.isSelected());
277            pnlList.removeAll();
278            if (cbInSelectionOnly.isSelected()) {
279                pnlList.add(new JScrollPane(lstInSelection), BorderLayout.CENTER);
280            } else {
281                pnlList.add(new JScrollPane(lstInActiveDataLayer), BorderLayout.CENTER);
282            }
283            validate();
284            repaint();
285        }
286    }
287
288    /**
289     * Selects objects for the currently selected changesets.
290     */
291    class SelectObjectsAction extends AbstractAction implements ListSelectionListener, ItemListener {
292
293        SelectObjectsAction() {
294            putValue(NAME, tr("Select"));
295            putValue(SHORT_DESCRIPTION, tr("Select all objects assigned to the currently selected changesets"));
296            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true);
297            updateEnabledState();
298        }
299
300        public void selectObjectsByChangesetIds(DataSet ds, Set<Integer> ids) {
301            if (ds == null || ids == null)
302                return;
303            Set<OsmPrimitive> sel = new HashSet<>();
304            for (OsmPrimitive p: ds.allPrimitives()) {
305                if (ids.contains(p.getChangesetId())) {
306                    sel.add(p);
307                }
308            }
309            ds.setSelected(sel);
310        }
311
312        @Override
313        public void actionPerformed(ActionEvent e) {
314            if (Main.getLayerManager().getEditLayer() == null)
315                return;
316            ChangesetListModel model = getCurrentChangesetListModel();
317            Set<Integer> sel = model.getSelectedChangesetIds();
318            if (sel.isEmpty())
319                return;
320
321            DataSet ds = Main.getLayerManager().getEditLayer().data;
322            selectObjectsByChangesetIds(ds, sel);
323        }
324
325        protected void updateEnabledState() {
326            setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0);
327        }
328
329        @Override
330        public void itemStateChanged(ItemEvent e) {
331            updateEnabledState();
332
333        }
334
335        @Override
336        public void valueChanged(ListSelectionEvent e) {
337            updateEnabledState();
338        }
339    }
340
341    /**
342     * Downloads selected changesets
343     *
344     */
345    class ReadChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener {
346        ReadChangesetsAction() {
347            putValue(NAME, tr("Download"));
348            putValue(SHORT_DESCRIPTION, tr("Download information about the selected changesets from the OSM server"));
349            new ImageProvider("download").getResource().attachImageIcon(this, true);
350            updateEnabledState();
351        }
352
353        @Override
354        public void actionPerformed(ActionEvent e) {
355            ChangesetListModel model = getCurrentChangesetListModel();
356            Set<Integer> sel = model.getSelectedChangesetIds();
357            if (sel.isEmpty())
358                return;
359            ChangesetHeaderDownloadTask task = new ChangesetHeaderDownloadTask(sel);
360            Main.worker.submit(new PostDownloadHandler(task, task.download()));
361        }
362
363        protected void updateEnabledState() {
364            setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0 && !Main.isOffline(OnlineResource.OSM_API));
365        }
366
367        @Override
368        public void itemStateChanged(ItemEvent e) {
369            updateEnabledState();
370        }
371
372        @Override
373        public void valueChanged(ListSelectionEvent e) {
374            updateEnabledState();
375        }
376    }
377
378    /**
379     * Closes the currently selected changesets
380     *
381     */
382    class CloseOpenChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener {
383        CloseOpenChangesetsAction() {
384            putValue(NAME, tr("Close open changesets"));
385            putValue(SHORT_DESCRIPTION, tr("Closes the selected open changesets"));
386            new ImageProvider("closechangeset").getResource().attachImageIcon(this, true);
387            updateEnabledState();
388        }
389
390        @Override
391        public void actionPerformed(ActionEvent e) {
392            List<Changeset> sel = getCurrentChangesetListModel().getSelectedOpenChangesets();
393            if (sel.isEmpty())
394                return;
395            Main.worker.submit(new CloseChangesetTask(sel));
396        }
397
398        protected void updateEnabledState() {
399            setEnabled(getCurrentChangesetListModel().hasSelectedOpenChangesets());
400        }
401
402        @Override
403        public void itemStateChanged(ItemEvent e) {
404            updateEnabledState();
405        }
406
407        @Override
408        public void valueChanged(ListSelectionEvent e) {
409            updateEnabledState();
410        }
411    }
412
413    /**
414     * Show information about the currently selected changesets
415     *
416     */
417    class ShowChangesetInfoAction extends AbstractAction implements ListSelectionListener, ItemListener {
418        ShowChangesetInfoAction() {
419            putValue(NAME, tr("Show info"));
420            putValue(SHORT_DESCRIPTION, tr("Open a web page for each selected changeset"));
421            new ImageProvider("help/internet").getResource().attachImageIcon(this, true);
422            updateEnabledState();
423        }
424
425        @Override
426        public void actionPerformed(ActionEvent e) {
427            Set<Changeset> sel = getCurrentChangesetListModel().getSelectedChangesets();
428            if (sel.isEmpty())
429                return;
430            if (sel.size() > 10 && !AbstractInfoAction.confirmLaunchMultiple(sel.size()))
431                return;
432            String baseUrl = Main.getBaseBrowseUrl();
433            for (Changeset cs: sel) {
434                OpenBrowser.displayUrl(baseUrl + "/changeset/" + cs.getId());
435            }
436        }
437
438        protected void updateEnabledState() {
439            setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0);
440        }
441
442        @Override
443        public void itemStateChanged(ItemEvent e) {
444            updateEnabledState();
445        }
446
447        @Override
448        public void valueChanged(ListSelectionEvent e) {
449            updateEnabledState();
450        }
451    }
452
453    /**
454     * Show information about the currently selected changesets
455     *
456     */
457    class LaunchChangesetManagerAction extends AbstractAction {
458        LaunchChangesetManagerAction() {
459            putValue(NAME, tr("Details"));
460            putValue(SHORT_DESCRIPTION, tr("Opens the Changeset Manager window for the selected changesets"));
461            new ImageProvider("dialogs/changeset", "changesetmanager").getResource().attachImageIcon(this, true);
462        }
463
464        @Override
465        public void actionPerformed(ActionEvent e) {
466            ChangesetListModel model = getCurrentChangesetListModel();
467            Set<Integer> sel = model.getSelectedChangesetIds();
468            LaunchChangesetManager.displayChangesets(sel);
469        }
470    }
471
472    /**
473     * A utility class to fetch changesets and display the changeset dialog.
474     */
475    public static final class LaunchChangesetManager {
476
477        private LaunchChangesetManager() {
478            // Hide implicit public constructor for utility classes
479        }
480
481        private static void launchChangesetManager(Collection<Integer> toSelect) {
482            ChangesetCacheManager cm = ChangesetCacheManager.getInstance();
483            if (cm.isVisible()) {
484                cm.setExtendedState(Frame.NORMAL);
485                cm.toFront();
486                cm.requestFocus();
487            } else {
488                cm.setVisible(true);
489                cm.toFront();
490                cm.requestFocus();
491            }
492            cm.setSelectedChangesetsById(toSelect);
493        }
494
495        /**
496         * Fetches changesets and display the changeset dialog.
497         * @param sel the changeset ids to fetch and display.
498         */
499        public static void displayChangesets(final Set<Integer> sel) {
500            final Set<Integer> toDownload = new HashSet<>();
501            if (!Main.isOffline(OnlineResource.OSM_API)) {
502                ChangesetCache cc = ChangesetCache.getInstance();
503                for (int id: sel) {
504                    if (!cc.contains(id)) {
505                        toDownload.add(id);
506                    }
507                }
508            }
509
510            final ChangesetHeaderDownloadTask task;
511            final Future<?> future;
512            if (toDownload.isEmpty()) {
513                task = null;
514                future = null;
515            } else {
516                task = new ChangesetHeaderDownloadTask(toDownload);
517                future = Main.worker.submit(new PostDownloadHandler(task, task.download()));
518            }
519
520            Runnable r = () -> {
521                // first, wait for the download task to finish, if a download task was launched
522                if (future != null) {
523                    try {
524                        future.get();
525                    } catch (InterruptedException e1) {
526                        Main.warn(e1, "InterruptedException in ChangesetDialog while downloading changeset header");
527                    } catch (ExecutionException e2) {
528                        Main.error(e2);
529                        BugReportExceptionHandler.handleException(e2.getCause());
530                        return;
531                    }
532                }
533                if (task != null) {
534                    if (task.isCanceled())
535                        // don't launch the changeset manager if the download task was canceled
536                        return;
537                    if (task.isFailed()) {
538                        toDownload.clear();
539                    }
540                }
541                // launch the task
542                GuiHelper.runInEDT(() -> launchChangesetManager(sel));
543            };
544            Main.worker.submit(r);
545        }
546    }
547
548    class ChangesetDialogPopup extends ListPopupMenu {
549        ChangesetDialogPopup(JList<?> ... lists) {
550            super(lists);
551            add(selectObjectsAction);
552            addSeparator();
553            add(readChangesetAction);
554            add(closeChangesetAction);
555            addSeparator();
556            add(showChangesetInfoAction);
557        }
558    }
559
560    public void addPopupMenuSeparator() {
561        popupMenu.addSeparator();
562    }
563
564    public JMenuItem addPopupMenuAction(Action a) {
565        return popupMenu.add(a);
566    }
567}