001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.tags;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Font;
008import java.awt.event.FocusAdapter;
009import java.awt.event.FocusEvent;
010import java.awt.event.ItemEvent;
011import java.awt.event.KeyEvent;
012import java.util.concurrent.CopyOnWriteArrayList;
013
014import javax.swing.AbstractCellEditor;
015import javax.swing.DefaultComboBoxModel;
016import javax.swing.JLabel;
017import javax.swing.JList;
018import javax.swing.JTable;
019import javax.swing.ListCellRenderer;
020import javax.swing.UIManager;
021import javax.swing.table.TableCellEditor;
022
023import org.openstreetmap.josm.gui.widgets.JosmComboBox;
024import org.openstreetmap.josm.tools.Logging;
025
026/**
027 * This is a table cell editor for selecting a possible tag value from a list of
028 * proposed tag values. The editor also allows to select all proposed valued or
029 * to remove the tag.
030 *
031 * The editor responds intercepts some keys and interprets them as navigation keys. It
032 * forwards navigation events to {@link NavigationListener}s registred with this editor.
033 * You should register the parent table using this editor as {@link NavigationListener}.
034 *
035 * {@link KeyEvent#VK_ENTER} and {@link KeyEvent#VK_TAB} trigger a {@link NavigationListener#gotoNextDecision()}.
036 */
037public class MultiValueCellEditor extends AbstractCellEditor implements TableCellEditor {
038
039    /**
040     * Defines the interface for an object implementing navigation between rows
041     */
042    public interface NavigationListener {
043        /** Call when need to go to next row */
044        void gotoNextDecision();
045
046        /** Call when need to go to previous row */
047        void gotoPreviousDecision();
048    }
049
050    /** the combo box used as editor */
051    private final JosmComboBox<Object> editor;
052    private final DefaultComboBoxModel<Object> editorModel;
053    private final CopyOnWriteArrayList<NavigationListener> listeners;
054
055    /**
056     * Adds a navigation listener.
057     * @param listener navigation listener to add
058     */
059    public void addNavigationListener(NavigationListener listener) {
060        if (listener != null) {
061            listeners.addIfAbsent(listener);
062        }
063    }
064
065    /**
066     * Removes a navigation listener.
067     * @param listener navigation listener to remove
068     */
069    public void removeNavigationListener(NavigationListener listener) {
070        listeners.remove(listener);
071    }
072
073    protected void fireGotoNextDecision() {
074        for (NavigationListener l: listeners) {
075            l.gotoNextDecision();
076        }
077    }
078
079    protected void fireGotoPreviousDecision() {
080        for (NavigationListener l: listeners) {
081            l.gotoPreviousDecision();
082        }
083    }
084
085    /**
086     * Construct a new {@link MultiValueCellEditor}
087     */
088    public MultiValueCellEditor() {
089        editorModel = new DefaultComboBoxModel<>();
090        editor = new JosmComboBox<Object>(editorModel) {
091            @Override
092            public void processKeyEvent(KeyEvent e) {
093                if (e.getID() == KeyEvent.KEY_PRESSED) {
094                    int keyCode = e.getKeyCode();
095                    if (keyCode == KeyEvent.VK_ENTER) {
096                        fireGotoNextDecision();
097                    } else if (keyCode == KeyEvent.VK_TAB) {
098                        if (e.isShiftDown()) {
099                            fireGotoPreviousDecision();
100                        } else {
101                            fireGotoNextDecision();
102                        }
103                    } else if (keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_BACK_SPACE) {
104                        if (editorModel.getIndexOf(MultiValueDecisionType.KEEP_NONE) > 0) {
105                            editorModel.setSelectedItem(MultiValueDecisionType.KEEP_NONE);
106                            fireGotoNextDecision();
107                        }
108                    } else if (keyCode == KeyEvent.VK_ESCAPE) {
109                        cancelCellEditing();
110                    }
111                }
112                super.processKeyEvent(e);
113            }
114        };
115        editor.addFocusListener(
116                new FocusAdapter() {
117                    @Override
118                    public void focusGained(FocusEvent e) {
119                        editor.showPopup();
120                    }
121                }
122        );
123        editor.addItemListener(e -> {
124            if (e.getStateChange() == ItemEvent.SELECTED)
125                fireEditingStopped();
126        });
127        editor.setRenderer(new EditorCellRenderer());
128        listeners = new CopyOnWriteArrayList<>();
129    }
130
131    /**
132     * Populate model with possible values for a decision, and select current choice.
133     * @param decision The {@link MultiValueResolutionDecision} to proceed
134     */
135    protected void initEditor(MultiValueResolutionDecision decision) {
136        editorModel.removeAllElements();
137        if (!decision.isDecided()) {
138            editorModel.addElement(MultiValueDecisionType.UNDECIDED);
139        }
140        for (String value: decision.getValues()) {
141            editorModel.addElement(value);
142        }
143        if (decision.canSumAllNumeric()) {
144            editorModel.addElement(MultiValueDecisionType.SUM_ALL_NUMERIC);
145        }
146        if (decision.canKeepNone()) {
147            editorModel.addElement(MultiValueDecisionType.KEEP_NONE);
148        }
149        if (decision.canKeepAll()) {
150            editorModel.addElement(MultiValueDecisionType.KEEP_ALL);
151        }
152        switch(decision.getDecisionType()) {
153        case UNDECIDED:
154            editor.setSelectedItem(MultiValueDecisionType.UNDECIDED);
155            break;
156        case KEEP_ONE:
157            editor.setSelectedItem(decision.getChosenValue());
158            break;
159        case KEEP_NONE:
160            editor.setSelectedItem(MultiValueDecisionType.KEEP_NONE);
161            break;
162        case KEEP_ALL:
163            editor.setSelectedItem(MultiValueDecisionType.KEEP_ALL);
164            break;
165        case SUM_ALL_NUMERIC:
166            editor.setSelectedItem(MultiValueDecisionType.SUM_ALL_NUMERIC);
167            break;
168        default:
169            Logging.error("Unknown decision type in initEditor(): "+decision.getDecisionType());
170        }
171    }
172
173    @Override
174    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
175        MultiValueResolutionDecision decision = (MultiValueResolutionDecision) value;
176        initEditor(decision);
177        editor.requestFocus();
178        return editor;
179    }
180
181    @Override
182    public Object getCellEditorValue() {
183        return editor.getSelectedItem();
184    }
185
186    /**
187     * The cell renderer used in the edit combo box
188     *
189     */
190    private static class EditorCellRenderer extends JLabel implements ListCellRenderer<Object> {
191
192        /**
193         * Construct a new {@link EditorCellRenderer}.
194         */
195        EditorCellRenderer() {
196            setOpaque(true);
197        }
198
199        /**
200         * Set component color.
201         * @param selected true if is selected
202         */
203        protected void renderColors(boolean selected) {
204            if (selected) {
205                setForeground(UIManager.getColor("ComboBox.selectionForeground"));
206                setBackground(UIManager.getColor("ComboBox.selectionBackground"));
207            } else {
208                setForeground(UIManager.getColor("ComboBox.foreground"));
209                setBackground(UIManager.getColor("ComboBox.background"));
210            }
211        }
212
213        /**
214         * Set text for a value
215         * @param value {@link String} or {@link MultiValueDecisionType}
216         */
217        protected void renderValue(Object value) {
218            setFont(UIManager.getFont("ComboBox.font"));
219            if (String.class.isInstance(value)) {
220                setText(String.class.cast(value));
221            } else if (MultiValueDecisionType.class.isInstance(value)) {
222                switch(MultiValueDecisionType.class.cast(value)) {
223                case UNDECIDED:
224                    setText(tr("Choose a value"));
225                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
226                    break;
227                case KEEP_NONE:
228                    setText(tr("none"));
229                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
230                    break;
231                case KEEP_ALL:
232                    setText(tr("all"));
233                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
234                    break;
235                case SUM_ALL_NUMERIC:
236                    setText(tr("sum"));
237                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
238                    break;
239                default:
240                    // don't display other values
241                }
242            }
243        }
244
245        @Override
246        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
247            renderColors(isSelected);
248            renderValue(value);
249            return this;
250        }
251    }
252}