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 transient 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    private transient 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 (Main.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) {
139                            Main.map.mapView.repaint();
140                        }
141                    }
142                });
143            }
144        }
145    };
146
147    private void initHighlighting() {
148        highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
149        if (!highlightEnabled) return;
150        getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener);
151        if (Main.isDisplayingMapView()) {
152            HighlightHelper.clearAllHighlighted();
153            Main.map.mapView.repaint();
154        }
155    }
156
157    /**
158     * Action to be run when the user navigates to the next cell in the table, for instance by
159     * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul>
160     * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row
161     * when the user leaves the last cell in the table</li></ul>
162     */
163    class SelectNextColumnCellAction extends AbstractAction {
164        @Override
165        public void actionPerformed(ActionEvent e) {
166            run();
167        }
168
169        public void run() {
170            int col = getSelectedColumn();
171            int row = getSelectedRow();
172            if (getCellEditor() != null) {
173                getCellEditor().stopCellEditing();
174            }
175
176            if (col == 0 && row < getRowCount() - 1) {
177                row++;
178            } else if (row < getRowCount() - 1) {
179                col = 0;
180                row++;
181            } else {
182                // go to next component, no more rows in this table
183                KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
184                manager.focusNextComponent();
185                return;
186            }
187            changeSelection(row, col, false, false);
188        }
189    }
190
191    /**
192     * Action to be run when the user navigates to the previous cell in the table, for instance by
193     * pressing Shift-TAB
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        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        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        /**
273         * Constructs a new {@code ZoomToGapAction}.
274         */
275        ZoomToGapAction() {
276            putValue(NAME, tr("Zoom to Gap"));
277            putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence"));
278            updateEnabledState();
279        }
280
281        private WayConnectionType getConnectionType() {
282            return getMemberTableModel().getWayConnection(getSelectedRows()[0]);
283        }
284
285        private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(
286                WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD);
287
288        private boolean hasGap() {
289            WayConnectionType connectionType = getConnectionType();
290            return connectionTypesOfInterest.contains(connectionType.direction)
291                    && !(connectionType.linkNext && connectionType.linkPrev);
292        }
293
294        @Override
295        public void actionPerformed(ActionEvent e) {
296            WayConnectionType connectionType = getConnectionType();
297            Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]);
298            if (!connectionType.linkPrev) {
299                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
300                        ? way.firstNode() : way.lastNode());
301                AutoScaleAction.autoScale("selection");
302            } else if (!connectionType.linkNext) {
303                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
304                        ? way.lastNode() : way.firstNode());
305                AutoScaleAction.autoScale("selection");
306            }
307        }
308
309        private void updateEnabledState() {
310            setEnabled(Main.main != null
311                    && Main.main.getEditLayer() == getLayer()
312                    && getSelectedRowCount() == 1
313                    && hasGap());
314        }
315
316        @Override
317        public void valueChanged(ListSelectionEvent e) {
318            updateEnabledState();
319        }
320
321        @Override
322        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
323            updateEnabledState();
324        }
325
326        @Override
327        public void layerAdded(Layer newLayer) {
328            updateEnabledState();
329        }
330
331        @Override
332        public void layerRemoved(Layer oldLayer) {
333            updateEnabledState();
334        }
335    }
336
337    protected MemberTableModel getMemberTableModel() {
338        return (MemberTableModel) getModel();
339    }
340}