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.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.Insets; 009import java.awt.Point; 010import java.awt.event.ActionEvent; 011import java.awt.event.MouseAdapter; 012import java.awt.event.MouseEvent; 013 014import javax.swing.AbstractAction; 015import javax.swing.JPanel; 016import javax.swing.JPopupMenu; 017import javax.swing.JScrollPane; 018import javax.swing.JTable; 019import javax.swing.ListSelectionModel; 020import javax.swing.event.TableModelEvent; 021import javax.swing.event.TableModelListener; 022import javax.swing.table.TableModel; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.actions.AutoScaleAction; 026import org.openstreetmap.josm.data.osm.OsmPrimitive; 027import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 028import org.openstreetmap.josm.data.osm.PrimitiveId; 029import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 030import org.openstreetmap.josm.data.osm.history.History; 031import org.openstreetmap.josm.data.osm.history.HistoryDataSet; 032import org.openstreetmap.josm.gui.layer.OsmDataLayer; 033import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer; 034import org.openstreetmap.josm.gui.util.GuiHelper; 035import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 036import org.openstreetmap.josm.tools.ImageProvider; 037 038/** 039 * NodeListViewer is a UI component which displays the node list of two 040 * version of a {@link OsmPrimitive} in a {@link History}. 041 * 042 * <ul> 043 * <li>on the left, it displays the node list for the version at {@link PointInTimeType#REFERENCE_POINT_IN_TIME}</li> 044 * <li>on the right, it displays the node list for the version at {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li> 045 * </ul> 046 * 047 */ 048public class NodeListViewer extends JPanel { 049 050 private transient HistoryBrowserModel model; 051 private VersionInfoPanel referenceInfoPanel; 052 private VersionInfoPanel currentInfoPanel; 053 private transient AdjustmentSynchronizer adjustmentSynchronizer; 054 private transient SelectionSynchronizer selectionSynchronizer; 055 private NodeListPopupMenu popupMenu; 056 057 /** 058 * Constructs a new {@code NodeListViewer}. 059 * @param model history browser model 060 */ 061 public NodeListViewer(HistoryBrowserModel model) { 062 setModel(model); 063 build(); 064 } 065 066 protected JScrollPane embeddInScrollPane(JTable table) { 067 JScrollPane pane = new JScrollPane(table); 068 adjustmentSynchronizer.participateInSynchronizedScrolling(pane.getVerticalScrollBar()); 069 return pane; 070 } 071 072 protected JTable buildReferenceNodeListTable() { 073 final DiffTableModel tableModel = model.getNodeListTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME); 074 final NodeListTableColumnModel columnModel = new NodeListTableColumnModel(); 075 final JTable table = new JTable(tableModel, columnModel); 076 tableModel.addTableModelListener(newReversedChangeListener(table, columnModel)); 077 table.setName("table.referencenodelisttable"); 078 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 079 selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel()); 080 table.addMouseListener(new InternalPopupMenuLauncher()); 081 table.addMouseListener(new DoubleClickAdapter(table)); 082 return table; 083 } 084 085 protected JTable buildCurrentNodeListTable() { 086 final DiffTableModel tableModel = model.getNodeListTableModel(PointInTimeType.CURRENT_POINT_IN_TIME); 087 final NodeListTableColumnModel columnModel = new NodeListTableColumnModel(); 088 final JTable table = new JTable(tableModel, columnModel); 089 tableModel.addTableModelListener(newReversedChangeListener(table, columnModel)); 090 table.setName("table.currentnodelisttable"); 091 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 092 selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel()); 093 table.addMouseListener(new InternalPopupMenuLauncher()); 094 table.addMouseListener(new DoubleClickAdapter(table)); 095 return table; 096 } 097 098 protected TableModelListener newReversedChangeListener(final JTable table, final NodeListTableColumnModel columnModel) { 099 return new TableModelListener() { 100 private Boolean reversed; 101 private final String nonReversedText = tr("Nodes") + (table.getFont().canDisplay('\u25bc') ? " \u25bc" : " (1-n)"); 102 private final String reversedText = tr("Nodes") + (table.getFont().canDisplay('\u25b2') ? " \u25b2" : " (n-1)"); 103 104 @Override 105 public void tableChanged(TableModelEvent e) { 106 if (e.getSource() instanceof DiffTableModel) { 107 final DiffTableModel mod = (DiffTableModel) e.getSource(); 108 if (reversed == null || reversed != mod.isReversed()) { 109 reversed = mod.isReversed(); 110 columnModel.getColumn(0).setHeaderValue(reversed ? reversedText : nonReversedText); 111 table.getTableHeader().setToolTipText( 112 reversed ? tr("The nodes of this way are in reverse order") : null); 113 table.getTableHeader().repaint(); 114 } 115 } 116 } 117 }; 118 } 119 120 protected void build() { 121 setLayout(new GridBagLayout()); 122 GridBagConstraints gc = new GridBagConstraints(); 123 124 // --------------------------- 125 gc.gridx = 0; 126 gc.gridy = 0; 127 gc.gridwidth = 1; 128 gc.gridheight = 1; 129 gc.weightx = 0.5; 130 gc.weighty = 0.0; 131 gc.insets = new Insets(5, 5, 5, 0); 132 gc.fill = GridBagConstraints.HORIZONTAL; 133 gc.anchor = GridBagConstraints.FIRST_LINE_START; 134 referenceInfoPanel = new VersionInfoPanel(model, PointInTimeType.REFERENCE_POINT_IN_TIME); 135 add(referenceInfoPanel, gc); 136 137 gc.gridx = 1; 138 gc.gridy = 0; 139 gc.gridwidth = 1; 140 gc.gridheight = 1; 141 gc.fill = GridBagConstraints.HORIZONTAL; 142 gc.weightx = 0.5; 143 gc.weighty = 0.0; 144 gc.anchor = GridBagConstraints.FIRST_LINE_START; 145 currentInfoPanel = new VersionInfoPanel(model, PointInTimeType.CURRENT_POINT_IN_TIME); 146 add(currentInfoPanel, gc); 147 148 adjustmentSynchronizer = new AdjustmentSynchronizer(); 149 selectionSynchronizer = new SelectionSynchronizer(); 150 151 popupMenu = new NodeListPopupMenu(); 152 153 // --------------------------- 154 gc.gridx = 0; 155 gc.gridy = 1; 156 gc.gridwidth = 1; 157 gc.gridheight = 1; 158 gc.weightx = 0.5; 159 gc.weighty = 1.0; 160 gc.fill = GridBagConstraints.BOTH; 161 gc.anchor = GridBagConstraints.NORTHWEST; 162 add(embeddInScrollPane(buildReferenceNodeListTable()), gc); 163 164 gc.gridx = 1; 165 gc.gridy = 1; 166 gc.gridwidth = 1; 167 gc.gridheight = 1; 168 gc.weightx = 0.5; 169 gc.weighty = 1.0; 170 gc.fill = GridBagConstraints.BOTH; 171 gc.anchor = GridBagConstraints.NORTHWEST; 172 add(embeddInScrollPane(buildCurrentNodeListTable()), gc); 173 } 174 175 protected void unregisterAsChangeListener(HistoryBrowserModel model) { 176 if (currentInfoPanel != null) { 177 model.removeChangeListener(currentInfoPanel); 178 } 179 if (referenceInfoPanel != null) { 180 model.removeChangeListener(referenceInfoPanel); 181 } 182 } 183 184 protected void registerAsChangeListener(HistoryBrowserModel model) { 185 if (currentInfoPanel != null) { 186 model.addChangeListener(currentInfoPanel); 187 } 188 if (referenceInfoPanel != null) { 189 model.addChangeListener(referenceInfoPanel); 190 } 191 } 192 193 /** 194 * Sets the history browser model. 195 * @param model the history browser model 196 */ 197 public void setModel(HistoryBrowserModel model) { 198 if (this.model != null) { 199 unregisterAsChangeListener(model); 200 } 201 this.model = model; 202 if (this.model != null) { 203 registerAsChangeListener(model); 204 } 205 } 206 207 static class NodeListPopupMenu extends JPopupMenu { 208 private final ZoomToNodeAction zoomToNodeAction; 209 private final ShowHistoryAction showHistoryAction; 210 211 NodeListPopupMenu() { 212 zoomToNodeAction = new ZoomToNodeAction(); 213 add(zoomToNodeAction); 214 showHistoryAction = new ShowHistoryAction(); 215 add(showHistoryAction); 216 } 217 218 public void prepare(PrimitiveId pid) { 219 zoomToNodeAction.setPrimitiveId(pid); 220 zoomToNodeAction.updateEnabledState(); 221 222 showHistoryAction.setPrimitiveId(pid); 223 showHistoryAction.updateEnabledState(); 224 } 225 } 226 227 static class ZoomToNodeAction extends AbstractAction { 228 private transient PrimitiveId primitiveId; 229 230 /** 231 * Constructs a new {@code ZoomToNodeAction}. 232 */ 233 ZoomToNodeAction() { 234 putValue(NAME, tr("Zoom to node")); 235 putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer")); 236 putValue(SMALL_ICON, ImageProvider.get("dialogs", "zoomin")); 237 } 238 239 @Override 240 public void actionPerformed(ActionEvent e) { 241 if (!isEnabled()) 242 return; 243 OsmPrimitive p = getPrimitiveToZoom(); 244 if (p != null) { 245 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 246 if (editLayer != null) { 247 editLayer.data.setSelected(p.getPrimitiveId()); 248 AutoScaleAction.autoScale("selection"); 249 } 250 } 251 } 252 253 public void setPrimitiveId(PrimitiveId pid) { 254 this.primitiveId = pid; 255 updateEnabledState(); 256 } 257 258 protected OsmPrimitive getPrimitiveToZoom() { 259 if (primitiveId == null) 260 return null; 261 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 262 if (editLayer == null) 263 return null; 264 return editLayer.data.getPrimitiveById(primitiveId); 265 } 266 267 public void updateEnabledState() { 268 setEnabled(Main.getLayerManager().getEditLayer() != null && getPrimitiveToZoom() != null); 269 } 270 } 271 272 static class ShowHistoryAction extends AbstractAction { 273 private transient PrimitiveId primitiveId; 274 275 /** 276 * Constructs a new {@code ShowHistoryAction}. 277 */ 278 ShowHistoryAction() { 279 putValue(NAME, tr("Show history")); 280 putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node")); 281 putValue(SMALL_ICON, ImageProvider.get("dialogs", "history")); 282 } 283 284 @Override 285 public void actionPerformed(ActionEvent e) { 286 if (isEnabled()) { 287 run(); 288 } 289 } 290 291 public void setPrimitiveId(PrimitiveId pid) { 292 this.primitiveId = pid; 293 updateEnabledState(); 294 } 295 296 public void run() { 297 if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) { 298 Main.worker.submit(new HistoryLoadTask().add(primitiveId)); 299 } 300 Runnable r = new Runnable() { 301 @Override 302 public void run() { 303 final History h = HistoryDataSet.getInstance().getHistory(primitiveId); 304 if (h == null) 305 return; 306 GuiHelper.runInEDT(new Runnable() { 307 @Override public void run() { 308 HistoryBrowserDialogManager.getInstance().show(h); 309 } 310 }); 311 } 312 }; 313 Main.worker.submit(r); 314 } 315 316 public void updateEnabledState() { 317 setEnabled(primitiveId != null && !primitiveId.isNew()); 318 } 319 } 320 321 private static PrimitiveId primitiveIdAtRow(TableModel model, int row) { 322 DiffTableModel castedModel = (DiffTableModel) model; 323 Long id = (Long) castedModel.getValueAt(row, 0).value; 324 return id == null ? null : new SimplePrimitiveId(id, OsmPrimitiveType.NODE); 325 } 326 327 class InternalPopupMenuLauncher extends PopupMenuLauncher { 328 InternalPopupMenuLauncher() { 329 super(popupMenu); 330 } 331 332 @Override 333 protected int checkTableSelection(JTable table, Point p) { 334 int row = super.checkTableSelection(table, p); 335 popupMenu.prepare(primitiveIdAtRow(table.getModel(), row)); 336 return row; 337 } 338 } 339 340 static class DoubleClickAdapter extends MouseAdapter { 341 private final JTable table; 342 private final ShowHistoryAction showHistoryAction; 343 344 DoubleClickAdapter(JTable table) { 345 this.table = table; 346 showHistoryAction = new ShowHistoryAction(); 347 } 348 349 @Override 350 public void mouseClicked(MouseEvent e) { 351 if (e.getClickCount() < 2) 352 return; 353 int row = table.rowAtPoint(e.getPoint()); 354 if (row <= 0) 355 return; 356 PrimitiveId pid = primitiveIdAtRow(table.getModel(), row); 357 if (pid == null || pid.isNew()) 358 return; 359 showHistoryAction.setPrimitiveId(pid); 360 showHistoryAction.run(); 361 } 362 } 363}