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}