001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.presets.items; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Cursor; 008import java.awt.Insets; 009import java.awt.event.ActionEvent; 010import java.awt.event.ActionListener; 011import java.awt.event.ComponentAdapter; 012import java.awt.event.ComponentEvent; 013import java.util.Arrays; 014import java.util.Comparator; 015 016import javax.swing.AbstractAction; 017import javax.swing.JButton; 018import javax.swing.JColorChooser; 019import javax.swing.JComponent; 020import javax.swing.JLabel; 021import javax.swing.JPanel; 022 023import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem; 024import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority; 025import org.openstreetmap.josm.gui.MainApplication; 026import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors; 027import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor; 028import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel; 029import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField; 030import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; 031import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport; 032import org.openstreetmap.josm.gui.widgets.JosmComboBox; 033import org.openstreetmap.josm.gui.widgets.OrientationAction; 034import org.openstreetmap.josm.tools.ColorHelper; 035import org.openstreetmap.josm.tools.GBC; 036 037/** 038 * Combobox type. 039 */ 040public class Combo extends ComboMultiSelect { 041 042 /** 043 * Whether the combo box is editable, which means that the user can add other values as text. 044 * Default is {@code true}. If {@code false} it is readonly, which means that the user can only select an item in the list. 045 */ 046 public boolean editable = true; // NOSONAR 047 /** The length of the combo box (number of characters allowed). */ 048 public int length; // NOSONAR 049 050 protected JosmComboBox<PresetListEntry> combobox; 051 protected AutoCompComboBoxModel<PresetListEntry> dropDownModel; 052 protected AutoCompComboBoxModel<AutoCompletionItem> autoCompModel; 053 054 class ComponentListener extends ComponentAdapter { 055 @Override 056 public void componentResized(ComponentEvent e) { 057 // Make multi-line JLabels the correct size 058 // Only needed if there is any short_description 059 JComponent component = (JComponent) e.getSource(); 060 int width = component.getWidth(); 061 if (width == 0) 062 width = 200; 063 Insets insets = component.getInsets(); 064 width -= insets.left + insets.right + 10; 065 ComboMultiSelectListCellRenderer renderer = (ComboMultiSelectListCellRenderer) combobox.getRenderer(); 066 renderer.setWidth(width); 067 combobox.setRenderer(null); // needed to make prop change fire 068 combobox.setRenderer(renderer); 069 } 070 } 071 072 /** 073 * Constructs a new {@code Combo}. 074 */ 075 public Combo() { 076 delimiter = ','; 077 } 078 079 private void addEntry(PresetListEntry entry) { 080 if (!seenValues.containsKey(entry.value)) { 081 dropDownModel.addElement(entry); 082 seenValues.put(entry.value, entry); 083 } 084 } 085 086 @Override 087 protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) { 088 initializeLocaleText(null); 089 usage = determineTextUsage(support.getSelected(), key); 090 seenValues.clear(); 091 // get the standard values from the preset definition 092 initListEntries(); 093 094 // init the model 095 dropDownModel = new AutoCompComboBoxModel<>(Comparator.<PresetListEntry>naturalOrder()); 096 097 if (!usage.hasUniqueValue() && !usage.unused()) { 098 addEntry(PresetListEntry.ENTRY_DIFFERENT); 099 } 100 presetListEntries.forEach(this::addEntry); 101 if (default_ != null) { 102 addEntry(new PresetListEntry(default_, this)); 103 } 104 addEntry(PresetListEntry.ENTRY_EMPTY); 105 106 usage.map.forEach((value, count) -> { 107 addEntry(new PresetListEntry(value, this)); 108 }); 109 110 combobox = new JosmComboBox<>(dropDownModel); 111 AutoCompComboBoxEditor<AutoCompletionItem> editor = new AutoCompComboBoxEditor<>(); 112 combobox.setEditor(editor); 113 114 // The default behaviour of JComboBox is to size the editor according to the tallest item in 115 // the dropdown list. We don't want that to happen because we want to show taller items in 116 // the list than in the editor. We can't use 117 // {@code combobox.setPrototypeDisplayValue(PresetListEntry.ENTRY_EMPTY);} because that would 118 // set a fixed cell height in JList. 119 combobox.setPreferredHeight(combobox.getPreferredSize().height); 120 121 // a custom cell renderer capable of displaying a short description text along with the 122 // value 123 combobox.setRenderer(new ComboMultiSelectListCellRenderer(combobox, combobox.getRenderer(), 200, key)); 124 combobox.setEditable(editable); 125 126 autoCompModel = new AutoCompComboBoxModel<>(Comparator.<AutoCompletionItem>naturalOrder()); 127 getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement); 128 getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD))); 129 130 AutoCompTextField<AutoCompletionItem> tf = editor.getEditorComponent(); 131 tf.setModel(autoCompModel); 132 133 if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) { 134 combobox.setHint(key); 135 } 136 if (length > 0) { 137 tf.setMaxTextLength(length); 138 } 139 140 JLabel label = addLabel(p); 141 142 if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) { 143 p.add(combobox, GBC.std().fill(GBC.HORIZONTAL)); // NOSONAR 144 JButton button = new JButton(new ChooseColorAction()); 145 button.setOpaque(true); 146 button.setBorderPainted(false); 147 button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 148 p.add(button, GBC.eol().fill(GBC.VERTICAL)); // NOSONAR 149 ActionListener updateColor = ignore -> button.setBackground(getColor()); 150 updateColor.actionPerformed(null); 151 combobox.addActionListener(updateColor); 152 } else { 153 p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL)); // NOSONAR 154 } 155 156 String initialValue = getInitialValue(usage, support); 157 PresetListEntry selItem = find(initialValue); 158 if (selItem != null) { 159 combobox.setSelectedItem(selItem); 160 } else { 161 combobox.setText(initialValue); 162 } 163 164 combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value)); 165 combobox.addComponentListener(new ComponentListener()); 166 167 label.setLabelFor(combobox); 168 combobox.setToolTipText(getKeyTooltipText()); 169 combobox.applyComponentOrientation(OrientationAction.getValueOrientation(key)); 170 171 return true; 172 } 173 174 /** 175 * Finds the PresetListEntry that matches value. 176 * <p> 177 * Looks in the model for an element whose {@code value} matches {@code value}. 178 * 179 * @param value The value to match. 180 * @return The entry or null 181 */ 182 private PresetListEntry find(String value) { 183 return dropDownModel.asCollection().stream().filter(o -> o.value.equals(value)).findAny().orElse(null); 184 } 185 186 class ChooseColorAction extends AbstractAction { 187 ChooseColorAction() { 188 putValue(SHORT_DESCRIPTION, tr("Choose a color")); 189 } 190 191 @Override 192 public void actionPerformed(ActionEvent e) { 193 Color color = getColor(); 194 color = JColorChooser.showDialog(MainApplication.getMainPanel(), tr("Choose a color"), color); 195 setColor(color); 196 } 197 } 198 199 protected void setColor(Color color) { 200 if (color != null) { 201 combobox.setSelectedItem(ColorHelper.color2html(color)); 202 } 203 } 204 205 protected Color getColor() { 206 String colorString = getSelectedItem().value; 207 return colorString.startsWith("#") 208 ? ColorHelper.html2color(colorString) 209 : CSSColors.get(colorString); 210 } 211 212 @Override 213 protected PresetListEntry getSelectedItem() { 214 Object sel = combobox.getSelectedItem(); 215 if (sel instanceof PresetListEntry) 216 // selected from the dropdown 217 return (PresetListEntry) sel; 218 if (sel instanceof String) { 219 // free edit. If the free edit corresponds to a known entry, use that entry. This is 220 // to avoid that we write a display_value to the tag's value, eg. if the user did an 221 // undo. 222 PresetListEntry selItem = dropDownModel.find((String) sel); 223 if (selItem != null) 224 return selItem; 225 return new PresetListEntry((String) sel, this); 226 } 227 return PresetListEntry.ENTRY_EMPTY; 228 } 229}