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.spi.preferences.Config;
038import org.openstreetmap.josm.tools.ImageProvider;
039import org.openstreetmap.josm.tools.OpenBrowser;
040
041/**
042 * VersionTable shows a list of version in a {@link org.openstreetmap.josm.data.osm.history.History}
043 * of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}.
044 * @since 1709
045 */
046public class VersionTable extends JTable implements ChangeListener {
047    private VersionTablePopupMenu popupMenu;
048    private final transient HistoryBrowserModel model;
049
050    /**
051     * Constructs a new {@code VersionTable}.
052     * @param model model used by the history browser
053     */
054    public VersionTable(HistoryBrowserModel model) {
055        super(model.getVersionTableModel(), new VersionTableColumnModel());
056        model.addChangeListener(this);
057        build();
058        this.model = model;
059    }
060
061    /**
062     * Builds the table.
063     */
064    protected void build() {
065        getTableHeader().setFont(getTableHeader().getFont().deriveFont(9f));
066        setRowSelectionAllowed(false);
067        setShowGrid(false);
068        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
069        GuiHelper.setBackgroundReadable(this, UIManager.getColor("Button.background"));
070        setIntercellSpacing(new Dimension(6, 0));
071        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
072        popupMenu = new VersionTablePopupMenu();
073        addMouseListener(new MouseListener());
074        addKeyListener(new KeyAdapter() {
075            @Override
076            public void keyReleased(KeyEvent e) {
077                // navigate history down/up using the corresponding arrow keys.
078                long ref = model.getReferencePointInTime().getVersion();
079                long cur = model.getCurrentPointInTime().getVersion();
080                if (e.getKeyCode() == KeyEvent.VK_DOWN) {
081                    History refNext = model.getHistory().from(ref);
082                    History curNext = model.getHistory().from(cur);
083                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
084                        model.setReferencePointInTime(refNext.sortAscending().get(1));
085                        model.setCurrentPointInTime(curNext.sortAscending().get(1));
086                    }
087                } else if (e.getKeyCode() == KeyEvent.VK_UP) {
088                    History refNext = model.getHistory().until(ref);
089                    History curNext = model.getHistory().until(cur);
090                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
091                        model.setReferencePointInTime(refNext.sortDescending().get(1));
092                        model.setCurrentPointInTime(curNext.sortDescending().get(1));
093                    }
094                }
095            }
096        });
097        getModel().addTableModelListener(e -> {
098            adjustColumnWidth(this, 0, 0);
099            adjustColumnWidth(this, 1, -8);
100            adjustColumnWidth(this, 2, -8);
101            adjustColumnWidth(this, 3, 0);
102            adjustColumnWidth(this, 4, 0);
103            adjustColumnWidth(this, 5, 0);
104        });
105    }
106
107    // some kind of hack to prevent the table from scrolling to the
108    // right when clicking on the cells
109    @Override
110    public void scrollRectToVisible(Rectangle aRect) {
111        super.scrollRectToVisible(new Rectangle(0, aRect.y, aRect.width, aRect.height));
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.setCurrentPointInTime(row);
132                    model.setReferencePointInTime(Math.max(0, row - 1));
133                }
134            }
135        }
136
137        @Override
138        protected int checkTableSelection(JTable table, Point p) {
139            int row = rowAtPoint(p);
140            if (row > -1 && !model.isLatest(row)) {
141                popupMenu.prepare(model.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            new ImageProvider("data/changeset").getResource().attachImageIcon(this, true);
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            new ImageProvider("data/user").getResource().attachImageIcon(this, true);
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        AlignedRenderer() {
317            this(SwingConstants.LEFT);
318        }
319
320        @Override
321        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
322                int row, int column) {
323            String v = "";
324            if (value != null) {
325                v = value.toString();
326            }
327            setText(v);
328            return this;
329        }
330    }
331
332    private static void adjustColumnWidth(JTable tbl, int col, int cellInset) {
333        int maxwidth = 0;
334
335        for (int row = 0; row < tbl.getRowCount(); row++) {
336            TableCellRenderer tcr = tbl.getCellRenderer(row, col);
337            Object val = tbl.getValueAt(row, col);
338            Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col);
339            maxwidth = Math.max(comp.getPreferredSize().width + cellInset, maxwidth);
340        }
341        TableCellRenderer tcr = tbl.getTableHeader().getDefaultRenderer();
342        Object val = tbl.getColumnModel().getColumn(col).getHeaderValue();
343        Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, -1, col);
344        maxwidth = Math.max(comp.getPreferredSize().width + Config.getPref().getInt("table.header-inset", 0), maxwidth);
345
346        int spacing = tbl.getIntercellSpacing().width;
347        tbl.getColumnModel().getColumn(col).setPreferredWidth(maxwidth + spacing);
348    }
349}