001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.FlowLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.ComponentAdapter;
011import java.awt.event.ComponentEvent;
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.util.Collection;
015import java.util.Collections;
016import java.util.List;
017import java.util.Objects;
018import java.util.Set;
019import java.util.stream.Collectors;
020
021import javax.swing.AbstractAction;
022import javax.swing.BorderFactory;
023import javax.swing.DefaultListSelectionModel;
024import javax.swing.JButton;
025import javax.swing.JComponent;
026import javax.swing.JOptionPane;
027import javax.swing.JPanel;
028import javax.swing.JPopupMenu;
029import javax.swing.JScrollPane;
030import javax.swing.JSeparator;
031import javax.swing.JTable;
032import javax.swing.JToolBar;
033import javax.swing.SwingConstants;
034import javax.swing.event.ListSelectionEvent;
035import javax.swing.event.ListSelectionListener;
036
037import org.openstreetmap.josm.actions.AbstractShowHistoryAction;
038import org.openstreetmap.josm.actions.AutoScaleAction;
039import org.openstreetmap.josm.actions.HistoryInfoAction;
040import org.openstreetmap.josm.actions.downloadtasks.ChangesetContentDownloadTask;
041import org.openstreetmap.josm.data.osm.Changeset;
042import org.openstreetmap.josm.data.osm.DataSet;
043import org.openstreetmap.josm.data.osm.OsmPrimitive;
044import org.openstreetmap.josm.data.osm.PrimitiveId;
045import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
046import org.openstreetmap.josm.gui.HelpAwareOptionPane;
047import org.openstreetmap.josm.gui.MainApplication;
048import org.openstreetmap.josm.gui.help.HelpUtil;
049import org.openstreetmap.josm.gui.history.HistoryBrowserDialogManager;
050import org.openstreetmap.josm.gui.io.DownloadPrimitivesWithReferrersTask;
051import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
052import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
053import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
054import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
055import org.openstreetmap.josm.tools.ImageProvider;
056import org.openstreetmap.josm.tools.Utils;
057
058/**
059 * The panel which displays the content of a changeset in a scrollable table.
060 *
061 * It listens to property change events for {@link ChangesetCacheManagerModel#CHANGESET_IN_DETAIL_VIEW_PROP}
062 * and updates its view accordingly.
063 * @since 2689
064 */
065public class ChangesetContentPanel extends JPanel implements PropertyChangeListener, ChangesetAware {
066    private JTable tblContent;
067    private ChangesetContentTableModel model;
068    private transient Changeset currentChangeset;
069
070    private DownloadChangesetContentAction actDownloadContentAction;
071    private ShowHistoryAction actShowHistory;
072    private SelectInCurrentLayerAction actSelectInCurrentLayerAction;
073    private ZoomInCurrentLayerAction actZoomInCurrentLayerAction;
074
075    private final HeaderPanel pnlHeader = new HeaderPanel();
076    protected DownloadObjectAction actDownloadObjectAction;
077
078    protected void buildModels() {
079        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
080        model = new ChangesetContentTableModel(selectionModel);
081        actDownloadContentAction = new DownloadChangesetContentAction(this);
082        actDownloadContentAction.initProperties();
083
084        actDownloadObjectAction = new DownloadObjectAction();
085        model.getSelectionModel().addListSelectionListener(actDownloadObjectAction);
086
087        actShowHistory = new ShowHistoryAction();
088        model.getSelectionModel().addListSelectionListener(actShowHistory);
089
090        actSelectInCurrentLayerAction = new SelectInCurrentLayerAction();
091        model.getSelectionModel().addListSelectionListener(actSelectInCurrentLayerAction);
092        MainApplication.getLayerManager().addActiveLayerChangeListener(actSelectInCurrentLayerAction);
093
094        actZoomInCurrentLayerAction = new ZoomInCurrentLayerAction();
095        model.getSelectionModel().addListSelectionListener(actZoomInCurrentLayerAction);
096        MainApplication.getLayerManager().addActiveLayerChangeListener(actZoomInCurrentLayerAction);
097
098        addComponentListener(
099                new ComponentAdapter() {
100                    @Override
101                    public void componentShown(ComponentEvent e) {
102                        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(actSelectInCurrentLayerAction);
103                        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(actZoomInCurrentLayerAction);
104                    }
105
106                    @Override
107                    public void componentHidden(ComponentEvent e) {
108                        // make sure the listener is unregistered when the panel becomes invisible
109                        MainApplication.getLayerManager().removeActiveLayerChangeListener(actSelectInCurrentLayerAction);
110                        MainApplication.getLayerManager().removeActiveLayerChangeListener(actZoomInCurrentLayerAction);
111                    }
112                }
113        );
114    }
115
116    protected JPanel buildContentPanel() {
117        JPanel pnl = new JPanel(new BorderLayout());
118        tblContent = new JTable(
119                model,
120                new ChangesetContentTableColumnModel(),
121                model.getSelectionModel()
122        );
123        tblContent.setRowSorter(new ChangesetContentTableRowSorter(model));
124        tblContent.addMouseListener(new PopupMenuLauncher(new ChangesetContentTablePopupMenu()));
125        HistoryInfoAction historyAction = MainApplication.getMenu().historyinfo;
126        tblContent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(historyAction.getShortcut().getKeyStroke(), "historyAction");
127        tblContent.getActionMap().put("historyAction", historyAction);
128        tblContent.getTableHeader().setReorderingAllowed(false);
129        pnl.add(new JScrollPane(tblContent), BorderLayout.CENTER);
130        return pnl;
131    }
132
133    protected JPanel buildActionButtonPanel() {
134        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
135        JToolBar tb = new JToolBar(SwingConstants.VERTICAL);
136        tb.setFloatable(false);
137
138        tb.add(actDownloadContentAction);
139        tb.addSeparator();
140        tb.add(actDownloadObjectAction);
141        tb.add(actShowHistory);
142        tb.addSeparator();
143        tb.add(actSelectInCurrentLayerAction);
144        tb.add(actZoomInCurrentLayerAction);
145
146        pnl.add(tb);
147        return pnl;
148    }
149
150    protected final void build() {
151        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
152        setLayout(new BorderLayout());
153        buildModels();
154
155        add(pnlHeader, BorderLayout.NORTH);
156        add(buildActionButtonPanel(), BorderLayout.WEST);
157        add(buildContentPanel(), BorderLayout.CENTER);
158    }
159
160    /**
161     * Constructs a new {@code ChangesetContentPanel}.
162     */
163    public ChangesetContentPanel() {
164        build();
165    }
166
167    /**
168     * Replies the changeset content model
169     * @return The model
170     */
171    public ChangesetContentTableModel getModel() {
172        return model;
173    }
174
175    protected void setCurrentChangeset(Changeset cs) {
176        currentChangeset = cs;
177        if (cs == null) {
178            model.populate(null);
179        } else {
180            model.populate(cs.getContent());
181        }
182        actDownloadContentAction.initProperties();
183        pnlHeader.setChangeset(cs);
184    }
185
186    /* ---------------------------------------------------------------------------- */
187    /* interface PropertyChangeListener                                             */
188    /* ---------------------------------------------------------------------------- */
189    @Override
190    public void propertyChange(PropertyChangeEvent evt) {
191        if (!evt.getPropertyName().equals(ChangesetCacheManagerModel.CHANGESET_IN_DETAIL_VIEW_PROP))
192            return;
193        Changeset cs = (Changeset) evt.getNewValue();
194        setCurrentChangeset(cs);
195    }
196
197    private void alertNoPrimitivesTo(Collection<HistoryOsmPrimitive> primitives, String title, String helpTopic) {
198        HelpAwareOptionPane.showOptionDialog(
199                this,
200                trn("<html>The selected object is not available in the current<br>"
201                        + "edit layer ''{0}''.</html>",
202                        "<html>None of the selected objects is available in the current<br>"
203                        + "edit layer ''{0}''.</html>",
204                        primitives.size(),
205                        Utils.escapeReservedCharactersHTML(MainApplication.getLayerManager().getEditLayer().getName())
206                ),
207                title, JOptionPane.WARNING_MESSAGE, helpTopic
208        );
209    }
210
211    private Set<HistoryOsmPrimitive> getSelectedPrimitives() {
212      return model.getSelectedPrimitives(tblContent);
213    }
214
215    class ChangesetContentTablePopupMenu extends JPopupMenu {
216        ChangesetContentTablePopupMenu() {
217            add(actDownloadContentAction);
218            add(new JSeparator());
219            add(actDownloadObjectAction);
220            add(actShowHistory);
221            add(new JSeparator());
222            add(actSelectInCurrentLayerAction);
223            add(actZoomInCurrentLayerAction);
224        }
225    }
226
227    class ShowHistoryAction extends AbstractShowHistoryAction implements ListSelectionListener {
228
229        ShowHistoryAction() {
230            super();
231            updateEnabledState();
232        }
233
234        protected final void updateEnabledState() {
235            setEnabled(model.hasSelectedPrimitives());
236        }
237
238        @Override
239        public void actionPerformed(ActionEvent e) {
240            Set<HistoryOsmPrimitive> selected = getSelectedPrimitives();
241            if (selected.isEmpty()) return;
242            HistoryBrowserDialogManager.getInstance().showHistory(selected);
243        }
244
245        @Override
246        public void valueChanged(ListSelectionEvent e) {
247            updateEnabledState();
248        }
249    }
250
251    class DownloadObjectAction extends AbstractAction implements ListSelectionListener {
252
253        DownloadObjectAction() {
254            putValue(NAME, tr("Download objects"));
255            new ImageProvider("downloadprimitive").getResource().attachImageIcon(this, true);
256            putValue(SHORT_DESCRIPTION, tr("Download the current version of the selected objects"));
257            updateEnabledState();
258        }
259
260        @Override
261        public void actionPerformed(ActionEvent e) {
262            final List<PrimitiveId> primitiveIds = getSelectedPrimitives().stream().map(HistoryOsmPrimitive::getPrimitiveId)
263                    .collect(Collectors.toList());
264            MainApplication.worker.submit(new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null));
265        }
266
267        protected final void updateEnabledState() {
268            setEnabled(model.hasSelectedPrimitives());
269        }
270
271        @Override
272        public void valueChanged(ListSelectionEvent e) {
273            updateEnabledState();
274        }
275    }
276
277    abstract class SelectionBasedAction extends AbstractAction implements ListSelectionListener, ActiveLayerChangeListener {
278
279        protected Set<OsmPrimitive> getTarget() {
280            DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
281            if (isEnabled() && ds != null) {
282                return getSelectedPrimitives().stream()
283                        .map(p -> ds.getPrimitiveById(p.getPrimitiveId())).filter(Objects::nonNull).collect(Collectors.toSet());
284            }
285            return Collections.emptySet();
286        }
287
288        public final void updateEnabledState() {
289            setEnabled(MainApplication.getLayerManager().getActiveDataSet() != null && model.hasSelectedPrimitives());
290        }
291
292        @Override
293        public void valueChanged(ListSelectionEvent e) {
294            updateEnabledState();
295        }
296
297        @Override
298        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
299            updateEnabledState();
300        }
301    }
302
303    class SelectInCurrentLayerAction extends SelectionBasedAction {
304
305        SelectInCurrentLayerAction() {
306            putValue(NAME, tr("Select in layer"));
307            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this);
308            putValue(SHORT_DESCRIPTION, tr("Select the corresponding primitives in the current data layer"));
309            updateEnabledState();
310        }
311
312        @Override
313        public void actionPerformed(ActionEvent e) {
314            final Set<OsmPrimitive> target = getTarget();
315            if (target.isEmpty()) {
316                alertNoPrimitivesTo(getSelectedPrimitives(), tr("Nothing to select"),
317                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer"));
318                return;
319            }
320            MainApplication.getLayerManager().getActiveDataSet().setSelected(target);
321        }
322    }
323
324    class ZoomInCurrentLayerAction extends SelectionBasedAction {
325
326        ZoomInCurrentLayerAction() {
327            putValue(NAME, tr("Zoom to in layer"));
328            new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this);
329            putValue(SHORT_DESCRIPTION, tr("Zoom to the corresponding objects in the current data layer"));
330            updateEnabledState();
331        }
332
333        @Override
334        public void actionPerformed(ActionEvent e) {
335            final Set<OsmPrimitive> target = getTarget();
336            if (target.isEmpty()) {
337                alertNoPrimitivesTo(getSelectedPrimitives(), tr("Nothing to zoom to"),
338                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo"));
339                return;
340            }
341            MainApplication.getLayerManager().getActiveDataSet().setSelected(target);
342            AutoScaleAction.zoomToSelection();
343        }
344    }
345
346    private static class HeaderPanel extends JPanel {
347
348        private transient Changeset current;
349
350        HeaderPanel() {
351            build();
352        }
353
354        protected final void build() {
355            setLayout(new FlowLayout(FlowLayout.LEFT));
356            add(new JMultilineLabel(tr("The content of this changeset is not downloaded yet.")));
357            add(new JButton(new DownloadAction()));
358
359        }
360
361        public void setChangeset(Changeset cs) {
362            setVisible(cs != null && cs.getContent() == null);
363            this.current = cs;
364        }
365
366        private class DownloadAction extends AbstractAction {
367            DownloadAction() {
368                putValue(NAME, tr("Download now"));
369                putValue(SHORT_DESCRIPTION, tr("Download the changeset content"));
370                new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this);
371            }
372
373            @Override
374            public void actionPerformed(ActionEvent evt) {
375                if (current == null) return;
376                ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(HeaderPanel.this, current.getId());
377                ChangesetCacheManager.getInstance().runDownloadTask(task);
378            }
379        }
380    }
381
382    @Override
383    public Changeset getCurrentChangeset() {
384        return currentChangeset;
385    }
386}