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