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}