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