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}