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