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