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.datatransfer.Clipboard;
014import java.awt.event.ActionEvent;
015import java.awt.event.KeyEvent;
016import java.awt.event.MouseEvent;
017import java.awt.event.WindowAdapter;
018import java.awt.event.WindowEvent;
019import java.util.Collection;
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.TransferHandler;
041import javax.swing.event.ListSelectionEvent;
042import javax.swing.event.ListSelectionListener;
043
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.PrimitiveId;
053import org.openstreetmap.josm.gui.HelpAwareOptionPane;
054import org.openstreetmap.josm.gui.MainApplication;
055import org.openstreetmap.josm.gui.datatransfer.ChangesetTransferable;
056import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
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.NetworkManager;
067import org.openstreetmap.josm.io.OnlineResource;
068import org.openstreetmap.josm.tools.ImageProvider;
069import org.openstreetmap.josm.tools.InputMapUtils;
070import org.openstreetmap.josm.tools.Logging;
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 changeset 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.setRowSorter(new ChangesetCacheTableRowSorter(model));
275        tblChangesets.addMouseListener(new MouseEventHandler());
276        InputMapUtils.addEnterAction(tblChangesets, new ShowDetailAction(model));
277        model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer(model));
278
279        // activate DEL on the table
280        tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "removeFromCache");
281        tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
282        tblChangesets.getTableHeader().setReorderingAllowed(false);
283
284        tblChangesets.setTransferHandler(new TransferHandler() {
285            @Override
286            public void exportToClipboard(JComponent comp, Clipboard clip, int action) {
287                List<Changeset> changesets = model.getSelectedChangesets();
288                ChangesetTransferable transferable = new ChangesetTransferable(changesets);
289                ClipboardUtils.copy(transferable);
290            }
291        });
292
293        pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
294        pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
295        return pnl;
296    }
297
298    protected void build() {
299        setTitle(tr("Changeset Manager"));
300        setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
301        Container cp = getContentPane();
302
303        cp.setLayout(new BorderLayout());
304
305        model = buildModel();
306        actRemoveFromCacheAction = new RemoveFromCacheAction(model);
307        actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction(model);
308        actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction(model);
309        actDownloadSelectedContent = new DownloadSelectedChangesetContentAction(model);
310        actDownloadSelectedChangesetObjects = new DownloadSelectedChangesetObjectsAction();
311
312        cp.add(buildToolbarPanel(), BorderLayout.NORTH);
313        cp.add(buildContentPanel(), BorderLayout.CENTER);
314        cp.add(buildButtonPanel(), BorderLayout.SOUTH);
315
316        // the help context
317        HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetManager"));
318
319        // make the dialog respond to ESC
320        InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
321
322        // install a window event handler
323        addWindowListener(new WindowEventHandler());
324    }
325
326    /**
327     * Constructs a new {@code ChangesetCacheManager}.
328     */
329    public ChangesetCacheManager() {
330        build();
331    }
332
333    @Override
334    public void setVisible(boolean visible) {
335        if (visible) {
336            new WindowGeometry(
337                    getClass().getName() + ".geometry",
338                    WindowGeometry.centerInWindow(
339                            getParent(),
340                            new Dimension(1000, 600)
341                    )
342            ).applySafe(this);
343            needsSplitPaneAdjustment = true;
344            model.init();
345
346        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
347            model.tearDown();
348            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
349        }
350        super.setVisible(visible);
351    }
352
353    /**
354     * Handler for window events
355     *
356     */
357    class WindowEventHandler extends WindowAdapter {
358        @Override
359        public void windowClosing(WindowEvent e) {
360            destroyInstance();
361        }
362
363        @Override
364        public void windowActivated(WindowEvent e) {
365            if (needsSplitPaneAdjustment) {
366                spContent.setDividerLocation(0.5);
367                needsSplitPaneAdjustment = false;
368            }
369        }
370    }
371
372    /**
373     * the cancel / close action
374     */
375    static class CancelAction extends AbstractAction {
376        CancelAction() {
377            putValue(NAME, tr("Close"));
378            new ImageProvider("cancel").getResource().attachImageIcon(this);
379            putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
380        }
381
382        public void cancelAndClose() {
383            destroyInstance();
384        }
385
386        @Override
387        public void actionPerformed(ActionEvent e) {
388            cancelAndClose();
389        }
390    }
391
392    /**
393     * The action to query and download changesets
394     */
395    static class QueryAction extends AbstractAction {
396
397        QueryAction() {
398            putValue(NAME, tr("Query"));
399            new ImageProvider("dialogs", "search").getResource().attachImageIcon(this);
400            putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
401            setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API));
402        }
403
404        @Override
405        public void actionPerformed(ActionEvent evt) {
406            Window parent = GuiHelper.getWindowAncestorFor(evt);
407            ChangesetQueryDialog dialog = new ChangesetQueryDialog(parent);
408            dialog.initForUserInput();
409            dialog.setVisible(true);
410            if (dialog.isCanceled())
411                return;
412
413            try {
414                ChangesetQuery query = dialog.getChangesetQuery();
415                if (query != null) {
416                    ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query));
417                }
418            } catch (IllegalStateException e) {
419                Logging.error(e);
420                JOptionPane.showMessageDialog(parent, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
421            }
422        }
423    }
424
425    /**
426     * Removes the selected changesets from the local changeset cache
427     *
428     */
429    static class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener {
430        private final ChangesetCacheManagerModel model;
431
432        RemoveFromCacheAction(ChangesetCacheManagerModel model) {
433            putValue(NAME, tr("Remove from cache"));
434            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
435            putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
436            this.model = model;
437            updateEnabledState();
438        }
439
440        @Override
441        public void actionPerformed(ActionEvent e) {
442            ChangesetCache.getInstance().remove(model.getSelectedChangesets());
443        }
444
445        protected void updateEnabledState() {
446            setEnabled(model.hasSelectedChangesets());
447        }
448
449        @Override
450        public void valueChanged(ListSelectionEvent e) {
451            if (e == null || !e.getValueIsAdjusting())
452                updateEnabledState();
453        }
454    }
455
456    /**
457     * Closes the selected changesets
458     *
459     */
460    static class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener {
461        private final ChangesetCacheManagerModel model;
462
463        CloseSelectedChangesetsAction(ChangesetCacheManagerModel model) {
464            putValue(NAME, tr("Close"));
465            new ImageProvider("closechangeset").getResource().attachImageIcon(this);
466            putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
467            this.model = model;
468            updateEnabledState();
469        }
470
471        @Override
472        public void actionPerformed(ActionEvent e) {
473            MainApplication.worker.submit(new CloseChangesetTask(model.getSelectedChangesets()));
474        }
475
476        protected void updateEnabledState() {
477            List<Changeset> selected = model.getSelectedChangesets();
478            UserIdentityManager im = UserIdentityManager.getInstance();
479            for (Changeset cs: selected) {
480                if (cs.isOpen()) {
481                    if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
482                        setEnabled(true);
483                        return;
484                    }
485                    if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
486                        setEnabled(true);
487                        return;
488                    }
489                }
490            }
491            setEnabled(false);
492        }
493
494        @Override
495        public void valueChanged(ListSelectionEvent e) {
496            if (e == null || !e.getValueIsAdjusting())
497                updateEnabledState();
498        }
499    }
500
501    /**
502     * Downloads the selected changesets
503     *
504     */
505    static class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener {
506        private final ChangesetCacheManagerModel model;
507
508        DownloadSelectedChangesetsAction(ChangesetCacheManagerModel model) {
509            putValue(NAME, tr("Update changeset"));
510            new ImageProvider("dialogs/changeset", "updatechangeset").getResource().attachImageIcon(this);
511            putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
512            this.model = model;
513            updateEnabledState();
514        }
515
516        @Override
517        public void actionPerformed(ActionEvent e) {
518            if (!GraphicsEnvironment.isHeadless()) {
519                ChangesetCacheManager.getInstance().runDownloadTask(
520                        ChangesetHeaderDownloadTask.buildTaskForChangesets(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesets()));
521            }
522        }
523
524        protected void updateEnabledState() {
525            setEnabled(model.hasSelectedChangesets() && !NetworkManager.isOffline(OnlineResource.OSM_API));
526        }
527
528        @Override
529        public void valueChanged(ListSelectionEvent e) {
530            if (e == null || !e.getValueIsAdjusting())
531                updateEnabledState();
532        }
533    }
534
535    /**
536     * Downloads the content of selected changesets from the OSM server
537     *
538     */
539    static class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener {
540        private final ChangesetCacheManagerModel model;
541
542        DownloadSelectedChangesetContentAction(ChangesetCacheManagerModel model) {
543            putValue(NAME, tr("Download changeset content"));
544            new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this);
545            putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
546            this.model = model;
547            updateEnabledState();
548        }
549
550        @Override
551        public void actionPerformed(ActionEvent e) {
552            if (!GraphicsEnvironment.isHeadless()) {
553                ChangesetCacheManager.getInstance().runDownloadTask(
554                        new ChangesetContentDownloadTask(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesetIds()));
555            }
556        }
557
558        protected void updateEnabledState() {
559            setEnabled(model.hasSelectedChangesets() && !NetworkManager.isOffline(OnlineResource.OSM_API));
560        }
561
562        @Override
563        public void valueChanged(ListSelectionEvent e) {
564            if (e == null || !e.getValueIsAdjusting())
565                updateEnabledState();
566        }
567    }
568
569    /**
570     * Downloads the objects contained in the selected changesets from the OSM server
571     */
572    private class DownloadSelectedChangesetObjectsAction extends AbstractAction implements ListSelectionListener {
573
574        DownloadSelectedChangesetObjectsAction() {
575            putValue(NAME, tr("Download changed objects"));
576            new ImageProvider("downloadprimitive").getResource().attachImageIcon(this);
577            putValue(SHORT_DESCRIPTION, tr("Download the current version of the changed objects in the selected changesets"));
578            updateEnabledState();
579        }
580
581        @Override
582        public void actionPerformed(ActionEvent e) {
583            if (!GraphicsEnvironment.isHeadless()) {
584                if (model.getSelectedChangesets().stream().anyMatch(cs -> !cs.hasContent() || cs.isOpen()))
585                    actDownloadSelectedContent.actionPerformed(e);
586                MainApplication.worker.submit(() -> {
587                    final List<PrimitiveId> primitiveIds = model.getSelectedChangesets().stream()
588                            .map(Changeset::getContent)
589                            .filter(Objects::nonNull)
590                            .flatMap(content -> content.getIds().stream())
591                            .distinct()
592                            .collect(Collectors.toList());
593                    new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null).run();
594                });
595            }
596        }
597
598        protected void updateEnabledState() {
599            setEnabled(model.hasSelectedChangesets() && !NetworkManager.isOffline(OnlineResource.OSM_API));
600        }
601
602        @Override
603        public void valueChanged(ListSelectionEvent e) {
604            if (e == null || !e.getValueIsAdjusting())
605                updateEnabledState();
606        }
607    }
608
609    static class ShowDetailAction extends AbstractAction {
610        private final ChangesetCacheManagerModel model;
611
612        ShowDetailAction(ChangesetCacheManagerModel model) {
613            this.model = model;
614        }
615
616        protected void showDetails() {
617            List<Changeset> selected = model.getSelectedChangesets();
618            if (selected.size() == 1) {
619                model.setChangesetInDetailView(selected.get(0));
620            }
621        }
622
623        @Override
624        public void actionPerformed(ActionEvent e) {
625            showDetails();
626        }
627    }
628
629    static class DownloadMyChangesets extends AbstractAction {
630        DownloadMyChangesets() {
631            putValue(NAME, tr("My changesets"));
632            new ImageProvider("dialogs/changeset", "downloadchangeset").getResource().attachImageIcon(this);
633            putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
634            setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API));
635        }
636
637        protected void alertAnonymousUser(Component parent) {
638            HelpAwareOptionPane.showOptionDialog(
639                    parent,
640                    tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
641                            + "your changesets from the OSM server unless you enter your OSM user name<br>"
642                            + "in the JOSM preferences.</html>"
643                    ),
644                    tr("Warning"),
645                    JOptionPane.WARNING_MESSAGE,
646                    HelpUtil.ht("/Dialog/ChangesetManager#CanDownloadMyChangesets")
647            );
648        }
649
650        @Override
651        public void actionPerformed(ActionEvent e) {
652            Window parent = GuiHelper.getWindowAncestorFor(e);
653            try {
654                ChangesetQuery query = ChangesetQuery.forCurrentUser();
655                if (!GraphicsEnvironment.isHeadless()) {
656                    ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query));
657                }
658            } catch (IllegalStateException ex) {
659                alertAnonymousUser(parent);
660                Logging.trace(ex);
661            }
662        }
663    }
664
665    class MouseEventHandler extends PopupMenuLauncher {
666
667        MouseEventHandler() {
668            super(new ChangesetTablePopupMenu());
669        }
670
671        @Override
672        public void mouseClicked(MouseEvent evt) {
673            if (isDoubleClick(evt)) {
674                new ShowDetailAction(model).showDetails();
675            }
676        }
677    }
678
679    class ChangesetTablePopupMenu extends JPopupMenu {
680        ChangesetTablePopupMenu() {
681            add(actRemoveFromCacheAction);
682            add(actCloseSelectedChangesetsAction);
683            add(actDownloadSelectedChangesets);
684            add(actDownloadSelectedContent);
685            add(actDownloadSelectedChangesetObjects);
686        }
687    }
688
689    static class ChangesetDetailViewSynchronizer implements ListSelectionListener {
690        private final ChangesetCacheManagerModel model;
691
692        ChangesetDetailViewSynchronizer(ChangesetCacheManagerModel model) {
693            this.model = model;
694        }
695
696        @Override
697        public void valueChanged(ListSelectionEvent e) {
698            if (e != null && e.getValueIsAdjusting())
699                return;
700
701            List<Changeset> selected = model.getSelectedChangesets();
702            if (selected.size() == 1) {
703                model.setChangesetInDetailView(selected.get(0));
704            } else {
705                model.setChangesetInDetailView(null);
706            }
707        }
708    }
709
710    /**
711     * Returns the changeset cache model.
712     * @return the changeset cache model
713     * @since 12495
714     */
715    public ChangesetCacheManagerModel getModel() {
716        return model;
717    }
718
719    /**
720     * Selects the changesets  in <code>changesets</code>, provided the
721     * respective changesets are already present in the local changeset cache.
722     *
723     * @param changesets the collection of changesets. If {@code null}, the
724     * selection is cleared.
725     */
726    public void setSelectedChangesets(Collection<Changeset> changesets) {
727        model.setSelectedChangesets(changesets);
728        final int idx = model.getSelectionModel().getMinSelectionIndex();
729        if (idx < 0)
730            return;
731        GuiHelper.runInEDTAndWait(() -> tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true)));
732        repaint();
733    }
734
735    /**
736     * Selects the changesets with the ids in <code>ids</code>, provided the
737     * respective changesets are already present in the local changeset cache.
738     *
739     * @param ids the collection of ids. If null, the selection is cleared.
740     */
741    public void setSelectedChangesetsById(Collection<Integer> ids) {
742        if (ids == null) {
743            setSelectedChangesets(null);
744            return;
745        }
746        ChangesetCache cc = ChangesetCache.getInstance();
747        Set<Changeset> toSelect = ids.stream()
748                .filter(cc::contains)
749                .map(cc::get)
750                .collect(Collectors.toSet());
751        setSelectedChangesets(toSelect);
752    }
753
754    /**
755     * Selects the given component in the detail tabbed panel
756     * @param clazz the class of the component to select
757     */
758    public void setSelectedComponentInDetailPanel(Class<? extends JComponent> clazz) {
759        for (Component component : pnlChangesetDetailTabs.getComponents()) {
760            if (component.getClass().equals(clazz)) {
761                pnlChangesetDetailTabs.setSelectedComponent(component);
762                break;
763            }
764        }
765    }
766
767    /**
768     * Runs the given changeset download task.
769     * @param task The changeset download task to run
770     */
771    public void runDownloadTask(final AbstractChangesetDownloadTask task) {
772        MainApplication.worker.submit(new PostDownloadHandler(task, task.download()));
773        MainApplication.worker.submit(() -> {
774            if (task.isCanceled() || task.isFailed())
775                return;
776            GuiHelper.runInEDT(() -> setSelectedChangesets(task.getDownloadedData()));
777        });
778    }
779}