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