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}