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