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.util.GuiHelper;
037import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
038import org.openstreetmap.josm.io.XmlWriter;
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 Observer {
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.addObserver(this);
058        build();
059        this.model = model;
060    }
061
062    protected void build() {
063        getTableHeader().setFont(getTableHeader().getFont().deriveFont(9f));
064        setRowSelectionAllowed(false);
065        setShowGrid(false);
066        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
067        GuiHelper.setBackgroundReadable(this, UIManager.getColor("Button.background"));
068        setIntercellSpacing(new Dimension(6, 0));
069        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
070        popupMenu = new VersionTablePopupMenu();
071        addMouseListener(new MouseListener());
072        addKeyListener(new KeyAdapter() {
073            @Override
074            public void keyReleased(KeyEvent e) {
075                // navigate history down/up using the corresponding arrow keys.
076                long ref = model.getReferencePointInTime().getVersion();
077                long cur = model.getCurrentPointInTime().getVersion();
078                if (e.getKeyCode() == KeyEvent.VK_DOWN) {
079                    History refNext = model.getHistory().from(ref);
080                    History curNext = model.getHistory().from(cur);
081                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
082                        model.setReferencePointInTime(refNext.sortAscending().get(1));
083                        model.setCurrentPointInTime(curNext.sortAscending().get(1));
084                    }
085                } else if (e.getKeyCode() == KeyEvent.VK_UP) {
086                    History refNext = model.getHistory().until(ref);
087                    History curNext = model.getHistory().until(cur);
088                    if (refNext.getNumVersions() > 1 && curNext.getNumVersions() > 1) {
089                        model.setReferencePointInTime(refNext.sortDescending().get(1));
090                        model.setCurrentPointInTime(curNext.sortDescending().get(1));
091                    }
092                }
093            }
094        });
095        getModel().addTableModelListener(new TableModelListener() {
096            @Override
097            public void tableChanged(TableModelEvent e) {
098                adjustColumnWidth(VersionTable.this, 0, 0);
099                adjustColumnWidth(VersionTable.this, 1, -8);
100                adjustColumnWidth(VersionTable.this, 2, -8);
101                adjustColumnWidth(VersionTable.this, 3, 0);
102                adjustColumnWidth(VersionTable.this, 4, 0);
103            }
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    protected HistoryBrowserModel.VersionTableModel getVersionTableModel() {
115        return (HistoryBrowserModel.VersionTableModel) getModel();
116    }
117
118    @Override
119    public void update(Observable o, Object arg) {
120        repaint();
121    }
122
123    final class MouseListener extends PopupMenuLauncher {
124        private MouseListener() {
125            super(popupMenu);
126        }
127
128        @Override
129        public void mousePressed(MouseEvent e) {
130            super.mousePressed(e);
131            if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
132                int row = rowAtPoint(e.getPoint());
133                int col = columnAtPoint(e.getPoint());
134                if (row >= 0 && (col == VersionTableColumnModel.COL_DATE || col == VersionTableColumnModel.COL_USER)) {
135                    model.getVersionTableModel().setCurrentPointInTime(row);
136                    model.getVersionTableModel().setReferencePointInTime(Math.max(0, row - 1));
137                }
138            }
139        }
140
141        @Override
142        protected int checkTableSelection(JTable table, Point p) {
143            HistoryBrowserModel.VersionTableModel tableModel = getVersionTableModel();
144            int row = rowAtPoint(p);
145            if (row > -1 && !tableModel.isLatest(row)) {
146                popupMenu.prepare(tableModel.getPrimitive(row));
147            }
148            return row;
149        }
150    }
151
152    static class ChangesetInfoAction extends AbstractInfoAction {
153        private transient HistoryOsmPrimitive primitive;
154
155        /**
156         * Constructs a new {@code ChangesetInfoAction}.
157         */
158        ChangesetInfoAction() {
159            super(true);
160            putValue(NAME, tr("Changeset info"));
161            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the changeset"));
162            putValue(SMALL_ICON, ImageProvider.get("data/changeset"));
163        }
164
165        @Override
166        protected String createInfoUrl(Object infoObject) {
167            if (infoObject instanceof HistoryOsmPrimitive) {
168                HistoryOsmPrimitive prim = (HistoryOsmPrimitive) infoObject;
169                return Main.getBaseBrowseUrl() + "/changeset/" + prim.getChangesetId();
170            } else {
171                return null;
172            }
173        }
174
175        @Override
176        public void actionPerformed(ActionEvent e) {
177            if (!isEnabled())
178                return;
179            String url = createInfoUrl(primitive);
180            OpenBrowser.displayUrl(url);
181        }
182
183        public void prepare(HistoryOsmPrimitive primitive) {
184            putValue(NAME, tr("Show changeset {0}", primitive.getChangesetId()));
185            this.primitive = primitive;
186        }
187    }
188
189    static class UserInfoAction extends AbstractInfoAction {
190        private transient HistoryOsmPrimitive primitive;
191
192        /**
193         * Constructs a new {@code UserInfoAction}.
194         */
195        UserInfoAction() {
196            super(true);
197            putValue(NAME, tr("User info"));
198            putValue(SHORT_DESCRIPTION, tr("Launch browser with information about the user"));
199            putValue(SMALL_ICON, ImageProvider.get("data/user"));
200        }
201
202        @Override
203        protected String createInfoUrl(Object infoObject) {
204            if (infoObject instanceof HistoryOsmPrimitive) {
205                HistoryOsmPrimitive hp = (HistoryOsmPrimitive) infoObject;
206                return hp.getUser() == null ? null : Main.getBaseUserUrl() + '/' + hp.getUser().getName();
207            } else {
208                return null;
209            }
210        }
211
212        @Override
213        public void actionPerformed(ActionEvent e) {
214            if (!isEnabled())
215                return;
216            String url = createInfoUrl(primitive);
217            OpenBrowser.displayUrl(url);
218        }
219
220        public void prepare(HistoryOsmPrimitive primitive) {
221            final User user = primitive.getUser();
222            putValue(NAME, "<html>" + tr("Show user {0}", user == null ? "?" :
223                    XmlWriter.encode(user.getName(), true) + " <font color=gray>(" + user.getId() + ")</font>") + "</html>");
224            this.primitive = primitive;
225        }
226    }
227
228    static class VersionTablePopupMenu extends JPopupMenu {
229
230        private ChangesetInfoAction changesetInfoAction;
231        private UserInfoAction userInfoAction;
232
233        /**
234         * Constructs a new {@code VersionTablePopupMenu}.
235         */
236        VersionTablePopupMenu() {
237            super();
238            build();
239        }
240
241        protected void build() {
242            changesetInfoAction = new ChangesetInfoAction();
243            add(changesetInfoAction);
244            userInfoAction = new UserInfoAction();
245            add(userInfoAction);
246        }
247
248        public void prepare(HistoryOsmPrimitive primitive) {
249            changesetInfoAction.prepare(primitive);
250            userInfoAction.prepare(primitive);
251            invalidate();
252        }
253    }
254
255    /**
256     * Renderer for history radio buttons in columns A and B.
257     */
258    public static class RadioButtonRenderer extends JRadioButton implements TableCellRenderer {
259
260        @Override
261        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
262                int row, int column) {
263            setSelected(value != null && (Boolean) value);
264            setHorizontalAlignment(SwingConstants.CENTER);
265            return this;
266        }
267    }
268
269    /**
270     * Editor for history radio buttons in columns A and B.
271     */
272    public static class RadioButtonEditor extends DefaultCellEditor implements ItemListener {
273
274        private final JRadioButton btn;
275
276        /**
277         * Constructs a new {@code RadioButtonEditor}.
278         */
279        public RadioButtonEditor() {
280            super(new JCheckBox());
281            btn = new JRadioButton();
282            btn.setHorizontalAlignment(SwingConstants.CENTER);
283        }
284
285        @Override
286        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
287            if (value == null)
288                return null;
289            boolean val = (Boolean) value;
290            btn.setSelected(val);
291            btn.addItemListener(this);
292            return btn;
293        }
294
295        @Override
296        public Object getCellEditorValue() {
297            btn.removeItemListener(this);
298            return btn.isSelected();
299        }
300
301        @Override
302        public void itemStateChanged(ItemEvent e) {
303            fireEditingStopped();
304        }
305    }
306
307    /**
308     * Renderer for history version labels, allowing to define horizontal alignment.
309     */
310    public static class AlignedRenderer extends JLabel implements TableCellRenderer {
311
312        /**
313         * Constructs a new {@code AlignedRenderer}.
314         * @param hAlignment Horizontal alignement. One of the following constants defined in SwingConstants:
315         *        LEFT, CENTER (the default for image-only labels), RIGHT, LEADING (the default for text-only labels) or TRAILING
316         */
317        public AlignedRenderer(int hAlignment) {
318            setHorizontalAlignment(hAlignment);
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}