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.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 ChangeListener { 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.addChangeListener(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 stateChanged(ChangeEvent e) { 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 // for unit tests 322 private AlignedRenderer() { 323 this(SwingConstants.LEFT); 324 } 325 326 @Override 327 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, 328 int row, int column) { 329 String v = ""; 330 if (value != null) { 331 v = value.toString(); 332 } 333 setText(v); 334 return this; 335 } 336 } 337 338 private static void adjustColumnWidth(JTable tbl, int col, int cellInset) { 339 int maxwidth = 0; 340 341 for (int row = 0; row < tbl.getRowCount(); row++) { 342 TableCellRenderer tcr = tbl.getCellRenderer(row, col); 343 Object val = tbl.getValueAt(row, col); 344 Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col); 345 maxwidth = Math.max(comp.getPreferredSize().width + cellInset, maxwidth); 346 } 347 TableCellRenderer tcr = tbl.getTableHeader().getDefaultRenderer(); 348 Object val = tbl.getColumnModel().getColumn(col).getHeaderValue(); 349 Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, -1, col); 350 maxwidth = Math.max(comp.getPreferredSize().width + Main.pref.getInteger("table.header-inset", 0), maxwidth); 351 352 int spacing = tbl.getIntercellSpacing().width; 353 tbl.getColumnModel().getColumn(col).setPreferredWidth(maxwidth + spacing); 354 } 355}