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