// 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.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GraphicsEnvironment; import java.awt.Window; 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.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; 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.actions.downloadtasks.AbstractChangesetDownloadTask; import org.openstreetmap.josm.actions.downloadtasks.ChangesetContentDownloadTask; import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask; import org.openstreetmap.josm.actions.downloadtasks.ChangesetQueryTask; import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; import org.openstreetmap.josm.data.osm.Changeset; import org.openstreetmap.josm.data.osm.ChangesetCache; import org.openstreetmap.josm.data.osm.ChangesetDataSet; import org.openstreetmap.josm.data.osm.PrimitiveId; import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; import org.openstreetmap.josm.gui.HelpAwareOptionPane; import org.openstreetmap.josm.gui.JosmUserIdentityManager; import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog; 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.io.DownloadPrimitivesWithReferrersTask; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; import org.openstreetmap.josm.io.ChangesetQuery; import org.openstreetmap.josm.io.OnlineResource; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.InputMapUtils; import org.openstreetmap.josm.tools.StreamUtils; 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. * @since 2689 */ public class ChangesetCacheManager extends JFrame { /** the unique instance of the cache manager */ private static volatile ChangesetCacheManager instance; private JTabbedPane pnlChangesetDetailTabs; /** * 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 DownloadSelectedChangesetObjectsAction actDownloadSelectedChangesetObjects; private JTable tblChangesets; /** * Creates the various models required. * @return the changeset cache model */ static ChangesetCacheManagerModel buildModel() { DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); return new ChangesetCacheManagerModel(selectionModel); } /** * builds the toolbar panel in the heading of the dialog * * @return the toolbar panel */ static JPanel buildToolbarPanel() { JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton btn = new JButton(new QueryAction()); pnl.add(btn); pnl.add(new SingleChangesetDownloadPanel()); pnl.add(new JButton(new DownloadMyChangesets())); return pnl; } /** * builds the button panel in the footer of the dialog * * @return the button row pane */ static JPanel buildButtonPanel() { JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); //-- cancel and close action pnl.add(new JButton(new CancelAction())); //-- help action pnl.add(new JButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/ChangesetManager")))); 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(); pnlChangesetDetailTabs = tp; // -- 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); // -- add the panel for the changeset discussion ChangesetDiscussionPanel pnlChangesetDiscussion = new ChangesetDiscussionPanel(); tp.add(pnlChangesetDiscussion); model.addPropertyChangeListener(pnlChangesetDiscussion); 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")); tp.setTitleAt(3, tr("Discussion")); tp.setToolTipTextAt(3, tr("Display the public discussion around this 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); // -- download the objects contained in the selected changesets from the OSM server model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesetObjects); tb.add(actDownloadSelectedChangesetObjects); 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()); InputMapUtils.addEnterAction(tblChangesets, new ShowDetailAction(model)); model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer(model)); // 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()); model = buildModel(); actRemoveFromCacheAction = new RemoveFromCacheAction(model); actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction(model); actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction(model); actDownloadSelectedContent = new DownloadSelectedChangesetContentAction(model); actDownloadSelectedChangesetObjects = new DownloadSelectedChangesetObjectsAction(); cp.add(buildToolbarPanel(), BorderLayout.NORTH); cp.add(buildContentPanel(), BorderLayout.CENTER); cp.add(buildButtonPanel(), BorderLayout.SOUTH); // the help context HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetManager")); // make the dialog respond to ESC InputMapUtils.addEscapeAction(getRootPane(), 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 e) { if (needsSplitPaneAdjustment) { spContent.setDividerLocation(0.5); needsSplitPaneAdjustment = false; } } } /** * the cancel / close action */ static class CancelAction extends AbstractAction { CancelAction() { putValue(NAME, tr("Close")); new ImageProvider("cancel").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Close the dialog")); } public void cancelAndClose() { destroyInstance(); } @Override public void actionPerformed(ActionEvent e) { cancelAndClose(); } } /** * The action to query and download changesets */ static class QueryAction extends AbstractAction { QueryAction() { putValue(NAME, tr("Query")); new ImageProvider("dialogs", "search").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets")); setEnabled(!Main.isOffline(OnlineResource.OSM_API)); } @Override public void actionPerformed(ActionEvent evt) { Window parent = GuiHelper.getWindowAncestorFor(evt); if (!GraphicsEnvironment.isHeadless()) { ChangesetQueryDialog dialog = new ChangesetQueryDialog(parent); dialog.initForUserInput(); dialog.setVisible(true); if (dialog.isCanceled()) return; try { ChangesetQuery query = dialog.getChangesetQuery(); if (query != null) { ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query)); } } catch (IllegalStateException e) { Main.error(e); JOptionPane.showMessageDialog(parent, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE); } } } } /** * Removes the selected changesets from the local changeset cache * */ static class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener { private final ChangesetCacheManagerModel model; RemoveFromCacheAction(ChangesetCacheManagerModel model) { putValue(NAME, tr("Remove from cache")); new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache")); this.model = model; updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { ChangesetCache.getInstance().remove(model.getSelectedChangesets()); } protected void updateEnabledState() { setEnabled(model.hasSelectedChangesets()); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Closes the selected changesets * */ static class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener { private final ChangesetCacheManagerModel model; CloseSelectedChangesetsAction(ChangesetCacheManagerModel model) { putValue(NAME, tr("Close")); new ImageProvider("closechangeset").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Close the selected changesets")); this.model = model; updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { Main.worker.submit(new CloseChangesetTask(model.getSelectedChangesets())); } 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 * */ static class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener { private final ChangesetCacheManagerModel model; DownloadSelectedChangesetsAction(ChangesetCacheManagerModel model) { putValue(NAME, tr("Update changeset")); new ImageProvider("dialogs/changeset", "updatechangeset").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server")); this.model = model; updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { if (!GraphicsEnvironment.isHeadless()) { ChangesetCacheManager.getInstance().runDownloadTask( ChangesetHeaderDownloadTask.buildTaskForChangesets(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesets())); } } protected void updateEnabledState() { setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API)); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Downloads the content of selected changesets from the OSM server * */ static class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener { private final ChangesetCacheManagerModel model; DownloadSelectedChangesetContentAction(ChangesetCacheManagerModel model) { putValue(NAME, tr("Download changeset content")); new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server")); this.model = model; updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { if (!GraphicsEnvironment.isHeadless()) { ChangesetCacheManager.getInstance().runDownloadTask( new ChangesetContentDownloadTask(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesetIds())); } } protected void updateEnabledState() { setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API)); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Downloads the objects contained in the selected changesets from the OSM server */ private class DownloadSelectedChangesetObjectsAction extends AbstractAction implements ListSelectionListener { DownloadSelectedChangesetObjectsAction() { putValue(NAME, tr("Download changed objects")); new ImageProvider("downloadprimitive").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Download the current version of the changed objects in the selected changesets")); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { if (!GraphicsEnvironment.isHeadless()) { actDownloadSelectedContent.actionPerformed(e); Main.worker.submit(() -> { final List<PrimitiveId> primitiveIds = model.getSelectedChangesets().stream() .map(Changeset::getContent) .filter(Objects::nonNull) .flatMap(content -> StreamUtils.toStream(content::iterator)) .map(ChangesetDataSet.ChangesetDataSetEntry::getPrimitive) .map(HistoryOsmPrimitive::getPrimitiveId) .distinct() .collect(Collectors.toList()); new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null).run(); }); } } protected void updateEnabledState() { setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API)); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } static class ShowDetailAction extends AbstractAction { private final ChangesetCacheManagerModel model; ShowDetailAction(ChangesetCacheManagerModel model) { this.model = model; } protected void showDetails() { List<Changeset> selected = model.getSelectedChangesets(); if (selected.size() == 1) { model.setChangesetInDetailView(selected.get(0)); } } @Override public void actionPerformed(ActionEvent e) { showDetails(); } } static class DownloadMyChangesets extends AbstractAction { DownloadMyChangesets() { putValue(NAME, tr("My changesets")); new ImageProvider("dialogs/changeset", "downloadchangeset").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)")); setEnabled(!Main.isOffline(OnlineResource.OSM_API)); } protected void alertAnonymousUser(Component parent) { HelpAwareOptionPane.showOptionDialog( parent, 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/ChangesetManager#CanDownloadMyChangesets") ); } @Override public void actionPerformed(ActionEvent e) { Window parent = GuiHelper.getWindowAncestorFor(e); JosmUserIdentityManager im = JosmUserIdentityManager.getInstance(); if (im.isAnonymous()) { alertAnonymousUser(parent); return; } ChangesetQuery query = new ChangesetQuery(); if (im.isFullyIdentified()) { query = query.forUser(im.getUserId()); } else { query = query.forUser(im.getUserName()); } if (!GraphicsEnvironment.isHeadless()) { ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query)); } } } class MouseEventHandler extends PopupMenuLauncher { MouseEventHandler() { super(new ChangesetTablePopupMenu()); } @Override public void mouseClicked(MouseEvent evt) { if (isDoubleClick(evt)) { new ShowDetailAction(model).showDetails(); } } } class ChangesetTablePopupMenu extends JPopupMenu { ChangesetTablePopupMenu() { add(actRemoveFromCacheAction); add(actCloseSelectedChangesetsAction); add(actDownloadSelectedChangesets); add(actDownloadSelectedContent); add(actDownloadSelectedChangesetObjects); } } static class ChangesetDetailViewSynchronizer implements ListSelectionListener { private final ChangesetCacheManagerModel model; ChangesetDetailViewSynchronizer(ChangesetCacheManagerModel model) { this.model = model; } @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); final int idx = model.getSelectionModel().getMinSelectionIndex(); if (idx < 0) return; GuiHelper.runInEDTAndWait(() -> 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); } /** * Selects the given component in the detail tabbed panel * @param clazz the class of the component to select */ public void setSelectedComponentInDetailPanel(Class<? extends JComponent> clazz) { for (Component component : pnlChangesetDetailTabs.getComponents()) { if (component.getClass().equals(clazz)) { pnlChangesetDetailTabs.setSelectedComponent(component); break; } } } /** * Runs the given changeset download task. * @param task The changeset download task to run */ public void runDownloadTask(final AbstractChangesetDownloadTask task) { Main.worker.submit(new PostDownloadHandler(task, task.download())); Main.worker.submit(() -> { if (task.isCanceled() || task.isFailed()) return; GuiHelper.runInEDT(() -> setSelectedChangesets(task.getDownloadedData())); }); } }