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}