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