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