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