// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.conflict.tags; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Component; import java.awt.Font; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; import java.awt.event.KeyEvent; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.AbstractCellEditor; import javax.swing.DefaultComboBoxModel; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JTable; import javax.swing.ListCellRenderer; import javax.swing.UIManager; import javax.swing.table.TableCellEditor; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.gui.widgets.JosmComboBox; /** * This is a table cell editor for selecting a possible tag value from a list of * proposed tag values. The editor also allows to select all proposed valued or * to remove the tag. * * The editor responds intercepts some keys and interprets them as navigation keys. It * forwards navigation events to {@link NavigationListener}s registred with this editor. * You should register the parent table using this editor as {@link NavigationListener}. * * {@link KeyEvent#VK_ENTER} and {@link KeyEvent#VK_TAB} trigger a {@link NavigationListener#gotoNextDecision()}. */ public class MultiValueCellEditor extends AbstractCellEditor implements TableCellEditor { /** * Defines the interface for an object implementing navigation between rows */ public interface NavigationListener { /** Call when need to go to next row */ void gotoNextDecision(); /** Call when need to go to previous row */ void gotoPreviousDecision(); } /** the combo box used as editor */ private final JosmComboBox<Object> editor; private final DefaultComboBoxModel<Object> editorModel; private final CopyOnWriteArrayList<NavigationListener> listeners; /** * Adds a navigation listener. * @param listener navigation listener to add */ public void addNavigationListener(NavigationListener listener) { if (listener != null) { listeners.addIfAbsent(listener); } } /** * Removes a navigation listener. * @param listener navigation listener to remove */ public void removeNavigationListener(NavigationListener listener) { listeners.remove(listener); } protected void fireGotoNextDecision() { for (NavigationListener l: listeners) { l.gotoNextDecision(); } } protected void fireGotoPreviousDecision() { for (NavigationListener l: listeners) { l.gotoPreviousDecision(); } } /** * Construct a new {@link MultiValueCellEditor} */ public MultiValueCellEditor() { editorModel = new DefaultComboBoxModel<>(); editor = new JosmComboBox<Object>(editorModel) { @Override public void processKeyEvent(KeyEvent e) { if (e.getID() == KeyEvent.KEY_PRESSED) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_ENTER) { fireGotoNextDecision(); } else if (keyCode == KeyEvent.VK_TAB) { if (e.isShiftDown()) { fireGotoPreviousDecision(); } else { fireGotoNextDecision(); } } else if (keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_BACK_SPACE) { if (editorModel.getIndexOf(MultiValueDecisionType.KEEP_NONE) > 0) { editorModel.setSelectedItem(MultiValueDecisionType.KEEP_NONE); fireGotoNextDecision(); } } else if (keyCode == KeyEvent.VK_ESCAPE) { cancelCellEditing(); } } super.processKeyEvent(e); } }; editor.addFocusListener( new FocusAdapter() { @Override public void focusGained(FocusEvent e) { editor.showPopup(); } } ); editor.addItemListener(e -> { if (e.getStateChange() == ItemEvent.SELECTED) fireEditingStopped(); }); editor.setRenderer(new EditorCellRenderer()); listeners = new CopyOnWriteArrayList<>(); } /** * Populate model with possible values for a decision, and select current choice. * @param decision The {@link MultiValueResolutionDecision} to proceed */ protected void initEditor(MultiValueResolutionDecision decision) { editorModel.removeAllElements(); if (!decision.isDecided()) { editorModel.addElement(MultiValueDecisionType.UNDECIDED); } for (String value: decision.getValues()) { editorModel.addElement(value); } if (decision.canSumAllNumeric()) { editorModel.addElement(MultiValueDecisionType.SUM_ALL_NUMERIC); } if (decision.canKeepNone()) { editorModel.addElement(MultiValueDecisionType.KEEP_NONE); } if (decision.canKeepAll()) { editorModel.addElement(MultiValueDecisionType.KEEP_ALL); } switch(decision.getDecisionType()) { case UNDECIDED: editor.setSelectedItem(MultiValueDecisionType.UNDECIDED); break; case KEEP_ONE: editor.setSelectedItem(decision.getChosenValue()); break; case KEEP_NONE: editor.setSelectedItem(MultiValueDecisionType.KEEP_NONE); break; case KEEP_ALL: editor.setSelectedItem(MultiValueDecisionType.KEEP_ALL); break; case SUM_ALL_NUMERIC: editor.setSelectedItem(MultiValueDecisionType.SUM_ALL_NUMERIC); break; default: Main.error("Unknown decision type in initEditor(): "+decision.getDecisionType()); } } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { MultiValueResolutionDecision decision = (MultiValueResolutionDecision) value; initEditor(decision); editor.requestFocus(); return editor; } @Override public Object getCellEditorValue() { return editor.getSelectedItem(); } /** * The cell renderer used in the edit combo box * */ private static class EditorCellRenderer extends JLabel implements ListCellRenderer<Object> { /** * Construct a new {@link EditorCellRenderer}. */ EditorCellRenderer() { setOpaque(true); } /** * Set component color. * @param selected true if is selected */ protected void renderColors(boolean selected) { if (selected) { setForeground(UIManager.getColor("ComboBox.selectionForeground")); setBackground(UIManager.getColor("ComboBox.selectionBackground")); } else { setForeground(UIManager.getColor("ComboBox.foreground")); setBackground(UIManager.getColor("ComboBox.background")); } } /** * Set text for a value * @param value {@link String} or {@link MultiValueDecisionType} */ protected void renderValue(Object value) { setFont(UIManager.getFont("ComboBox.font")); if (String.class.isInstance(value)) { setText(String.class.cast(value)); } else if (MultiValueDecisionType.class.isInstance(value)) { switch(MultiValueDecisionType.class.cast(value)) { case UNDECIDED: setText(tr("Choose a value")); setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD)); break; case KEEP_NONE: setText(tr("none")); setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD)); break; case KEEP_ALL: setText(tr("all")); setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD)); break; case SUM_ALL_NUMERIC: setText(tr("sum")); setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD)); break; default: // don't display other values } } } @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { renderColors(isSelected); renderValue(value); return this; } } }