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