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