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