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        });
100    }
101
102    // some kind of hack to prevent the table from scrolling to the
103    // right when clicking on the cells
104    @Override
105    public void scrollRectToVisible(Rectangle aRect) {
106        super.scrollRectToVisible(new Rectangle(0, aRect.y, aRect.width, aRect.height));
107    }
108
109    protected HistoryBrowserModel.VersionTableModel getVersionTableModel() {
110        return (HistoryBrowserModel.VersionTableModel) getModel();
111    }
112
113    @Override
114    public void stateChanged(ChangeEvent e) {
115        repaint();
116    }
117
118    final class MouseListener extends PopupMenuLauncher {
119        private MouseListener() {
120            super(popupMenu);
121        }
122
123        @Override
124        public void mousePressed(MouseEvent e) {
125            super.mousePressed(e);
126            if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
127                int row = rowAtPoint(e.getPoint());
128                int col = columnAtPoint(e.getPoint());
129                if (row >= 0 && (col == VersionTableColumnModel.COL_DATE || col == VersionTableColumnModel.COL_USER)) {
130                    model.getVersionTableModel().setCurrentPointInTime(row);
131                    model.getVersionTableModel().setReferencePointInTime(Math.max(0, row - 1));
132                }
133            }
134        }
135
136        @Override
137        protected int checkTableSelection(JTable table, Point p) {
138            HistoryBrowserModel.VersionTableModel tableModel = getVersionTableModel();
139            int row = rowAtPoint(p);
140            if (row > -1 && !tableModel.isLatest(row)) {
141                popupMenu.prepare(tableModel.getPrimitive(row));
142            }
143            return row;
144        }
145    }
146
147    static class ChangesetInfoAction extends AbstractInfoAction {
148        private transient HistoryOsmPrimitive primitive;
149
150        /**
151         * Constructs a new {@code ChangesetInfoAction}.
152         */
153        ChangesetInfoAction() {
154            super(true);
155            putValue(NAME, tr("Changeset info"));
156            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the changeset"));
157            putValue(SMALL_ICON, ImageProvider.get("data/changeset"));
158        }
159
160        @Override
161        protected String createInfoUrl(Object infoObject) {
162            if (infoObject instanceof HistoryOsmPrimitive) {
163                HistoryOsmPrimitive prim = (HistoryOsmPrimitive) infoObject;
164                return Main.getBaseBrowseUrl() + "/changeset/" + prim.getChangesetId();
165            } else {
166                return null;
167            }
168        }
169
170        @Override
171        public void actionPerformed(ActionEvent e) {
172            if (!isEnabled())
173                return;
174            String url = createInfoUrl(primitive);
175            OpenBrowser.displayUrl(url);
176        }
177
178        public void prepare(HistoryOsmPrimitive primitive) {
179            putValue(NAME, tr("Show changeset {0}", primitive.getChangesetId()));
180            this.primitive = primitive;
181        }
182    }
183
184    static class UserInfoAction extends AbstractInfoAction {
185        private transient HistoryOsmPrimitive primitive;
186
187        /**
188         * Constructs a new {@code UserInfoAction}.
189         */
190        UserInfoAction() {
191            super(true);
192            putValue(NAME, tr("User info"));
193            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the user"));
194            putValue(SMALL_ICON, ImageProvider.get("data/user"));
195        }
196
197        @Override
198        protected String createInfoUrl(Object infoObject) {
199            if (infoObject instanceof HistoryOsmPrimitive) {
200                HistoryOsmPrimitive hp = (HistoryOsmPrimitive) infoObject;
201                return hp.getUser() == null ? null : Main.getBaseUserUrl() + '/' + hp.getUser().getName();
202            } else {
203                return null;
204            }
205        }
206
207        @Override
208        public void actionPerformed(ActionEvent e) {
209            if (!isEnabled())
210                return;
211            String url = createInfoUrl(primitive);
212            OpenBrowser.displayUrl(url);
213        }
214
215        public void prepare(HistoryOsmPrimitive primitive) {
216            final User user = primitive.getUser();
217            putValue(NAME, "<html>" + tr("Show user {0}", user == null ? "?" :
218                    XmlWriter.encode(user.getName(), true) + " <font color=gray>(" + user.getId() + ")</font>") + "</html>");
219            this.primitive = primitive;
220        }
221    }
222
223    static class VersionTablePopupMenu extends JPopupMenu {
224
225        private ChangesetInfoAction changesetInfoAction;
226        private UserInfoAction userInfoAction;
227
228        /**
229         * Constructs a new {@code VersionTablePopupMenu}.
230         */
231        VersionTablePopupMenu() {
232            super();
233            build();
234        }
235
236        protected void build() {
237            changesetInfoAction = new ChangesetInfoAction();
238            add(changesetInfoAction);
239            userInfoAction = new UserInfoAction();
240            add(userInfoAction);
241        }
242
243        public void prepare(HistoryOsmPrimitive primitive) {
244            changesetInfoAction.prepare(primitive);
245            userInfoAction.prepare(primitive);
246            invalidate();
247        }
248    }
249
250    /**
251     * Renderer for history radio buttons in columns A and B.
252     */
253    public static class RadioButtonRenderer extends JRadioButton implements TableCellRenderer {
254
255        @Override
256        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
257                int row, int column) {
258            setSelected(value != null && (Boolean) value);
259            setHorizontalAlignment(SwingConstants.CENTER);
260            return this;
261        }
262    }
263
264    /**
265     * Editor for history radio buttons in columns A and B.
266     */
267    public static class RadioButtonEditor extends DefaultCellEditor implements ItemListener {
268
269        private final JRadioButton btn;
270
271        /**
272         * Constructs a new {@code RadioButtonEditor}.
273         */
274        public RadioButtonEditor() {
275            super(new JCheckBox());
276            btn = new JRadioButton();
277            btn.setHorizontalAlignment(SwingConstants.CENTER);
278        }
279
280        @Override
281        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
282            if (value == null)
283                return null;
284            boolean val = (Boolean) value;
285            btn.setSelected(val);
286            btn.addItemListener(this);
287            return btn;
288        }
289
290        @Override
291        public Object getCellEditorValue() {
292            btn.removeItemListener(this);
293            return btn.isSelected();
294        }
295
296        @Override
297        public void itemStateChanged(ItemEvent e) {
298            fireEditingStopped();
299        }
300    }
301
302    /**
303     * Renderer for history version labels, allowing to define horizontal alignment.
304     */
305    public static class AlignedRenderer extends JLabel implements TableCellRenderer {
306
307        /**
308         * Constructs a new {@code AlignedRenderer}.
309         * @param hAlignment Horizontal alignement. One of the following constants defined in SwingConstants:
310         *        LEFT, CENTER (the default for image-only labels), RIGHT, LEADING (the default for text-only labels) or TRAILING
311         */
312        public AlignedRenderer(int hAlignment) {
313            setHorizontalAlignment(hAlignment);
314        }
315
316        // for unit tests
317        private AlignedRenderer() {
318            this(SwingConstants.LEFT);
319        }
320
321        @Override
322        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
323                int row, int column) {
324            String v = "";
325            if (value != null) {
326                v = value.toString();
327            }
328            setText(v);
329            return this;
330        }
331    }
332
333    private static void adjustColumnWidth(JTable tbl, int col, int cellInset) {
334        int maxwidth = 0;
335
336        for (int row = 0; row < tbl.getRowCount(); row++) {
337            TableCellRenderer tcr = tbl.getCellRenderer(row, col);
338            Object val = tbl.getValueAt(row, col);
339            Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col);
340            maxwidth = Math.max(comp.getPreferredSize().width + cellInset, maxwidth);
341        }
342        TableCellRenderer tcr = tbl.getTableHeader().getDefaultRenderer();
343        Object val = tbl.getColumnModel().getColumn(col).getHeaderValue();
344        Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, -1, col);
345        maxwidth = Math.max(comp.getPreferredSize().width + Main.pref.getInteger("table.header-inset", 0), maxwidth);
346
347        int spacing = tbl.getIntercellSpacing().width;
348        tbl.getColumnModel().getColumn(col).setPreferredWidth(maxwidth + spacing);
349    }
350}