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