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.MapView;
046import org.openstreetmap.josm.gui.SideButton;
047import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager;
048import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetInSelectionListModel;
049import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListCellRenderer;
050import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListModel;
051import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetsInActiveDataLayerListModel;
052import org.openstreetmap.josm.gui.help.HelpUtil;
053import org.openstreetmap.josm.gui.io.CloseChangesetTask;
054import org.openstreetmap.josm.gui.layer.OsmDataLayer;
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.tools.ImageProvider;
060import org.openstreetmap.josm.tools.OpenBrowser;
061import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
062
063/**
064 * ChangesetDialog is a toggle dialog which displays the current list of changesets.
065 * It either displays
066 * <ul>
067 *   <li>the list of changesets the currently selected objects are assigned to</li>
068 *   <li>the list of changesets objects in the current data layer are assigend to</li>
069 * </ul>
070 *
071 * The dialog offers actions to download and to close changesets. It can also launch an external
072 * browser with information about a changeset. Furthermore, it can select all objects in
073 * the current data layer being assigned to a specific changeset.
074 * @since 2613
075 */
076public class ChangesetDialog extends ToggleDialog {
077    private ChangesetInSelectionListModel inSelectionModel;
078    private ChangesetsInActiveDataLayerListModel inActiveDataLayerModel;
079    private JList<Changeset> lstInSelection;
080    private JList<Changeset> lstInActiveDataLayer;
081    private JCheckBox cbInSelectionOnly;
082    private JPanel pnlList;
083
084    // the actions
085    private SelectObjectsAction selectObjectsAction;
086    private ReadChangesetsAction readChangesetAction;
087    private ShowChangesetInfoAction showChangesetInfoAction;
088    private CloseOpenChangesetsAction closeChangesetAction;
089    private LaunchChangesetManagerAction launchChangesetManagerAction;
090
091    private ChangesetDialogPopup popupMenu;
092
093    protected void buildChangesetsLists() {
094        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
095        inSelectionModel = new ChangesetInSelectionListModel(selectionModel);
096
097        lstInSelection = new JList<>(inSelectionModel);
098        lstInSelection.setSelectionModel(selectionModel);
099        lstInSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
100        lstInSelection.setCellRenderer(new ChangesetListCellRenderer());
101
102        selectionModel = new DefaultListSelectionModel();
103        inActiveDataLayerModel = new ChangesetsInActiveDataLayerListModel(selectionModel);
104        lstInActiveDataLayer = new JList<>(inActiveDataLayerModel);
105        lstInActiveDataLayer.setSelectionModel(selectionModel);
106        lstInActiveDataLayer.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
107        lstInActiveDataLayer.setCellRenderer(new ChangesetListCellRenderer());
108
109        DblClickHandler dblClickHandler = new DblClickHandler();
110        lstInSelection.addMouseListener(dblClickHandler);
111        lstInActiveDataLayer.addMouseListener(dblClickHandler);
112    }
113
114    protected void registerAsListener() {
115        // let the model for changesets in the current selection listen to various events
116        ChangesetCache.getInstance().addChangesetCacheListener(inSelectionModel);
117        MapView.addEditLayerChangeListener(inSelectionModel);
118        DataSet.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        MapView.addEditLayerChangeListener(inActiveDataLayerModel);
124        OsmDataLayer editLayer = Main.main.getEditLayer();
125        if (editLayer != null) {
126            editLayer.data.addDataSetListener(inActiveDataLayerModel);
127            inActiveDataLayerModel.initFromDataSet(editLayer.data);
128            inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected());
129        }
130    }
131
132    protected void unregisterAsListener() {
133        // remove the list model for the current edit layer as listener
134        //
135        ChangesetCache.getInstance().removeChangesetCacheListener(inActiveDataLayerModel);
136        MapView.removeEditLayerChangeListener(inActiveDataLayerModel);
137        OsmDataLayer editLayer = Main.main.getEditLayer();
138        if (editLayer != null) {
139            editLayer.data.removeDataSetListener(inActiveDataLayerModel);
140        }
141
142        // remove the list model for the changesets in the current selection as
143        // listener
144        //
145        MapView.removeEditLayerChangeListener(inSelectionModel);
146        DataSet.removeSelectionListener(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(Main.pref.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    protected void build() {
184        JPanel pnl = new JPanel(new BorderLayout());
185        pnl.add(buildFilterPanel(), BorderLayout.NORTH);
186        pnlList = buildListPanel();
187        pnl.add(pnlList, BorderLayout.CENTER);
188
189        cbInSelectionOnly.addItemListener(new FilterChangeHandler());
190
191        HelpUtil.setHelpContext(pnl, HelpUtil.ht("/Dialog/ChangesetList"));
192
193        // -- select objects action
194        selectObjectsAction = new SelectObjectsAction();
195        cbInSelectionOnly.addItemListener(selectObjectsAction);
196
197        // -- read changesets action
198        readChangesetAction = new ReadChangesetsAction();
199        cbInSelectionOnly.addItemListener(readChangesetAction);
200
201        // -- close changesets action
202        closeChangesetAction = new CloseOpenChangesetsAction();
203        cbInSelectionOnly.addItemListener(closeChangesetAction);
204
205        // -- show info action
206        showChangesetInfoAction = new ShowChangesetInfoAction();
207        cbInSelectionOnly.addItemListener(showChangesetInfoAction);
208
209        // -- launch changeset manager action
210        launchChangesetManagerAction = new LaunchChangesetManagerAction();
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(new SideButton[] {
219            new SideButton(selectObjectsAction, false),
220            new SideButton(readChangesetAction, false),
221            new SideButton(closeChangesetAction, false),
222            new SideButton(showChangesetInfoAction, false),
223            new SideButton(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        OsmDataLayer editLayer = Main.main.getEditLayer();
241        if (editLayer != null) {
242            inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected());
243            inActiveDataLayerModel.initFromDataSet(editLayer.data);
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                null, /* no keyboard shortcut */
256                200, /* the preferred height */
257                false /* don't show if there is no preference */
258        );
259        build();
260        initWithCurrentData();
261    }
262
263    class DblClickHandler extends MouseAdapter {
264        @Override
265        public void mouseClicked(MouseEvent e) {
266            if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() < 2)
267                return;
268            Set<Integer> sel = getCurrentChangesetListModel().getSelectedChangesetIds();
269            if (sel.isEmpty())
270                return;
271            if (Main.main.getCurrentDataSet() == null)
272                return;
273            new SelectObjectsAction().selectObjectsByChangesetIds(Main.main.getCurrentDataSet(), sel);
274        }
275
276    }
277
278    class FilterChangeHandler implements ItemListener {
279        @Override
280        public void itemStateChanged(ItemEvent e) {
281            Main.pref.put("changeset-dialog.for-selected-objects-only", cbInSelectionOnly.isSelected());
282            pnlList.removeAll();
283            if (cbInSelectionOnly.isSelected()) {
284                pnlList.add(new JScrollPane(lstInSelection), BorderLayout.CENTER);
285            } else {
286                pnlList.add(new JScrollPane(lstInActiveDataLayer), BorderLayout.CENTER);
287            }
288            validate();
289            repaint();
290        }
291    }
292
293    /**
294     * Selects objects for the currently selected changesets.
295     */
296    class SelectObjectsAction extends AbstractAction implements ListSelectionListener, ItemListener {
297
298        SelectObjectsAction() {
299            putValue(NAME, tr("Select"));
300            putValue(SHORT_DESCRIPTION, tr("Select all objects assigned to the currently selected changesets"));
301            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
302            updateEnabledState();
303        }
304
305        public void selectObjectsByChangesetIds(DataSet ds, Set<Integer> ids) {
306            if (ds == null || ids == null)
307                return;
308            Set<OsmPrimitive> sel = new HashSet<>();
309            for (OsmPrimitive p: ds.allPrimitives()) {
310                if (ids.contains(p.getChangesetId())) {
311                    sel.add(p);
312                }
313            }
314            ds.setSelected(sel);
315        }
316
317        @Override
318        public void actionPerformed(ActionEvent e) {
319            if (!Main.main.hasEditLayer())
320                return;
321            ChangesetListModel model = getCurrentChangesetListModel();
322            Set<Integer> sel = model.getSelectedChangesetIds();
323            if (sel.isEmpty())
324                return;
325
326            DataSet ds = Main.main.getEditLayer().data;
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            putValue(SMALL_ICON, ImageProvider.get("download"));
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            Main.worker.submit(new PostDownloadHandler(task, task.download()));
366        }
367
368        protected void updateEnabledState() {
369            setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0 && !Main.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("Closes the selected open changesets"));
391            putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
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            Main.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            putValue(SMALL_ICON, ImageProvider.get("help/internet"));
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 && !AbstractInfoAction.confirmLaunchMultiple(sel.size()))
436                return;
437            String baseUrl = Main.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            putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "changesetmanager"));
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        protected static void launchChangesetManager(Collection<Integer> toSelect) {
487            ChangesetCacheManager cm = ChangesetCacheManager.getInstance();
488            if (cm.isVisible()) {
489                cm.setExtendedState(Frame.NORMAL);
490                cm.toFront();
491                cm.requestFocus();
492            } else {
493                cm.setVisible(true);
494                cm.toFront();
495                cm.requestFocus();
496            }
497            cm.setSelectedChangesetsById(toSelect);
498        }
499
500        /**
501         * Fetches changesets and display the changeset dialog.
502         * @param sel the changeset ids to fetch and display.
503         */
504        public static void displayChangesets(final Set<Integer> sel) {
505            final Set<Integer> toDownload = new HashSet<>();
506            if (!Main.isOffline(OnlineResource.OSM_API)) {
507                ChangesetCache cc = ChangesetCache.getInstance();
508                for (int id: sel) {
509                    if (!cc.contains(id)) {
510                        toDownload.add(id);
511                    }
512                }
513            }
514
515            final ChangesetHeaderDownloadTask task;
516            final Future<?> future;
517            if (toDownload.isEmpty()) {
518                task = null;
519                future = null;
520            } else {
521                task = new ChangesetHeaderDownloadTask(toDownload);
522                future = Main.worker.submit(new PostDownloadHandler(task, task.download()));
523            }
524
525            Runnable r = new Runnable() {
526                @Override
527                public void run() {
528                    // first, wait for the download task to finish, if a download task was launched
529                    if (future != null) {
530                        try {
531                            future.get();
532                        } catch (InterruptedException e) {
533                            Main.warn("InterruptedException in "+getClass().getSimpleName()+" while downloading changeset header");
534                        } catch (ExecutionException e) {
535                            Main.error(e);
536                            BugReportExceptionHandler.handleException(e.getCause());
537                            return;
538                        }
539                    }
540                    if (task != null) {
541                        if (task.isCanceled())
542                            // don't launch the changeset manager if the download task was canceled
543                            return;
544                        if (task.isFailed()) {
545                            toDownload.clear();
546                        }
547                    }
548                    // launch the task
549                    GuiHelper.runInEDT(new Runnable() {
550                        @Override
551                        public void run() {
552                            launchChangesetManager(sel);
553                        }
554                    });
555                }
556            };
557            Main.worker.submit(r);
558        }
559    }
560
561    class ChangesetDialogPopup extends ListPopupMenu {
562        ChangesetDialogPopup(JList<?> ... lists) {
563            super(lists);
564            add(selectObjectsAction);
565            addSeparator();
566            add(readChangesetAction);
567            add(closeChangesetAction);
568            addSeparator();
569            add(showChangesetInfoAction);
570        }
571    }
572
573    public void addPopupMenuSeparator() {
574        popupMenu.addSeparator();
575    }
576
577    public JMenuItem addPopupMenuAction(Action a) {
578        return popupMenu.add(a);
579    }
580}