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