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