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