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