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