001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Dimension;
008import java.awt.Point;
009import java.awt.Rectangle;
010import java.awt.event.ActionEvent;
011import java.awt.event.ItemEvent;
012import java.awt.event.ItemListener;
013import java.awt.event.KeyAdapter;
014import java.awt.event.KeyEvent;
015import java.awt.event.MouseEvent;
016import java.util.Objects;
017
018import javax.swing.DefaultCellEditor;
019import javax.swing.JCheckBox;
020import javax.swing.JLabel;
021import javax.swing.JPopupMenu;
022import javax.swing.JRadioButton;
023import javax.swing.JTable;
024import javax.swing.SwingConstants;
025import javax.swing.UIManager;
026import javax.swing.event.ChangeEvent;
027import javax.swing.event.ChangeListener;
028import javax.swing.table.TableCellRenderer;
029
030import org.openstreetmap.josm.actions.AbstractInfoAction;
031import org.openstreetmap.josm.data.osm.User;
032import org.openstreetmap.josm.data.osm.history.History;
033import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
034import org.openstreetmap.josm.gui.util.GuiHelper;
035import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
036import org.openstreetmap.josm.io.XmlWriter;
037import org.openstreetmap.josm.spi.preferences.Config;
038import org.openstreetmap.josm.tools.Destroyable;
039import org.openstreetmap.josm.tools.ImageProvider;
040import org.openstreetmap.josm.tools.OpenBrowser;
041
042/**
043 * VersionTable shows a list of version in a {@link org.openstreetmap.josm.data.osm.history.History}
044 * of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}.
045 * @since 1709
046 */
047public class VersionTable extends JTable implements ChangeListener, Destroyable {
048    private VersionTablePopupMenu popupMenu;
049    private final transient HistoryBrowserModel model;
050
051    /**
052     * Constructs a new {@code VersionTable}.
053     * @param model model used by the history browser
054     */
055    public VersionTable(HistoryBrowserModel model) {
056        super(model.getVersionTableModel(), new VersionTableColumnModel());
057        model.addChangeListener(this);
058        build();
059        this.model = model;
060    }
061
062    /**
063     * Builds the table.
064     */
065    protected void build() {
066        getTableHeader().setFont(getTableHeader().getFont().deriveFont(9f));
067        setRowSelectionAllowed(false);
068        setShowGrid(false);
069        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
070        GuiHelper.setBackgroundReadable(this, UIManager.getColor("Button.background"));
071        setIntercellSpacing(new Dimension(6, 0));
072        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
073        popupMenu = new VersionTablePopupMenu();
074        addMouseListener(new MouseListener());
075        addKeyListener(new KeyAdapter() {
076            @Override
077            public void keyReleased(KeyEvent e) {
078                // navigate history down/up using the corresponding arrow keys.
079                long ref = model.getReferencePointInTime().getVersion();
080                long cur = model.getCurrentPointInTime().getVersion();
081                if (e.getKeyCode() == KeyEvent.VK_DOWN) {
082                    History refNext = model.getHistory().from(ref);
083                    History curNext = model.getHistory().from(cur);
084                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
085                        model.setReferencePointInTime(refNext.sortAscending().get(1));
086                        model.setCurrentPointInTime(curNext.sortAscending().get(1));
087                    }
088                } else if (e.getKeyCode() == KeyEvent.VK_UP) {
089                    History refNext = model.getHistory().until(ref);
090                    History curNext = model.getHistory().until(cur);
091                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
092                        model.setReferencePointInTime(refNext.sortDescending().get(1));
093                        model.setCurrentPointInTime(curNext.sortDescending().get(1));
094                    }
095                }
096            }
097        });
098        getModel().addTableModelListener(e -> {
099            adjustColumnWidth(this, 0, 0);
100            adjustColumnWidth(this, 1, -8);
101            adjustColumnWidth(this, 2, -8);
102            adjustColumnWidth(this, 3, 0);
103            adjustColumnWidth(this, 4, 0);
104            adjustColumnWidth(this, 5, 0);
105        });
106    }
107
108    @Override
109    public void destroy() {
110        popupMenu.destroy();
111    }
112
113    // some kind of hack to prevent the table from scrolling to the
114    // right when clicking on the cells
115    @Override
116    public void scrollRectToVisible(Rectangle aRect) {
117        super.scrollRectToVisible(new Rectangle(0, aRect.y, aRect.width, aRect.height));
118    }
119
120    @Override
121    public void stateChanged(ChangeEvent e) {
122        repaint();
123    }
124
125    final class MouseListener extends PopupMenuLauncher {
126        private MouseListener() {
127            super(Objects.requireNonNull(popupMenu));
128        }
129
130        @Override
131        public void mousePressed(MouseEvent e) {
132            super.mousePressed(e);
133            if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
134                int row = rowAtPoint(e.getPoint());
135                int col = columnAtPoint(e.getPoint());
136                if (row >= 0 && (col == VersionTableColumnModel.COL_DATE || col == VersionTableColumnModel.COL_USER)) {
137                    model.setCurrentPointInTime(row);
138                    model.setReferencePointInTime(Math.max(0, row - 1));
139                }
140            }
141        }
142
143        @Override
144        protected int checkTableSelection(JTable table, Point p) {
145            int row = rowAtPoint(p);
146            if (row > -1 && !model.isLatest(row)) {
147                popupMenu.prepare(model.getPrimitive(row));
148            }
149            return row;
150        }
151    }
152
153    static class ChangesetInfoAction extends AbstractInfoAction {
154        private transient HistoryOsmPrimitive primitive;
155
156        /**
157         * Constructs a new {@code ChangesetInfoAction}.
158         */
159        ChangesetInfoAction() {
160            super(true);
161            putValue(NAME, tr("Changeset info"));
162            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the changeset"));
163            new ImageProvider("data/changeset").getResource().attachImageIcon(this, true);
164        }
165
166        @Override
167        protected String createInfoUrl(Object infoObject) {
168            if (infoObject instanceof HistoryOsmPrimitive) {
169                HistoryOsmPrimitive prim = (HistoryOsmPrimitive) infoObject;
170                return Config.getUrls().getBaseBrowseUrl() + "/changeset/" + prim.getChangesetId();
171            } else {
172                return null;
173            }
174        }
175
176        @Override
177        public void actionPerformed(ActionEvent e) {
178            if (!isEnabled())
179                return;
180            String url = createInfoUrl(primitive);
181            OpenBrowser.displayUrl(url);
182        }
183
184        public void prepare(HistoryOsmPrimitive primitive) {
185            putValue(NAME, tr("Show changeset {0}", primitive.getChangesetId()));
186            this.primitive = primitive;
187        }
188    }
189
190    static class UserInfoAction extends AbstractInfoAction {
191        private transient HistoryOsmPrimitive primitive;
192
193        /**
194         * Constructs a new {@code UserInfoAction}.
195         */
196        UserInfoAction() {
197            super(true);
198            putValue(NAME, tr("User info"));
199            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the user"));
200            new ImageProvider("data/user").getResource().attachImageIcon(this, true);
201        }
202
203        @Override
204        protected String createInfoUrl(Object infoObject) {
205            if (infoObject instanceof HistoryOsmPrimitive) {
206                HistoryOsmPrimitive hp = (HistoryOsmPrimitive) infoObject;
207                return hp.getUser() == null ? null : Config.getUrls().getBaseUserUrl() + '/' + hp.getUser().getName();
208            } else {
209                return null;
210            }
211        }
212
213        @Override
214        public void actionPerformed(ActionEvent e) {
215            if (!isEnabled())
216                return;
217            String url = createInfoUrl(primitive);
218            OpenBrowser.displayUrl(url);
219        }
220
221        public void prepare(HistoryOsmPrimitive primitive) {
222            final User user = primitive.getUser();
223            putValue(NAME, "<html>" + tr("Show user {0}", user == null ? "?" :
224                    XmlWriter.encode(user.getName(), true) + " <font color=gray>(" + user.getId() + ")</font>") + "</html>");
225            this.primitive = primitive;
226        }
227    }
228
229    static class VersionTablePopupMenu extends JPopupMenu implements Destroyable {
230
231        private ChangesetInfoAction changesetInfoAction;
232        private UserInfoAction userInfoAction;
233
234        /**
235         * Constructs a new {@code VersionTablePopupMenu}.
236         */
237        VersionTablePopupMenu() {
238            super();
239            build();
240        }
241
242        protected void build() {
243            changesetInfoAction = new ChangesetInfoAction();
244            add(changesetInfoAction);
245            userInfoAction = new UserInfoAction();
246            add(userInfoAction);
247        }
248
249        public void prepare(HistoryOsmPrimitive primitive) {
250            changesetInfoAction.prepare(primitive);
251            userInfoAction.prepare(primitive);
252            invalidate();
253        }
254
255        @Override
256        public void destroy() {
257            if (changesetInfoAction != null)
258                changesetInfoAction.destroy();
259            if (userInfoAction != null)
260                userInfoAction.destroy();
261        }
262    }
263
264    /**
265     * Renderer for history radio buttons in columns A and B.
266     */
267    public static class RadioButtonRenderer extends JRadioButton implements TableCellRenderer {
268
269        @Override
270        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
271                int row, int column) {
272            setSelected(value != null && (Boolean) value);
273            setHorizontalAlignment(SwingConstants.CENTER);
274            return this;
275        }
276    }
277
278    /**
279     * Editor for history radio buttons in columns A and B.
280     */
281    public static class RadioButtonEditor extends DefaultCellEditor implements ItemListener {
282
283        private final JRadioButton btn;
284
285        /**
286         * Constructs a new {@code RadioButtonEditor}.
287         */
288        public RadioButtonEditor() {
289            super(new JCheckBox());
290            btn = new JRadioButton();
291            btn.setHorizontalAlignment(SwingConstants.CENTER);
292        }
293
294        @Override
295        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
296            if (value == null)
297                return null;
298            boolean val = (Boolean) value;
299            btn.setSelected(val);
300            btn.addItemListener(this);
301            return btn;
302        }
303
304        @Override
305        public Object getCellEditorValue() {
306            btn.removeItemListener(this);
307            return btn.isSelected();
308        }
309
310        @Override
311        public void itemStateChanged(ItemEvent e) {
312            fireEditingStopped();
313        }
314    }
315
316    /**
317     * Renderer for history version labels, allowing to define horizontal alignment.
318     */
319    public static class AlignedRenderer extends JLabel implements TableCellRenderer {
320
321        /**
322         * Constructs a new {@code AlignedRenderer}.
323         * @param hAlignment Horizontal alignement. One of the following constants defined in SwingConstants:
324         *        LEFT, CENTER (the default for image-only labels), RIGHT, LEADING (the default for text-only labels) or TRAILING
325         */
326        public AlignedRenderer(int hAlignment) {
327            setHorizontalAlignment(hAlignment);
328        }
329
330        AlignedRenderer() {
331            this(SwingConstants.LEFT);
332        }
333
334        @Override
335        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
336                int row, int column) {
337            String v = "";
338            if (value != null) {
339                v = value.toString();
340            }
341            setText(v);
342            return this;
343        }
344    }
345
346    private static void adjustColumnWidth(JTable tbl, int col, int cellInset) {
347        int maxwidth = 0;
348
349        for (int row = 0; row < tbl.getRowCount(); row++) {
350            TableCellRenderer tcr = tbl.getCellRenderer(row, col);
351            Object val = tbl.getValueAt(row, col);
352            Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col);
353            maxwidth = Math.max(comp.getPreferredSize().width + cellInset, maxwidth);
354        }
355        TableCellRenderer tcr = tbl.getTableHeader().getDefaultRenderer();
356        Object val = tbl.getColumnModel().getColumn(col).getHeaderValue();
357        Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, -1, col);
358        maxwidth = Math.max(comp.getPreferredSize().width + Config.getPref().getInt("table.header-inset", 0), maxwidth);
359
360        int spacing = tbl.getIntercellSpacing().width;
361        tbl.getColumnModel().getColumn(col).setPreferredWidth(maxwidth + spacing);
362    }
363}