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 interface NavigationListener {
044        /** Call when need to go to next row */
045        void gotoNextDecision();
046
047        /** Call when need to go to previous row */
048        void gotoPreviousDecision();
049    }
050
051    /** the combo box used as editor */
052    private final JosmComboBox<Object> editor;
053    private final DefaultComboBoxModel<Object> editorModel;
054    private final CopyOnWriteArrayList<NavigationListener> listeners;
055
056    /**
057     * Adds a navigation listener.
058     * @param listener navigation listener to add
059     */
060    public void addNavigationListener(NavigationListener listener) {
061        if (listener != null) {
062            listeners.addIfAbsent(listener);
063        }
064    }
065
066    /**
067     * Removes a navigation listener.
068     * @param listener navigation listener to remove
069     */
070    public void removeNavigationListener(NavigationListener listener) {
071        listeners.remove(listener);
072    }
073
074    protected void fireGotoNextDecision() {
075        for (NavigationListener l: listeners) {
076            l.gotoNextDecision();
077        }
078    }
079
080    protected void fireGotoPreviousDecision() {
081        for (NavigationListener l: listeners) {
082            l.gotoPreviousDecision();
083        }
084    }
085
086    /**
087     * Construct a new {@link MultiValueCellEditor}
088     */
089    public MultiValueCellEditor() {
090        editorModel = new DefaultComboBoxModel<>();
091        editor = new JosmComboBox<Object>(editorModel) {
092            @Override
093            public void processKeyEvent(KeyEvent e) {
094                int keyCode = e.getKeyCode();
095                if (e.getID() == KeyEvent.KEY_PRESSED && keyCode == KeyEvent.VK_ENTER) {
096                    fireGotoNextDecision();
097                } else if (e.getID() == KeyEvent.KEY_PRESSED && keyCode == KeyEvent.VK_TAB) {
098                    if (e.isShiftDown()) {
099                        fireGotoPreviousDecision();
100                    } else {
101                        fireGotoNextDecision();
102                    }
103                } else if (e.getID() == KeyEvent.KEY_PRESSED && 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 (e.getID() == KeyEvent.KEY_PRESSED && keyCode == KeyEvent.VK_ESCAPE) {
109                    cancelCellEditing();
110                }
111                super.processKeyEvent(e);
112            }
113        };
114        editor.addFocusListener(
115                new FocusAdapter() {
116                    @Override
117                    public void focusGained(FocusEvent e) {
118                        editor.showPopup();
119                    }
120                }
121        );
122        editor.addItemListener(
123                new ItemListener() {
124                    @Override
125                    public void itemStateChanged(ItemEvent e) {
126                        if (e.getStateChange() == ItemEvent.SELECTED)
127                            fireEditingStopped();
128                    }
129                }
130        );
131        editor.setRenderer(new EditorCellRenderer());
132        listeners = new CopyOnWriteArrayList<>();
133    }
134
135    /**
136     * Populate model with possible values for a decision, and select current choice.
137     * @param decision The {@link MultiValueResolutionDecision} to proceed
138     */
139    protected void initEditor(MultiValueResolutionDecision decision) {
140        editorModel.removeAllElements();
141        if (!decision.isDecided()) {
142            editorModel.addElement(MultiValueDecisionType.UNDECIDED);
143        }
144        for (String value: decision.getValues()) {
145            editorModel.addElement(value);
146        }
147        if (decision.canSumAllNumeric()) {
148            editorModel.addElement(MultiValueDecisionType.SUM_ALL_NUMERIC);
149        }
150        if (decision.canKeepNone()) {
151            editorModel.addElement(MultiValueDecisionType.KEEP_NONE);
152        }
153        if (decision.canKeepAll()) {
154            editorModel.addElement(MultiValueDecisionType.KEEP_ALL);
155        }
156        switch(decision.getDecisionType()) {
157        case UNDECIDED:
158            editor.setSelectedItem(MultiValueDecisionType.UNDECIDED);
159            break;
160        case KEEP_ONE:
161            editor.setSelectedItem(decision.getChosenValue());
162            break;
163        case KEEP_NONE:
164            editor.setSelectedItem(MultiValueDecisionType.KEEP_NONE);
165            break;
166        case KEEP_ALL:
167            editor.setSelectedItem(MultiValueDecisionType.KEEP_ALL);
168            break;
169        case SUM_ALL_NUMERIC:
170            editor.setSelectedItem(MultiValueDecisionType.SUM_ALL_NUMERIC);
171            break;
172        default:
173            Main.error("Unknown decision type in initEditor(): "+decision.getDecisionType());
174        }
175    }
176
177    @Override
178    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
179        MultiValueResolutionDecision decision = (MultiValueResolutionDecision) value;
180        initEditor(decision);
181        editor.requestFocus();
182        return editor;
183    }
184
185    @Override
186    public Object getCellEditorValue() {
187        return editor.getSelectedItem();
188    }
189
190    /**
191     * The cell renderer used in the edit combo box
192     *
193     */
194    private static class EditorCellRenderer extends JLabel implements ListCellRenderer<Object> {
195
196        /**
197         * Construct a new {@link EditorCellRenderer}.
198         */
199        EditorCellRenderer() {
200            setOpaque(true);
201        }
202
203        /**
204         * Set component color.
205         * @param selected true if is selected
206         */
207        protected void renderColors(boolean selected) {
208            if (selected) {
209                setForeground(UIManager.getColor("ComboBox.selectionForeground"));
210                setBackground(UIManager.getColor("ComboBox.selectionBackground"));
211            } else {
212                setForeground(UIManager.getColor("ComboBox.foreground"));
213                setBackground(UIManager.getColor("ComboBox.background"));
214            }
215        }
216
217        /**
218         * Set text for a value
219         * @param value {@link String} or {@link MultiValueDecisionType}
220         */
221        protected void renderValue(Object value) {
222            setFont(UIManager.getFont("ComboBox.font"));
223            if (String.class.isInstance(value)) {
224                setText(String.class.cast(value));
225            } else if (MultiValueDecisionType.class.isInstance(value)) {
226                switch(MultiValueDecisionType.class.cast(value)) {
227                case UNDECIDED:
228                    setText(tr("Choose a value"));
229                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
230                    break;
231                case KEEP_NONE:
232                    setText(tr("none"));
233                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
234                    break;
235                case KEEP_ALL:
236                    setText(tr("all"));
237                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
238                    break;
239                case SUM_ALL_NUMERIC:
240                    setText(tr("sum"));
241                    setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
242                    break;
243                default:
244                    // don't display other values
245                }
246            }
247        }
248
249        @Override
250        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
251            renderColors(isSelected);
252            renderValue(value);
253            return this;
254        }
255    }
256}