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