// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.changeset;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.DefaultListSelectionModel;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.ChangesetCache;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.JosmUserIdentityManager;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog;
import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryTask;
import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.io.CloseChangesetTask;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.io.ChangesetQuery;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.WindowGeometry;
/**
* ChangesetCacheManager manages the local cache of changesets
* retrieved from the OSM API. It displays both a table of the locally cached changesets
* and detail information about an individual changeset. It also provides actions for
* downloading, querying, closing changesets, in addition to removing changesets from
* the local cache.
*
*/
public class ChangesetCacheManager extends JFrame {
/** The changeset download icon **/
public static final ImageIcon DOWNLOAD_CONTENT_ICON = ImageProvider.get("dialogs/changeset", "downloadchangesetcontent");
/** The changeset update icon **/
public static final ImageIcon UPDATE_CONTENT_ICON = ImageProvider.get("dialogs/changeset", "updatechangesetcontent");
/** the unique instance of the cache manager */
private static ChangesetCacheManager instance;
/**
* Replies the unique instance of the changeset cache manager
*
* @return the unique instance of the changeset cache manager
*/
public static ChangesetCacheManager getInstance() {
if (instance == null) {
instance = new ChangesetCacheManager();
}
return instance;
}
/**
* Hides and destroys the unique instance of the changeset cache
* manager.
*
*/
public static void destroyInstance() {
if (instance != null) {
instance.setVisible(true);
instance.dispose();
instance = null;
}
}
private ChangesetCacheManagerModel model;
private JSplitPane spContent;
private boolean needsSplitPaneAdjustment;
private RemoveFromCacheAction actRemoveFromCacheAction;
private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction;
private DownloadSelectedChangesetsAction actDownloadSelectedChangesets;
private DownloadSelectedChangesetContentAction actDownloadSelectedContent;
private JTable tblChangesets;
/**
* Creates the various models required
*/
protected void buildModel() {
DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
model = new ChangesetCacheManagerModel(selectionModel);
actRemoveFromCacheAction = new RemoveFromCacheAction();
actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction();
actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction();
actDownloadSelectedContent = new DownloadSelectedChangesetContentAction();
}
/**
* builds the toolbar panel in the heading of the dialog
*
* @return the toolbar panel
*/
protected JPanel buildToolbarPanel() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
SideButton btn = new SideButton(new QueryAction());
pnl.add(btn);
pnl.add(new SingleChangesetDownloadPanel());
pnl.add(new SideButton(new DownloadMyChangesets()));
return pnl;
}
/**
* builds the button panel in the footer of the dialog
*
* @return the button row pane
*/
protected JPanel buildButtonPanel() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
//-- cancel and close action
pnl.add(new SideButton(new CancelAction()));
//-- help action
pnl.add(new SideButton(
new ContextSensitiveHelpAction(
HelpUtil.ht("/Dialog/ChangesetCacheManager"))
)
);
return pnl;
}
/**
* Builds the panel with the changeset details
*
* @return the panel with the changeset details
*/
protected JPanel buildChangesetDetailPanel() {
JPanel pnl = new JPanel(new BorderLayout());
JTabbedPane tp = new JTabbedPane();
// -- add the details panel
ChangesetDetailPanel pnlChangesetDetail = new ChangesetDetailPanel();
tp.add(pnlChangesetDetail);
model.addPropertyChangeListener(pnlChangesetDetail);
// -- add the tags panel
ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel();
tp.add(pnlChangesetTags);
model.addPropertyChangeListener(pnlChangesetTags);
// -- add the panel for the changeset content
ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel();
tp.add(pnlChangesetContent);
model.addPropertyChangeListener(pnlChangesetContent);
tp.setTitleAt(0, tr("Properties"));
tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
tp.setTitleAt(1, tr("Tags"));
tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
tp.setTitleAt(2, tr("Content"));
tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
pnl.add(tp, BorderLayout.CENTER);
return pnl;
}
/**
* builds the content panel of the dialog
*
* @return the content panel
*/
protected JPanel buildContentPanel() {
JPanel pnl = new JPanel(new BorderLayout());
spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
spContent.setLeftComponent(buildChangesetTablePanel());
spContent.setRightComponent(buildChangesetDetailPanel());
spContent.setOneTouchExpandable(true);
spContent.setDividerLocation(0.5);
pnl.add(spContent, BorderLayout.CENTER);
return pnl;
}
/**
* Builds the table with actions which can be applied to the currently visible changesets
* in the changeset table.
*
* @return changset actions panel
*/
protected JPanel buildChangesetTableActionPanel() {
JPanel pnl = new JPanel(new BorderLayout());
JToolBar tb = new JToolBar(JToolBar.VERTICAL);
tb.setFloatable(false);
// -- remove from cache action
model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
tb.add(actRemoveFromCacheAction);
// -- close selected changesets action
model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
tb.add(actCloseSelectedChangesetsAction);
// -- download selected changesets
model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
tb.add(actDownloadSelectedChangesets);
// -- download the content of the selected changesets
model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
tb.add(actDownloadSelectedContent);
pnl.add(tb, BorderLayout.CENTER);
return pnl;
}
/**
* Builds the panel with the table of changesets
*
* @return the panel with the table of changesets
*/
protected JPanel buildChangesetTablePanel() {
JPanel pnl = new JPanel(new BorderLayout());
tblChangesets = new JTable(
model,
new ChangesetCacheTableColumnModel(),
model.getSelectionModel()
);
tblChangesets.addMouseListener(new MouseEventHandler());
tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "showDetails");
tblChangesets.getActionMap().put("showDetails", new ShowDetailAction());
model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer());
// activate DEL on the table
tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "removeFromCache");
tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
return pnl;
}
protected void build() {
setTitle(tr("Changeset Management Dialog"));
setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
Container cp = getContentPane();
cp.setLayout(new BorderLayout());
buildModel();
cp.add(buildToolbarPanel(), BorderLayout.NORTH);
cp.add(buildContentPanel(), BorderLayout.CENTER);
cp.add(buildButtonPanel(), BorderLayout.SOUTH);
// the help context
HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetCacheManager"));
// make the dialog respond to ESC
getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "cancelAndClose");
getRootPane().getActionMap().put("cancelAndClose", new CancelAction());
// install a window event handler
addWindowListener(new WindowEventHandler());
}
/**
* Constructs a new {@code ChangesetCacheManager}.
*/
public ChangesetCacheManager() {
build();
}
@Override
public void setVisible(boolean visible) {
if (visible) {
new WindowGeometry(
getClass().getName() + ".geometry",
WindowGeometry.centerInWindow(
getParent(),
new Dimension(1000,600)
)
).applySafe(this);
needsSplitPaneAdjustment = true;
model.init();
} else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
model.tearDown();
new WindowGeometry(this).remember(getClass().getName() + ".geometry");
}
super.setVisible(visible);
}
/**
* Handler for window events
*
*/
class WindowEventHandler extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
new CancelAction().cancelAndClose();
}
@Override
public void windowActivated(WindowEvent arg0) {
if (needsSplitPaneAdjustment) {
spContent.setDividerLocation(0.5);
needsSplitPaneAdjustment = false;
}
}
}
/**
* the cancel / close action
*/
static class CancelAction extends AbstractAction {
public CancelAction() {
putValue(NAME, tr("Close"));
putValue(SMALL_ICON, ImageProvider.get("cancel"));
putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
}
public void cancelAndClose() {
destroyInstance();
}
@Override
public void actionPerformed(ActionEvent arg0) {
cancelAndClose();
}
}
/**
* The action to query and download changesets
*/
class QueryAction extends AbstractAction {
public QueryAction() {
putValue(NAME, tr("Query"));
putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
}
@Override
public void actionPerformed(ActionEvent evt) {
ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this);
dialog.initForUserInput();
dialog.setVisible(true);
if (dialog.isCanceled())
return;
try {
ChangesetQuery query = dialog.getChangesetQuery();
if (query == null) return;
ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
ChangesetCacheManager.getInstance().runDownloadTask(task);
} catch (IllegalStateException e) {
JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
}
}
}
/**
* Removes the selected changesets from the local changeset cache
*
*/
class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{
public RemoveFromCacheAction() {
putValue(NAME, tr("Remove from cache"));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent arg0) {
List<Changeset> selected = model.getSelectedChangesets();
ChangesetCache.getInstance().remove(selected);
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets());
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Closes the selected changesets
*
*/
class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
public CloseSelectedChangesetsAction() {
putValue(NAME, tr("Close"));
putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent arg0) {
List<Changeset> selected = model.getSelectedChangesets();
Main.worker.submit(new CloseChangesetTask(selected));
}
protected void updateEnabledState() {
List<Changeset> selected = model.getSelectedChangesets();
JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
for (Changeset cs: selected) {
if (cs.isOpen()) {
if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
setEnabled(true);
return;
}
if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
setEnabled(true);
return;
}
}
}
setEnabled(false);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Downloads the selected changesets
*
*/
class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
public DownloadSelectedChangesetsAction() {
putValue(NAME, tr("Update changeset"));
putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset"));
putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent arg0) {
List<Changeset> selected = model.getSelectedChangesets();
ChangesetHeaderDownloadTask task =ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this,selected);
ChangesetCacheManager.getInstance().runDownloadTask(task);
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets());
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Downloads the content of selected changesets from the OSM server
*
*/
class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{
public DownloadSelectedChangesetContentAction() {
putValue(NAME, tr("Download changeset content"));
putValue(SMALL_ICON, DOWNLOAD_CONTENT_ICON);
putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent arg0) {
ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this,model.getSelectedChangesetIds());
ChangesetCacheManager.getInstance().runDownloadTask(task);
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets());
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
class ShowDetailAction extends AbstractAction {
public void showDetails() {
List<Changeset> selected = model.getSelectedChangesets();
if (selected.size() != 1) return;
model.setChangesetInDetailView(selected.get(0));
}
@Override
public void actionPerformed(ActionEvent arg0) {
showDetails();
}
}
class DownloadMyChangesets extends AbstractAction {
public DownloadMyChangesets() {
putValue(NAME, tr("My changesets"));
putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset"));
putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
}
protected void alertAnonymousUser() {
HelpAwareOptionPane.showOptionDialog(
ChangesetCacheManager.this,
tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
+ "your changesets from the OSM server unless you enter your OSM user name<br>"
+ "in the JOSM preferences.</html>"
),
tr("Warning"),
JOptionPane.WARNING_MESSAGE,
HelpUtil.ht("/Dialog/ChangesetCacheManager#CanDownloadMyChangesets")
);
}
@Override
public void actionPerformed(ActionEvent arg0) {
JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
if (im.isAnonymous()) {
alertAnonymousUser();
return;
}
ChangesetQuery query = new ChangesetQuery();
if (im.isFullyIdentified()) {
query = query.forUser(im.getUserId());
} else {
query = query.forUser(im.getUserName());
}
ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
ChangesetCacheManager.getInstance().runDownloadTask(task);
}
}
class MouseEventHandler extends PopupMenuLauncher {
public MouseEventHandler() {
super(new ChangesetTablePopupMenu());
}
@Override
public void mouseClicked(MouseEvent evt) {
if (isDoubleClick(evt)) {
new ShowDetailAction().showDetails();
}
}
}
class ChangesetTablePopupMenu extends JPopupMenu {
public ChangesetTablePopupMenu() {
add(actRemoveFromCacheAction);
add(actCloseSelectedChangesetsAction);
add(actDownloadSelectedChangesets);
add(actDownloadSelectedContent);
}
}
class ChangesetDetailViewSynchronizer implements ListSelectionListener {
@Override
public void valueChanged(ListSelectionEvent e) {
List<Changeset> selected = model.getSelectedChangesets();
if (selected.size() == 1) {
model.setChangesetInDetailView(selected.get(0));
} else {
model.setChangesetInDetailView(null);
}
}
}
/**
* Selects the changesets in <code>changests</code>, provided the
* respective changesets are already present in the local changeset cache.
*
* @param changesets the collection of changesets. If {@code null}, the
* selection is cleared.
*/
public void setSelectedChangesets(Collection<Changeset> changesets) {
model.setSelectedChangesets(changesets);
int idx = model.getSelectionModel().getMinSelectionIndex();
if (idx < 0) return;
tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true));
repaint();
}
/**
* Selects the changesets with the ids in <code>ids</code>, provided the
* respective changesets are already present in the local changeset cache.
*
* @param ids the collection of ids. If null, the selection is cleared.
*/
public void setSelectedChangesetsById(Collection<Integer> ids) {
if (ids == null) {
setSelectedChangesets(null);
return;
}
Set<Changeset> toSelect = new HashSet<>();
ChangesetCache cc = ChangesetCache.getInstance();
for (int id: ids) {
if (cc.contains(id)) {
toSelect.add(cc.get(id));
}
}
setSelectedChangesets(toSelect);
}
/**
* Runs the given changeset download task.
* @param task The changeset download task to run
*/
public void runDownloadTask(final ChangesetDownloadTask task) {
Main.worker.submit(task);
Main.worker.submit(new Runnable() {
@Override
public void run() {
if (task.isCanceled() || task.isFailed()) return;
setSelectedChangesets(task.getDownloadedChangesets());
}
});
}
}