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