// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.DefaultListSelectionModel; import javax.swing.JCheckBox; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.AbstractInfoAction; import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask; 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.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.event.DatasetEventManager; import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetInSelectionListModel; import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListCellRenderer; import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListModel; import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetsInActiveDataLayerListModel; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.io.CloseChangesetTask; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.widgets.ListPopupMenu; import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; import org.openstreetmap.josm.io.OnlineResource; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.OpenBrowser; import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler; /** * ChangesetDialog is a toggle dialog which displays the current list of changesets. * It either displays * <ul> * <li>the list of changesets the currently selected objects are assigned to</li> * <li>the list of changesets objects in the current data layer are assigend to</li> * </ul> * * The dialog offers actions to download and to close changesets. It can also launch an external * browser with information about a changeset. Furthermore, it can select all objects in * the current data layer being assigned to a specific changeset. * @since 2613 */ public class ChangesetDialog extends ToggleDialog { private ChangesetInSelectionListModel inSelectionModel; private ChangesetsInActiveDataLayerListModel inActiveDataLayerModel; private JList<Changeset> lstInSelection; private JList<Changeset> lstInActiveDataLayer; private JCheckBox cbInSelectionOnly; private JPanel pnlList; // the actions private SelectObjectsAction selectObjectsAction; private ReadChangesetsAction readChangesetAction; private ShowChangesetInfoAction showChangesetInfoAction; private CloseOpenChangesetsAction closeChangesetAction; private ChangesetDialogPopup popupMenu; protected void buildChangesetsLists() { DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); inSelectionModel = new ChangesetInSelectionListModel(selectionModel); lstInSelection = new JList<>(inSelectionModel); lstInSelection.setSelectionModel(selectionModel); lstInSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); lstInSelection.setCellRenderer(new ChangesetListCellRenderer()); selectionModel = new DefaultListSelectionModel(); inActiveDataLayerModel = new ChangesetsInActiveDataLayerListModel(selectionModel); lstInActiveDataLayer = new JList<>(inActiveDataLayerModel); lstInActiveDataLayer.setSelectionModel(selectionModel); lstInActiveDataLayer.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); lstInActiveDataLayer.setCellRenderer(new ChangesetListCellRenderer()); DblClickHandler dblClickHandler = new DblClickHandler(); lstInSelection.addMouseListener(dblClickHandler); lstInActiveDataLayer.addMouseListener(dblClickHandler); } protected void registerAsListener() { // let the model for changesets in the current selection listen to various events ChangesetCache.getInstance().addChangesetCacheListener(inSelectionModel); Main.getLayerManager().addActiveLayerChangeListener(inSelectionModel); DataSet.addSelectionListener(inSelectionModel); // let the model for changesets in the current layer listen to various // events and bootstrap it's content ChangesetCache.getInstance().addChangesetCacheListener(inActiveDataLayerModel); Main.getLayerManager().addActiveLayerChangeListener(inActiveDataLayerModel); OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); if (editLayer != null) { editLayer.data.addDataSetListener(inActiveDataLayerModel); inActiveDataLayerModel.initFromDataSet(editLayer.data); inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected()); } } protected void unregisterAsListener() { // remove the list model for the current edit layer as listener // ChangesetCache.getInstance().removeChangesetCacheListener(inActiveDataLayerModel); Main.getLayerManager().removeActiveLayerChangeListener(inActiveDataLayerModel); OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); if (editLayer != null) { editLayer.data.removeDataSetListener(inActiveDataLayerModel); } // remove the list model for the changesets in the current selection as // listener // Main.getLayerManager().removeActiveLayerChangeListener(inSelectionModel); DataSet.removeSelectionListener(inSelectionModel); } @Override public void showNotify() { registerAsListener(); DatasetEventManager.getInstance().addDatasetListener(inActiveDataLayerModel, FireMode.IN_EDT); } @Override public void hideNotify() { unregisterAsListener(); DatasetEventManager.getInstance().removeDatasetListener(inActiveDataLayerModel); } protected JPanel buildFilterPanel() { JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); pnl.setBorder(null); cbInSelectionOnly = new JCheckBox(tr("For selected objects only")); pnl.add(cbInSelectionOnly); cbInSelectionOnly.setToolTipText(tr("<html>Select to show changesets for the currently selected objects only.<br>" + "Unselect to show all changesets for objects in the current data layer.</html>")); cbInSelectionOnly.setSelected(Main.pref.getBoolean("changeset-dialog.for-selected-objects-only", false)); return pnl; } protected JPanel buildListPanel() { buildChangesetsLists(); JPanel pnl = new JPanel(new BorderLayout()); if (cbInSelectionOnly.isSelected()) { pnl.add(new JScrollPane(lstInSelection)); } else { pnl.add(new JScrollPane(lstInActiveDataLayer)); } return pnl; } protected void build() { JPanel pnl = new JPanel(new BorderLayout()); pnl.add(buildFilterPanel(), BorderLayout.NORTH); pnlList = buildListPanel(); pnl.add(pnlList, BorderLayout.CENTER); cbInSelectionOnly.addItemListener(new FilterChangeHandler()); HelpUtil.setHelpContext(pnl, HelpUtil.ht("/Dialog/ChangesetList")); // -- select objects action selectObjectsAction = new SelectObjectsAction(); cbInSelectionOnly.addItemListener(selectObjectsAction); // -- read changesets action readChangesetAction = new ReadChangesetsAction(); cbInSelectionOnly.addItemListener(readChangesetAction); // -- close changesets action closeChangesetAction = new CloseOpenChangesetsAction(); cbInSelectionOnly.addItemListener(closeChangesetAction); // -- show info action showChangesetInfoAction = new ShowChangesetInfoAction(); cbInSelectionOnly.addItemListener(showChangesetInfoAction); popupMenu = new ChangesetDialogPopup(lstInActiveDataLayer, lstInSelection); PopupMenuLauncher popupMenuLauncher = new PopupMenuLauncher(popupMenu); lstInSelection.addMouseListener(popupMenuLauncher); lstInActiveDataLayer.addMouseListener(popupMenuLauncher); createLayout(pnl, false, Arrays.asList(new SideButton[] { new SideButton(selectObjectsAction, false), new SideButton(readChangesetAction, false), new SideButton(closeChangesetAction, false), new SideButton(showChangesetInfoAction, false), new SideButton(new LaunchChangesetManagerAction(), false) })); } protected JList<Changeset> getCurrentChangesetList() { if (cbInSelectionOnly.isSelected()) return lstInSelection; return lstInActiveDataLayer; } protected ChangesetListModel getCurrentChangesetListModel() { if (cbInSelectionOnly.isSelected()) return inSelectionModel; return inActiveDataLayerModel; } protected void initWithCurrentData() { OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); if (editLayer != null) { inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected()); inActiveDataLayerModel.initFromDataSet(editLayer.data); } } /** * Constructs a new {@code ChangesetDialog}. */ public ChangesetDialog() { super( tr("Changesets"), "changesetdialog", tr("Open the list of changesets in the current layer."), null, /* no keyboard shortcut */ 200, /* the preferred height */ false /* don't show if there is no preference */ ); build(); initWithCurrentData(); } class DblClickHandler extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() < 2) return; Set<Integer> sel = getCurrentChangesetListModel().getSelectedChangesetIds(); if (sel.isEmpty()) return; if (Main.getLayerManager().getEditDataSet() == null) return; new SelectObjectsAction().selectObjectsByChangesetIds(Main.getLayerManager().getEditDataSet(), sel); } } class FilterChangeHandler implements ItemListener { @Override public void itemStateChanged(ItemEvent e) { Main.pref.put("changeset-dialog.for-selected-objects-only", cbInSelectionOnly.isSelected()); pnlList.removeAll(); if (cbInSelectionOnly.isSelected()) { pnlList.add(new JScrollPane(lstInSelection), BorderLayout.CENTER); } else { pnlList.add(new JScrollPane(lstInActiveDataLayer), BorderLayout.CENTER); } validate(); repaint(); } } /** * Selects objects for the currently selected changesets. */ class SelectObjectsAction extends AbstractAction implements ListSelectionListener, ItemListener { SelectObjectsAction() { putValue(NAME, tr("Select")); putValue(SHORT_DESCRIPTION, tr("Select all objects assigned to the currently selected changesets")); new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); updateEnabledState(); } public void selectObjectsByChangesetIds(DataSet ds, Set<Integer> ids) { if (ds == null || ids == null) return; Set<OsmPrimitive> sel = new HashSet<>(); for (OsmPrimitive p: ds.allPrimitives()) { if (ids.contains(p.getChangesetId())) { sel.add(p); } } ds.setSelected(sel); } @Override public void actionPerformed(ActionEvent e) { if (Main.getLayerManager().getEditLayer() == null) return; ChangesetListModel model = getCurrentChangesetListModel(); Set<Integer> sel = model.getSelectedChangesetIds(); if (sel.isEmpty()) return; DataSet ds = Main.getLayerManager().getEditLayer().data; selectObjectsByChangesetIds(ds, sel); } protected void updateEnabledState() { setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0); } @Override public void itemStateChanged(ItemEvent e) { updateEnabledState(); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Downloads selected changesets * */ class ReadChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener { ReadChangesetsAction() { putValue(NAME, tr("Download")); putValue(SHORT_DESCRIPTION, tr("Download information about the selected changesets from the OSM server")); new ImageProvider("download").getResource().attachImageIcon(this, true); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { ChangesetListModel model = getCurrentChangesetListModel(); Set<Integer> sel = model.getSelectedChangesetIds(); if (sel.isEmpty()) return; ChangesetHeaderDownloadTask task = new ChangesetHeaderDownloadTask(sel); Main.worker.submit(new PostDownloadHandler(task, task.download())); } protected void updateEnabledState() { setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0 && !Main.isOffline(OnlineResource.OSM_API)); } @Override public void itemStateChanged(ItemEvent e) { updateEnabledState(); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Closes the currently selected changesets * */ class CloseOpenChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener { CloseOpenChangesetsAction() { putValue(NAME, tr("Close open changesets")); putValue(SHORT_DESCRIPTION, tr("Closes the selected open changesets")); new ImageProvider("closechangeset").getResource().attachImageIcon(this, true); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { List<Changeset> sel = getCurrentChangesetListModel().getSelectedOpenChangesets(); if (sel.isEmpty()) return; Main.worker.submit(new CloseChangesetTask(sel)); } protected void updateEnabledState() { setEnabled(getCurrentChangesetListModel().hasSelectedOpenChangesets()); } @Override public void itemStateChanged(ItemEvent e) { updateEnabledState(); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Show information about the currently selected changesets * */ class ShowChangesetInfoAction extends AbstractAction implements ListSelectionListener, ItemListener { ShowChangesetInfoAction() { putValue(NAME, tr("Show info")); putValue(SHORT_DESCRIPTION, tr("Open a web page for each selected changeset")); new ImageProvider("help/internet").getResource().attachImageIcon(this, true); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { Set<Changeset> sel = getCurrentChangesetListModel().getSelectedChangesets(); if (sel.isEmpty()) return; if (sel.size() > 10 && !AbstractInfoAction.confirmLaunchMultiple(sel.size())) return; String baseUrl = Main.getBaseBrowseUrl(); for (Changeset cs: sel) { OpenBrowser.displayUrl(baseUrl + "/changeset/" + cs.getId()); } } protected void updateEnabledState() { setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0); } @Override public void itemStateChanged(ItemEvent e) { updateEnabledState(); } @Override public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Show information about the currently selected changesets * */ class LaunchChangesetManagerAction extends AbstractAction { LaunchChangesetManagerAction() { putValue(NAME, tr("Details")); putValue(SHORT_DESCRIPTION, tr("Opens the Changeset Manager window for the selected changesets")); new ImageProvider("dialogs/changeset", "changesetmanager").getResource().attachImageIcon(this, true); } @Override public void actionPerformed(ActionEvent e) { ChangesetListModel model = getCurrentChangesetListModel(); Set<Integer> sel = model.getSelectedChangesetIds(); LaunchChangesetManager.displayChangesets(sel); } } /** * A utility class to fetch changesets and display the changeset dialog. */ public static final class LaunchChangesetManager { private LaunchChangesetManager() { // Hide implicit public constructor for utility classes } private static void launchChangesetManager(Collection<Integer> toSelect) { ChangesetCacheManager cm = ChangesetCacheManager.getInstance(); if (cm.isVisible()) { cm.setExtendedState(Frame.NORMAL); cm.toFront(); cm.requestFocus(); } else { cm.setVisible(true); cm.toFront(); cm.requestFocus(); } cm.setSelectedChangesetsById(toSelect); } /** * Fetches changesets and display the changeset dialog. * @param sel the changeset ids to fetch and display. */ public static void displayChangesets(final Set<Integer> sel) { final Set<Integer> toDownload = new HashSet<>(); if (!Main.isOffline(OnlineResource.OSM_API)) { ChangesetCache cc = ChangesetCache.getInstance(); for (int id: sel) { if (!cc.contains(id)) { toDownload.add(id); } } } final ChangesetHeaderDownloadTask task; final Future<?> future; if (toDownload.isEmpty()) { task = null; future = null; } else { task = new ChangesetHeaderDownloadTask(toDownload); future = Main.worker.submit(new PostDownloadHandler(task, task.download())); } Runnable r = () -> { // first, wait for the download task to finish, if a download task was launched if (future != null) { try { future.get(); } catch (InterruptedException e1) { Main.warn(e1, "InterruptedException in ChangesetDialog while downloading changeset header"); Thread.currentThread().interrupt(); } catch (ExecutionException e2) { Main.error(e2); BugReportExceptionHandler.handleException(e2.getCause()); return; } } if (task != null) { if (task.isCanceled()) // don't launch the changeset manager if the download task was canceled return; if (task.isFailed()) { toDownload.clear(); } } // launch the task GuiHelper.runInEDT(() -> launchChangesetManager(sel)); }; Main.worker.submit(r); } } class ChangesetDialogPopup extends ListPopupMenu { ChangesetDialogPopup(JList<?>... lists) { super(lists); add(selectObjectsAction); addSeparator(); add(readChangesetAction); add(closeChangesetAction); addSeparator(); add(showChangesetInfoAction); } } public void addPopupMenuSeparator() { popupMenu.addSeparator(); } public JMenuItem addPopupMenuAction(Action a) { return popupMenu.add(a); } }