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