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