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