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