001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.dialogs.changeset;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.BorderLayout;
007    import java.awt.Container;
008    import java.awt.Dimension;
009    import java.awt.FlowLayout;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.KeyEvent;
012    import java.awt.event.MouseAdapter;
013    import java.awt.event.MouseEvent;
014    import java.awt.event.WindowAdapter;
015    import java.awt.event.WindowEvent;
016    import java.util.Collection;
017    import java.util.HashSet;
018    import java.util.List;
019    import java.util.Set;
020    
021    import javax.swing.AbstractAction;
022    import javax.swing.DefaultListSelectionModel;
023    import javax.swing.JComponent;
024    import javax.swing.JFrame;
025    import javax.swing.JOptionPane;
026    import javax.swing.JPanel;
027    import javax.swing.JPopupMenu;
028    import javax.swing.JScrollPane;
029    import javax.swing.JSplitPane;
030    import javax.swing.JTabbedPane;
031    import javax.swing.JTable;
032    import javax.swing.JToolBar;
033    import javax.swing.KeyStroke;
034    import javax.swing.ListSelectionModel;
035    import javax.swing.SwingUtilities;
036    import javax.swing.event.ListSelectionEvent;
037    import javax.swing.event.ListSelectionListener;
038    
039    import org.openstreetmap.josm.Main;
040    import org.openstreetmap.josm.data.osm.Changeset;
041    import org.openstreetmap.josm.data.osm.ChangesetCache;
042    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
043    import org.openstreetmap.josm.gui.JosmUserIdentityManager;
044    import org.openstreetmap.josm.gui.SideButton;
045    import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog;
046    import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryTask;
047    import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
048    import org.openstreetmap.josm.gui.help.HelpUtil;
049    import org.openstreetmap.josm.gui.io.CloseChangesetTask;
050    import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
051    import org.openstreetmap.josm.io.ChangesetQuery;
052    import org.openstreetmap.josm.tools.ImageProvider;
053    import org.openstreetmap.josm.tools.WindowGeometry;
054    
055    /**
056     * ChangesetCacheManager manages the local cache of changesets
057     * retrieved from the OSM API. It displays both a table of the locally cached changesets
058     * and detail information about an individual changeset. It also provides actions for
059     * downloading, querying, closing changesets, in addition to removing changesets from
060     * the local cache.
061     *
062     */
063    public class ChangesetCacheManager extends JFrame {
064    
065        /** the unique instance of the cache manager  */
066        private static ChangesetCacheManager instance;
067    
068        /**
069         * Replies the unique instance of the changeset cache manager
070         *
071         * @return the unique instance of the changeset cache manager
072         */
073        public static ChangesetCacheManager getInstance() {
074            if (instance == null) {
075                instance = new ChangesetCacheManager();
076            }
077            return instance;
078        }
079    
080        /**
081         * Hides and destroys the unique instance of the changeset cache
082         * manager.
083         *
084         */
085        public static void destroyInstance() {
086            if (instance != null) {
087                instance.setVisible(true);
088                instance.dispose();
089                instance = null;
090            }
091        }
092    
093        private ChangesetCacheManagerModel model;
094        private JSplitPane spContent;
095        private boolean needsSplitPaneAdjustment;
096    
097        private RemoveFromCacheAction actRemoveFromCacheAction;
098        private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction;
099        private DownloadSelectedChangesetsAction actDownloadSelectedChangesets;
100        private DownloadSelectedChangesetContentAction actDownloadSelectedContent;
101        private JTable tblChangesets;
102    
103        /**
104         * Creates the various models required
105         */
106        protected void buildModel() {
107            DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
108            selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
109            model = new ChangesetCacheManagerModel(selectionModel);
110    
111            actRemoveFromCacheAction = new RemoveFromCacheAction();
112            actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction();
113            actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction();
114            actDownloadSelectedContent = new DownloadSelectedChangesetContentAction();
115        }
116    
117        /**
118         * builds the toolbar panel in the heading of the dialog
119         *
120         * @return the toolbar panel
121         */
122        protected JPanel buildToolbarPanel() {
123            JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
124    
125            SideButton btn = new SideButton(new QueryAction());
126            pnl.add(btn);
127            pnl.add(new SingleChangesetDownloadPanel());
128            pnl.add(new SideButton(new DownloadMyChangesets()));
129    
130            return pnl;
131        }
132    
133        /**
134         * builds the button panel in the footer of the dialog
135         *
136         * @return the button row pane
137         */
138        protected JPanel buildButtonPanel() {
139            JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
140    
141            //-- cancel and close action
142            pnl.add(new SideButton(new CancelAction()));
143    
144            //-- help action
145            pnl.add(new SideButton(
146                    new ContextSensitiveHelpAction(
147                            HelpUtil.ht("/Dialog/ChangesetCacheManager"))
148            )
149            );
150    
151            return pnl;
152        }
153    
154        /**
155         * Builds the panel with the changeset details
156         *
157         * @return the panel with the changeset details
158         */
159        protected JPanel buildChangesetDetailPanel() {
160            JPanel pnl = new JPanel(new BorderLayout());
161            JTabbedPane tp = new JTabbedPane();
162    
163            // -- add the details panel
164            ChangesetDetailPanel pnlChangesetDetail;
165            tp.add(pnlChangesetDetail = new ChangesetDetailPanel());
166            model.addPropertyChangeListener(pnlChangesetDetail);
167    
168            // -- add the tags panel
169            ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel();
170            tp.add(pnlChangesetTags);
171            model.addPropertyChangeListener(pnlChangesetTags);
172    
173            // -- add the panel for the changeset content
174            ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel();
175            tp.add(pnlChangesetContent);
176            model.addPropertyChangeListener(pnlChangesetContent);
177    
178            tp.setTitleAt(0, tr("Properties"));
179            tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
180            tp.setTitleAt(1, tr("Tags"));
181            tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
182            tp.setTitleAt(2, tr("Content"));
183            tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
184    
185            pnl.add(tp, BorderLayout.CENTER);
186            return pnl;
187        }
188    
189        /**
190         * builds the content panel of the dialog
191         *
192         * @return the content panel
193         */
194        protected JPanel buildContentPanel() {
195            JPanel pnl = new JPanel(new BorderLayout());
196    
197            spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
198            spContent.setLeftComponent(buildChangesetTablePanel());
199            spContent.setRightComponent(buildChangesetDetailPanel());
200            spContent.setOneTouchExpandable(true);
201            spContent.setDividerLocation(0.5);
202    
203            pnl.add(spContent, BorderLayout.CENTER);
204            return pnl;
205        }
206    
207        /**
208         * Builds the table with actions which can be applied to the currently visible changesets
209         * in the changeset table.
210         *
211         * @return
212         */
213        protected JPanel buildChangesetTableActionPanel() {
214            JPanel pnl = new JPanel(new BorderLayout());
215    
216            JToolBar tb = new JToolBar(JToolBar.VERTICAL);
217            tb.setFloatable(false);
218    
219            // -- remove from cache action
220            model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
221            tb.add(actRemoveFromCacheAction);
222    
223            // -- close selected changesets action
224            model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
225            tb.add(actCloseSelectedChangesetsAction);
226    
227            // -- download selected changesets
228            model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
229            tb.add(actDownloadSelectedChangesets);
230    
231            // -- download the content of the selected changesets
232            model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
233            tb.add(actDownloadSelectedContent);
234    
235            pnl.add(tb, BorderLayout.CENTER);
236            return pnl;
237        }
238    
239        /**
240         * Builds the panel with the table of changesets
241         *
242         * @return the panel with the table of changesets
243         */
244        protected JPanel buildChangesetTablePanel() {
245            JPanel pnl = new JPanel(new BorderLayout());
246            tblChangesets = new JTable(
247                    model,
248                    new ChangesetCacheTableColumnModel(),
249                    model.getSelectionModel()
250            );
251            tblChangesets.addMouseListener(new ChangesetTablePopupMenuLauncher());
252            tblChangesets.addMouseListener(new DblClickHandler());
253            tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "showDetails");
254            tblChangesets.getActionMap().put("showDetails", new ShowDetailAction());
255            model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer());
256    
257            // activate DEL on the table
258            tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "removeFromCache");
259            tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
260    
261            pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
262            pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
263            return pnl;
264        }
265    
266        protected void build() {
267            setTitle(tr("Changeset Management Dialog"));
268            setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
269            Container cp = getContentPane();
270    
271            cp.setLayout(new BorderLayout());
272    
273            buildModel();
274            cp.add(buildToolbarPanel(), BorderLayout.NORTH);
275            cp.add(buildContentPanel(), BorderLayout.CENTER);
276            cp.add(buildButtonPanel(), BorderLayout.SOUTH);
277    
278            // the help context
279            HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetCacheManager"));
280    
281            // make the dialog respond to ESC
282            getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "cancelAndClose");
283            getRootPane().getActionMap().put("cancelAndClose", new CancelAction());
284    
285            // install a window event handler
286            addWindowListener(new WindowEventHandler());
287        }
288    
289        public ChangesetCacheManager() {
290            build();
291        }
292    
293        @Override
294        public void setVisible(boolean visible) {
295            if (visible) {
296                new WindowGeometry(
297                        getClass().getName() + ".geometry",
298                        WindowGeometry.centerInWindow(
299                                getParent(),
300                                new Dimension(1000,600)
301                        )
302                ).applySafe(this);
303                needsSplitPaneAdjustment = true;
304                model.init();
305    
306            } else if (!visible && isShowing()){
307                model.tearDown();
308                new WindowGeometry(this).remember(getClass().getName() + ".geometry");
309            }
310            super.setVisible(visible);
311        }
312    
313        /**
314         * Handler for window events
315         *
316         */
317        class WindowEventHandler extends WindowAdapter {
318            @Override
319            public void windowClosing(WindowEvent e) {
320                new CancelAction().cancelAndClose();
321            }
322    
323            @Override
324            public void windowActivated(WindowEvent arg0) {
325                if (needsSplitPaneAdjustment) {
326                    spContent.setDividerLocation(0.5);
327                    needsSplitPaneAdjustment = false;
328                }
329            }
330        }
331    
332        /**
333         * the cancel / close action
334         */
335        static class CancelAction extends AbstractAction {
336            public CancelAction() {
337                putValue(NAME, tr("Close"));
338                putValue(SMALL_ICON, ImageProvider.get("cancel"));
339                putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
340            }
341    
342            public void cancelAndClose() {
343                destroyInstance();
344            }
345    
346            public void actionPerformed(ActionEvent arg0) {
347                cancelAndClose();
348            }
349        }
350    
351        /**
352         * The action to query and download changesets
353         */
354        class QueryAction extends AbstractAction {
355            public QueryAction() {
356                putValue(NAME, tr("Query"));
357                putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
358                putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
359            }
360    
361            public void actionPerformed(ActionEvent evt) {
362                ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this);
363                dialog.initForUserInput();
364                dialog.setVisible(true);
365                if (dialog.isCanceled())
366                    return;
367    
368                try {
369                    ChangesetQuery query = dialog.getChangesetQuery();
370                    if (query == null) return;
371                    ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
372                    ChangesetCacheManager.getInstance().runDownloadTask(task);
373                } catch (IllegalStateException e) {
374                    JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
375                }
376            }
377        }
378    
379        /**
380         * Removes the selected changesets from the local changeset cache
381         *
382         */
383        class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{
384            public RemoveFromCacheAction() {
385                putValue(NAME, tr("Remove from cache"));
386                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
387                putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
388                updateEnabledState();
389            }
390    
391            public void actionPerformed(ActionEvent arg0) {
392                List<Changeset> selected = model.getSelectedChangesets();
393                ChangesetCache.getInstance().remove(selected);
394            }
395    
396            protected void updateEnabledState() {
397                setEnabled(model.hasSelectedChangesets());
398            }
399    
400            public void valueChanged(ListSelectionEvent e) {
401                updateEnabledState();
402    
403            }
404        }
405    
406        /**
407         * Closes the selected changesets
408         *
409         */
410        class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
411            public CloseSelectedChangesetsAction() {
412                putValue(NAME, tr("Close"));
413                putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
414                putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
415                updateEnabledState();
416            }
417    
418            public void actionPerformed(ActionEvent arg0) {
419                List<Changeset> selected = model.getSelectedChangesets();
420                Main.worker.submit(new CloseChangesetTask(selected));
421            }
422    
423            protected void updateEnabledState() {
424                List<Changeset> selected = model.getSelectedChangesets();
425                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
426                for (Changeset cs: selected) {
427                    if (cs.isOpen()) {
428                        if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
429                            setEnabled(true);
430                            return;
431                        }
432                        if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
433                            setEnabled(true);
434                            return;
435                        }
436                    }
437                }
438                setEnabled(false);
439            }
440    
441            public void valueChanged(ListSelectionEvent e) {
442                updateEnabledState();
443            }
444        }
445    
446        /**
447         * Downloads the selected changesets
448         *
449         */
450        class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
451            public DownloadSelectedChangesetsAction() {
452                putValue(NAME, tr("Update changeset"));
453                putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset"));
454                putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
455                updateEnabledState();
456            }
457    
458            public void actionPerformed(ActionEvent arg0) {
459                List<Changeset> selected = model.getSelectedChangesets();
460                ChangesetHeaderDownloadTask task =ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this,selected);
461                ChangesetCacheManager.getInstance().runDownloadTask(task);
462            }
463    
464            protected void updateEnabledState() {
465                setEnabled(model.hasSelectedChangesets());
466            }
467    
468            public void valueChanged(ListSelectionEvent e) {
469                updateEnabledState();
470            }
471        }
472    
473        /**
474         * Downloads the content of selected changesets from the OSM server
475         *
476         */
477        class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{
478            public DownloadSelectedChangesetContentAction() {
479                putValue(NAME, tr("Download changeset content"));
480                putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangesetcontent"));
481                putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
482                updateEnabledState();
483            }
484    
485            public void actionPerformed(ActionEvent arg0) {
486                ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this,model.getSelectedChangesetIds());
487                ChangesetCacheManager.getInstance().runDownloadTask(task);
488            }
489    
490            protected void updateEnabledState() {
491                setEnabled(model.hasSelectedChangesets());
492            }
493    
494            public void valueChanged(ListSelectionEvent e) {
495                updateEnabledState();
496            }
497        }
498    
499        class ShowDetailAction extends AbstractAction {
500    
501            public void showDetails() {
502                List<Changeset> selected = model.getSelectedChangesets();
503                if (selected.size() != 1) return;
504                model.setChangesetInDetailView(selected.get(0));
505            }
506    
507            public void actionPerformed(ActionEvent arg0) {
508                showDetails();
509            }
510        }
511    
512        class DownloadMyChangesets extends AbstractAction {
513            public DownloadMyChangesets() {
514                putValue(NAME, tr("My changesets"));
515                putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset"));
516                putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
517            }
518    
519            protected void alertAnonymousUser() {
520                HelpAwareOptionPane.showOptionDialog(
521                        ChangesetCacheManager.this,
522                        tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
523                                + "your changesets from the OSM server unless you enter your OSM user name<br>"
524                                + "in the JOSM preferences.</html>"
525                        ),
526                        tr("Warning"),
527                        JOptionPane.WARNING_MESSAGE,
528                        HelpUtil.ht("/Dialog/ChangesetCacheManager#CanDownloadMyChangesets")
529                );
530            }
531    
532            public void actionPerformed(ActionEvent arg0) {
533                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
534                if (im.isAnonymous()) {
535                    alertAnonymousUser();
536                    return;
537                }
538                ChangesetQuery query = new ChangesetQuery();
539                if (im.isFullyIdentified()) {
540                    query = query.forUser(im.getUserId());
541                } else {
542                    query = query.forUser(im.getUserName());
543                }
544                ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
545                ChangesetCacheManager.getInstance().runDownloadTask(task);
546            }
547        }
548    
549        class DblClickHandler extends MouseAdapter {
550            @Override
551            public void mouseClicked(MouseEvent evt) {
552                if (! SwingUtilities.isLeftMouseButton(evt) || evt.getClickCount()<2)
553                    return;
554                new ShowDetailAction().showDetails();
555            }
556        }
557    
558        class ChangesetTablePopupMenuLauncher extends PopupMenuLauncher {
559            ChangesetTablePopupMenu menu = new ChangesetTablePopupMenu();
560            @Override
561            public void launch(MouseEvent evt) {
562                if (! model.hasSelectedChangesets()) {
563                    int row = tblChangesets.rowAtPoint(evt.getPoint());
564                    if (row >= 0) {
565                        model.setSelectedByIdx(row);
566                    }
567                }
568                menu.show(tblChangesets, evt.getPoint().x, evt.getPoint().y);
569            }
570        }
571    
572        class ChangesetTablePopupMenu extends JPopupMenu {
573            public ChangesetTablePopupMenu() {
574                add(actRemoveFromCacheAction);
575                add(actCloseSelectedChangesetsAction);
576                add(actDownloadSelectedChangesets);
577                add(actDownloadSelectedContent);
578            }
579        }
580    
581        class ChangesetDetailViewSynchronizer implements ListSelectionListener {
582            public void valueChanged(ListSelectionEvent e) {
583                List<Changeset> selected = model.getSelectedChangesets();
584                if (selected.size() == 1) {
585                    model.setChangesetInDetailView(selected.get(0));
586                } else {
587                    model.setChangesetInDetailView(null);
588                }
589            }
590        }
591    
592        /**
593         * Selects the changesets  in <code>changests</code>, provided the
594         * respective changesets are already present in the local changeset cache.
595         *
596         * @param ids the collection of changesets. If null, the selection is cleared.
597         */
598        public void setSelectedChangesets(Collection<Changeset> changesets) {
599            model.setSelectedChangesets(changesets);
600            int idx = model.getSelectionModel().getMinSelectionIndex();
601            if (idx < 0) return;
602            tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true));
603            repaint();
604        }
605    
606        /**
607         * Selects the changesets with the ids in <code>ids</code>, provided the
608         * respective changesets are already present in the local changeset cache.
609         *
610         * @param ids the collection of ids. If null, the selection is cleared.
611         */
612        public void setSelectedChangesetsById(Collection<Integer> ids) {
613            if (ids == null) {
614                setSelectedChangesets(null);
615                return;
616            }
617            Set<Changeset> toSelect = new HashSet<Changeset>();
618            ChangesetCache cc = ChangesetCache.getInstance();
619            for (int id: ids) {
620                if (cc.contains(id)) {
621                    toSelect.add(cc.get(id));
622                }
623            }
624            setSelectedChangesets(toSelect);
625        }
626    
627        public void runDownloadTask(final ChangesetDownloadTask task) {
628            Main.worker.submit(task);
629            Runnable r = new Runnable() {
630                public void run() {
631                    if (task.isCanceled() || task.isFailed()) return;
632                    setSelectedChangesets(task.getDownloadedChangesets());
633                }
634            };
635            Main.worker.submit(r);
636        }
637    }