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}