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