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