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