001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.relation; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Container; 007import java.awt.Dimension; 008import java.awt.KeyboardFocusManager; 009import java.awt.event.ActionEvent; 010import java.awt.event.KeyEvent; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.List; 015 016import javax.swing.AbstractAction; 017import javax.swing.JComponent; 018import javax.swing.JPopupMenu; 019import javax.swing.JTable; 020import javax.swing.JViewport; 021import javax.swing.KeyStroke; 022import javax.swing.ListSelectionModel; 023import javax.swing.SwingUtilities; 024import javax.swing.event.ListSelectionEvent; 025import javax.swing.event.ListSelectionListener; 026 027import org.openstreetmap.josm.Main; 028import org.openstreetmap.josm.actions.AutoScaleAction; 029import org.openstreetmap.josm.actions.ZoomToAction; 030import org.openstreetmap.josm.data.osm.OsmPrimitive; 031import org.openstreetmap.josm.data.osm.RelationMember; 032import org.openstreetmap.josm.data.osm.Way; 033import org.openstreetmap.josm.gui.MapView; 034import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 035import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType; 036import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction; 037import org.openstreetmap.josm.gui.layer.Layer; 038import org.openstreetmap.josm.gui.layer.OsmDataLayer; 039import org.openstreetmap.josm.gui.util.HighlightHelper; 040import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable; 041 042public class MemberTable extends OsmPrimitivesTable implements IMemberModelListener { 043 044 /** the additional actions in popup menu */ 045 private ZoomToGapAction zoomToGap; 046 private HighlightHelper highlightHelper = new HighlightHelper(); 047 private boolean highlightEnabled; 048 049 /** 050 * constructor for relation member table 051 * 052 * @param layer the data layer of the relation 053 * @param model the table model 054 */ 055 public MemberTable(OsmDataLayer layer, MemberTableModel model) { 056 super(model, new MemberTableColumnModel(layer.data), model.getSelectionModel()); 057 setLayer(layer); 058 model.addMemberModelListener(this); 059 init(); 060 } 061 062 /** 063 * initialize the table 064 */ 065 protected void init() { 066 MemberRoleCellEditor ce = (MemberRoleCellEditor)getColumnModel().getColumn(0).getCellEditor(); 067 setRowHeight(ce.getEditor().getPreferredSize().height); 068 setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); 069 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 070 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); 071 072 // make ENTER behave like TAB 073 // 074 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( 075 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell"); 076 077 initHighlighting(); 078 079 // install custom navigation actions 080 // 081 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction()); 082 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction()); 083 } 084 085 @Override 086 protected ZoomToAction buildZoomToAction() { 087 return new ZoomToAction(this); 088 } 089 090 @Override 091 protected JPopupMenu buildPopupMenu() { 092 JPopupMenu menu = super.buildPopupMenu(); 093 zoomToGap = new ZoomToGapAction(); 094 MapView.addLayerChangeListener(zoomToGap); 095 getSelectionModel().addListSelectionListener(zoomToGap); 096 menu.add(zoomToGap); 097 menu.addSeparator(); 098 menu.add(new SelectPreviousGapAction()); 099 menu.add(new SelectNextGapAction()); 100 return menu; 101 } 102 103 @Override 104 public Dimension getPreferredSize(){ 105 Container c = getParent(); 106 while(c != null && ! (c instanceof JViewport)) { 107 c = c.getParent(); 108 } 109 if (c != null) { 110 Dimension d = super.getPreferredSize(); 111 d.width = c.getSize().width; 112 return d; 113 } 114 return super.getPreferredSize(); 115 } 116 117 @Override 118 public void makeMemberVisible(int index) { 119 scrollRectToVisible(getCellRect(index, 0, true)); 120 } 121 122 ListSelectionListener highlighterListener = new ListSelectionListener() { 123 @Override 124 public void valueChanged(ListSelectionEvent lse) { 125 if (Main.isDisplayingMapView()) { 126 Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers(); 127 final List<OsmPrimitive> toHighlight = new ArrayList<OsmPrimitive>(); 128 for (RelationMember r: sel) { 129 if (r.getMember().isUsable()) { 130 toHighlight.add(r.getMember()); 131 } 132 } 133 SwingUtilities.invokeLater(new Runnable() { 134 @Override 135 public void run() { 136 if (highlightHelper.highlightOnly(toHighlight)) { 137 Main.map.mapView.repaint(); 138 } 139 } 140 }); 141 } 142 }}; 143 144 private void initHighlighting() { 145 highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true); 146 if (!highlightEnabled) return; 147 getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener); 148 if (Main.isDisplayingMapView()) { 149 HighlightHelper.clearAllHighlighted(); 150 Main.map.mapView.repaint(); 151 } 152 } 153 154 /** 155 * Action to be run when the user navigates to the next cell in the table, for instance by 156 * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul> 157 * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row 158 * when the user leaves the last cell in the table</li> <ul> 159 * 160 * 161 */ 162 class SelectNextColumnCellAction extends AbstractAction { 163 @Override 164 public void actionPerformed(ActionEvent e) { 165 run(); 166 } 167 168 public void run() { 169 int col = getSelectedColumn(); 170 int row = getSelectedRow(); 171 if (getCellEditor() != null) { 172 getCellEditor().stopCellEditing(); 173 } 174 175 if (col == 0 && row < getRowCount() - 1) { 176 row++; 177 } else if (row < getRowCount() - 1) { 178 col = 0; 179 row++; 180 } else { 181 // go to next component, no more rows in this table 182 KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); 183 manager.focusNextComponent(); 184 return; 185 } 186 changeSelection(row, col, false, false); 187 } 188 } 189 190 /** 191 * Action to be run when the user navigates to the previous cell in the table, for instance by 192 * pressing Shift-TAB 193 * 194 */ 195 private class SelectPreviousColumnCellAction extends AbstractAction { 196 197 @Override 198 public void actionPerformed(ActionEvent e) { 199 int col = getSelectedColumn(); 200 int row = getSelectedRow(); 201 if (getCellEditor() != null) { 202 getCellEditor().stopCellEditing(); 203 } 204 205 if (col <= 0 && row <= 0) { 206 // change nothing 207 } else if (row > 0) { 208 col = 0; 209 row--; 210 } 211 changeSelection(row, col, false, false); 212 } 213 } 214 215 @Override 216 public void unlinkAsListener() { 217 super.unlinkAsListener(); 218 MapView.removeLayerChangeListener(zoomToGap); 219 } 220 221 public void stopHighlighting() { 222 if (highlighterListener == null) return; 223 if (!highlightEnabled) return; 224 getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener); 225 highlighterListener = null; 226 if (Main.isDisplayingMapView()) { 227 HighlightHelper.clearAllHighlighted(); 228 Main.map.mapView.repaint(); 229 } 230 } 231 232 private class SelectPreviousGapAction extends AbstractAction { 233 234 public SelectPreviousGapAction() { 235 putValue(NAME, tr("Select previous Gap")); 236 putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap")); 237 } 238 239 @Override 240 public void actionPerformed(ActionEvent e) { 241 int i = getSelectedRow() - 1; 242 while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) { 243 i--; 244 } 245 if (i >= 0) { 246 getSelectionModel().setSelectionInterval(i, i); 247 } 248 } 249 } 250 251 private class SelectNextGapAction extends AbstractAction { 252 253 public SelectNextGapAction() { 254 putValue(NAME, tr("Select next Gap")); 255 putValue(SHORT_DESCRIPTION, tr("Select the next relation member which gives rise to a gap")); 256 } 257 258 @Override 259 public void actionPerformed(ActionEvent e) { 260 int i = getSelectedRow() + 1; 261 while (i < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) { 262 i++; 263 } 264 if (i < getRowCount()) { 265 getSelectionModel().setSelectionInterval(i, i); 266 } 267 } 268 } 269 270 private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ListSelectionListener { 271 272 public ZoomToGapAction() { 273 putValue(NAME, tr("Zoom to Gap")); 274 putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence")); 275 updateEnabledState(); 276 } 277 278 private WayConnectionType getConnectionType() { 279 return getMemberTableModel().getWayConnection(getSelectedRows()[0]); 280 } 281 282 private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD); 283 284 private boolean hasGap() { 285 WayConnectionType connectionType = getConnectionType(); 286 return connectionTypesOfInterest.contains(connectionType.direction) 287 && !(connectionType.linkNext && connectionType.linkPrev); 288 } 289 290 @Override 291 public void actionPerformed(ActionEvent e) { 292 WayConnectionType connectionType = getConnectionType(); 293 Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]); 294 if (!connectionType.linkPrev) { 295 getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction) 296 ? way.firstNode() : way.lastNode()); 297 AutoScaleAction.autoScale("selection"); 298 } else if (!connectionType.linkNext) { 299 getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction) 300 ? way.lastNode() : way.firstNode()); 301 AutoScaleAction.autoScale("selection"); 302 } 303 } 304 305 private void updateEnabledState() { 306 setEnabled(Main.main != null 307 && Main.main.getEditLayer() == getLayer() 308 && getSelectedRowCount() == 1 309 && hasGap()); 310 } 311 312 @Override 313 public void valueChanged(ListSelectionEvent e) { 314 updateEnabledState(); 315 } 316 317 @Override 318 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 319 updateEnabledState(); 320 } 321 322 @Override 323 public void layerAdded(Layer newLayer) { 324 updateEnabledState(); 325 } 326 327 @Override 328 public void layerRemoved(Layer oldLayer) { 329 updateEnabledState(); 330 } 331 } 332 333 protected MemberTableModel getMemberTableModel() { 334 return (MemberTableModel) getModel(); 335 } 336}