Changeset 18221 in josm


Ignore:
Timestamp:
2021-09-13T00:41:53+02:00 (3 years ago)
Author:
Don-vip
Message:

fix #21319 - Refactoring of class hierarchy around JosmComboBox / AutoCompComboBox (patch by marcello):

  • Code reuse: JosmComboBox now uses a JosmTextField as editor
  • Code reuse: AutoCompComboBox now uses AutoCompTextField as editor
  • JosmComboBox uses more of the original L&F
  • JosmComboBox lists now expand all the way to the bottom or the top of the screen
  • Variable height items in combobox lists now work, see #19321
  • Autocomplete uses different algorithm, fix #21290
  • editable="false" comboboxes in Presets now work, fix #6157 see #11024 see #18714
  • The user may toggle LTR-RTL script in JosmTextField (menu and ctrl+space)
  • LTR-RTL automatically toggles according to key in AddTag and EditTag dialogs, fix #16163
Location:
trunk
Files:
9 added
31 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java

    r16824 r18221  
    2222import java.util.stream.IntStream;
    2323
    24 import javax.swing.AbstractListModel;
    25 import javax.swing.ComboBoxModel;
    2624import javax.swing.DefaultListSelectionModel;
    2725import javax.swing.JOptionPane;
     
    4240import org.openstreetmap.josm.gui.util.ChangeNotifier;
    4341import org.openstreetmap.josm.gui.util.TableHelper;
     42import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    4443import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
    4544import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    833832    }
    834833
    835     public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
     834    public class ComparePairListModel extends JosmComboBoxModel<ComparePairType> {
    836835
    837836        private int selectedIdx;
  • trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java

    r17333 r18221  
    1313
    1414import javax.swing.AbstractCellEditor;
    15 import javax.swing.DefaultComboBoxModel;
    1615import javax.swing.JLabel;
    1716import javax.swing.JList;
     
    2221
    2322import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     23import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    2424import org.openstreetmap.josm.tools.Logging;
    2525
     
    5050    /** the combo box used as editor */
    5151    private final JosmComboBox<Object> editor;
    52     private final DefaultComboBoxModel<Object> editorModel;
     52    private final JosmComboBoxModel<Object> editorModel;
    5353    private final CopyOnWriteArrayList<NavigationListener> listeners;
    5454
     
    8787     */
    8888    public MultiValueCellEditor() {
    89         editorModel = new DefaultComboBoxModel<>();
     89        editorModel = new JosmComboBoxModel<>();
    9090        editor = new JosmComboBox<Object>(editorModel) {
    9191            @Override
  • trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java

    r12620 r18221  
    77import java.awt.Font;
    88
    9 import javax.swing.DefaultComboBoxModel;
    109import javax.swing.ImageIcon;
    1110import javax.swing.JLabel;
     
    1615import org.openstreetmap.josm.gui.conflict.ConflictColors;
    1716import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     17import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    1818import org.openstreetmap.josm.tools.ImageProvider;
    1919import org.openstreetmap.josm.tools.Logging;
     
    2727    private final ImageIcon iconDecided;
    2828    private final ImageIcon iconUndecided;
    29     private final DefaultComboBoxModel<Object> model;
     29    private final JosmComboBoxModel<Object> model;
    3030    private final JosmComboBox<Object> cbDecisionRenderer;
    3131
     
    3737        iconDecided = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
    3838        iconUndecided = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
    39         model = new DefaultComboBoxModel<>();
     39        model = new JosmComboBoxModel<>();
    4040        cbDecisionRenderer = new JosmComboBox<>(model);
    4141    }
  • trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java

    r18215 r18221  
    77import java.awt.BorderLayout;
    88import java.awt.Component;
     9import java.awt.ComponentOrientation;
    910import java.awt.Container;
    1011import java.awt.Cursor;
     
    1415import java.awt.GridBagConstraints;
    1516import java.awt.GridBagLayout;
    16 import java.awt.datatransfer.Clipboard;
    17 import java.awt.datatransfer.Transferable;
    1817import java.awt.event.ActionEvent;
    19 import java.awt.event.FocusAdapter;
    2018import java.awt.event.FocusEvent;
     19import java.awt.event.FocusListener;
    2120import java.awt.event.InputEvent;
    2221import java.awt.event.KeyEvent;
     
    4544import javax.swing.Box;
    4645import javax.swing.ButtonGroup;
    47 import javax.swing.ComboBoxModel;
    48 import javax.swing.DefaultListCellRenderer;
    4946import javax.swing.ImageIcon;
    5047import javax.swing.JCheckBoxMenuItem;
     
    6158import javax.swing.ListCellRenderer;
    6259import javax.swing.SwingUtilities;
     60import javax.swing.event.PopupMenuEvent;
     61import javax.swing.event.PopupMenuListener;
    6362import javax.swing.table.DefaultTableModel;
    64 import javax.swing.text.JTextComponent;
    6563
    6664import org.openstreetmap.josm.actions.JosmAction;
     
    8785import org.openstreetmap.josm.gui.IExtendedDialog;
    8886import org.openstreetmap.josm.gui.MainApplication;
    89 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    9087import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
     88import org.openstreetmap.josm.gui.tagging.ac.AutoCompEvent;
     89import org.openstreetmap.josm.gui.tagging.ac.AutoCompListener;
    9190import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
    9291import org.openstreetmap.josm.gui.util.GuiHelper;
    9392import org.openstreetmap.josm.gui.util.WindowGeometry;
     93import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
     94import org.openstreetmap.josm.gui.widgets.OrientationAction;
    9495import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
    9596import org.openstreetmap.josm.io.XmlWriter;
     
    116117
    117118    private String changedKey;
    118     private String objKey;
    119119
    120120    static final Comparator<AutoCompletionItem> DEFAULT_AC_ITEM_COMPARATOR =
     
    125125    /** Maximum number of recent tags */
    126126    public static final int MAX_LRU_TAGS_NUMBER = 30;
    127 
    128127    /** Autocomplete keys by default */
    129128    public static final BooleanProperty AUTOCOMPLETE_KEYS = new BooleanProperty("properties.autocomplete-keys", true);
     
    193192
    194193    /**
     194     * A custom list cell renderer that adds the value count to some items.
     195     */
     196    static class TEHListCellRenderer extends JosmListCellRenderer<AutoCompletionItem> {
     197        protected Map<String, Integer> map;
     198
     199        TEHListCellRenderer(Component component, ListCellRenderer<? super AutoCompletionItem> renderer, Map<String, Integer> map) {
     200            super(component, renderer);
     201            this.map = map;
     202        }
     203
     204        @Override
     205        public Component getListCellRendererComponent(JList<? extends AutoCompletionItem> list, AutoCompletionItem value,
     206                                                    int index, boolean isSelected, boolean cellHasFocus) {
     207            Integer count = null;
     208            // if there is a value count add it to the text
     209            if (map != null) {
     210                String text = value == null ? "" : value.toString();
     211                count = map.get(text);
     212                if (count != null) {
     213                    value = new AutoCompletionItem(tr("{0} ({1})", text, count));
     214                }
     215            }
     216            Component l = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
     217            l.setComponentOrientation(component.getComponentOrientation());
     218            if (count != null) {
     219                l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD));
     220            }
     221            return l;
     222        }
     223    }
     224
     225    /**
    195226     * Constructs a new {@code TagEditHelper}.
    196227     * @param tagTable tag table
     
    284315            return;
    285316
    286         String key = getDataKey(row);
    287         objKey = key;
    288 
    289         final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, key);
     317        final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, getDataKey(row));
    290318        editDialog.showDialog();
    291319        if (editDialog.getValue() != 1)
     
    444472        private final transient Map<String, Integer> m;
    445473        private final transient Comparator<AutoCompletionItem> usedValuesAwareComparator;
    446 
    447         private final transient ListCellRenderer<AutoCompletionItem> cellRenderer = new ListCellRenderer<AutoCompletionItem>() {
    448             private final DefaultListCellRenderer def = new DefaultListCellRenderer();
    449             @Override
    450             public Component getListCellRendererComponent(JList<? extends AutoCompletionItem> list,
    451                     AutoCompletionItem value, int index, boolean isSelected, boolean cellHasFocus) {
    452                 Component c = def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
    453                 if (c instanceof JLabel) {
    454                     String str = value.getValue();
    455                     if (valueCount.containsKey(objKey)) {
    456                         Map<String, Integer> map = valueCount.get(objKey);
    457                         if (map.containsKey(str)) {
    458                             str = tr("{0} ({1})", str, map.get(str));
    459                             c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD));
    460                         }
    461                     }
    462                     ((JLabel) c).setText(str);
    463                 }
    464                 return c;
    465             }
    466         };
    467 
    468         protected EditTagDialog(String key, Map<String, Integer> map, final boolean initialFocusOnKey) {
     474        private final transient AutoCompletionManager autocomplete;
     475
     476        protected EditTagDialog(String key, Map<String, Integer> map, boolean initialFocusOnKey) {
    469477            super(MainApplication.getMainFrame(), trn("Change value?", "Change values?", map.size()), tr("OK"), tr("Cancel"));
    470478            setButtonIcons("ok", "cancel");
     
    473481            this.key = key;
    474482            this.m = map;
     483            this.initialFocusOnKey = initialFocusOnKey;
    475484
    476485            usedValuesAwareComparator = (o1, o2) -> {
     
    493502            mainPanel.add(new JLabel(msg), BorderLayout.NORTH);
    494503
    495             JPanel p = new JPanel(new GridBagLayout());
     504            JPanel p = new JPanel(new GridBagLayout()) {
     505                /**
     506                 * This hack allows the comboboxes to have their own orientation.
     507                 *
     508                 * The problem is that
     509                 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
     510                 * {@code applyComponentOrientation} very late in the dialog construction process
     511                 * thus overwriting the orientation the components have chosen for themselves.
     512                 *
     513                 * This stops the propagation of {@code applyComponentOrientation}, thus all
     514                 * components may (and have to) set their own orientation.
     515                 */
     516                @Override
     517                public void applyComponentOrientation(ComponentOrientation o) {
     518                    setComponentOrientation(o);
     519                }
     520            };
    496521            mainPanel.add(p, BorderLayout.CENTER);
    497522
    498             AutoCompletionManager autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
     523            autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
    499524            List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR);
    500525
    501526            keys = new AutoCompComboBox<>();
    502527            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
    503             keys.setPrototypeDisplayValue(new AutoCompletionItem(key));
    504528            keys.setEditable(true);
     529            keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
    505530            keys.getModel().addAllElements(keyList);
    506             keys.setSelectedItem(key);
     531            keys.setSelectedItemText(key);
    507532
    508533            p.add(Box.createVerticalStrut(5), GBC.eol());
     
    517542            values = new AutoCompComboBox<>();
    518543            values.getModel().setComparator(Comparator.naturalOrder());
    519             values.setPrototypeDisplayValue(new AutoCompletionItem(selection));
    520             values.setRenderer(cellRenderer);
     544            values.setRenderer(new TEHListCellRenderer(values, values.getRenderer(), valueCount.get(key)));
    521545            values.setEditable(true);
     546            values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
    522547            values.getModel().addAllElements(valueList);
    523             values.setSelectedItem(selection);
    524             values.getEditor().setItem(selection);
     548            values.setSelectedItemText(selection);
    525549
    526550            p.add(Box.createVerticalStrut(5), GBC.eol());
     
    528552            p.add(Box.createHorizontalStrut(10), GBC.std());
    529553            p.add(values, GBC.eol().fill(GBC.HORIZONTAL));
    530             values.getEditor().addActionListener(e -> buttonAction(0, null));
    531             addFocusAdapter(autocomplete, usedValuesAwareComparator);
    532 
    533             addUpdateIconListener();
     554            p.add(Box.createVerticalStrut(2), GBC.eol());
     555
     556            p.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
     557            keys.applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
     558            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
    534559
    535560            setContent(mainPanel, false);
    536561
    537             addWindowListener(new WindowAdapter() {
    538                 @Override
    539                 public void windowOpened(WindowEvent e) {
    540                     if (initialFocusOnKey) {
    541                         selectKeysComboBox();
    542                     } else {
    543                         selectValuesCombobox();
    544                     }
    545                 }
    546             });
     562            addEventListeners();
     563        }
     564
     565        @Override
     566        public void autoCompBefore(AutoCompEvent e) {
     567            updateValueModel(autocomplete, usedValuesAwareComparator);
     568        }
     569
     570        @Override
     571        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
     572            updateValueModel(autocomplete, usedValuesAwareComparator);
    547573        }
    548574
     
    600626    }
    601627
    602     protected abstract class AbstractTagsDialog extends ExtendedDialog {
     628    protected abstract class AbstractTagsDialog extends ExtendedDialog implements AutoCompListener, FocusListener, PopupMenuListener {
    603629        protected AutoCompComboBox<AutoCompletionItem> keys;
    604630        protected AutoCompComboBox<AutoCompletionItem> values;
     631        protected boolean initialFocusOnKey = true;
     632        /**
     633         * The 'values' model is currently holding values for this key. Used for lazy-loading of values.
     634         */
     635        protected String currentValuesModelKey = "";
    605636
    606637        AbstractTagsDialog(Component parent, String title, String... buttonTexts) {
     
    623654            setRememberWindowGeometry(getClass().getName() + ".geometry",
    624655                WindowGeometry.centerInWindow(MainApplication.getMainFrame(), size));
     656            keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
    625657        }
    626658
     
    642674                    rememberWindowGeometry(geometry);
    643675                }
    644                 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
    645676                updateOkButtonIcon();
    646677            }
     
    648679        }
    649680
    650         private void selectACComboBoxSavingUnixBuffer(AutoCompComboBox<AutoCompletionItem> cb) {
    651             // select combobox with saving unix system selection (middle mouse paste)
    652             Clipboard sysSel = ClipboardUtils.getSystemSelection();
    653             if (sysSel != null) {
    654                 Transferable old = ClipboardUtils.getClipboardContent(sysSel);
    655                 cb.requestFocusInWindow();
    656                 cb.getEditor().selectAll();
    657                 if (old != null) {
    658                     sysSel.setContents(old, null);
    659                 }
    660             } else {
    661                 cb.requestFocusInWindow();
    662                 cb.getEditor().selectAll();
    663             }
    664         }
    665 
    666         public void selectKeysComboBox() {
    667             selectACComboBoxSavingUnixBuffer(keys);
    668         }
    669 
    670         public void selectValuesCombobox() {
    671             selectACComboBoxSavingUnixBuffer(values);
    672         }
    673 
    674681        /**
    675         * Create a focus handling adapter and apply in to the editor component of value
    676         * autocompletion box.
    677         * @param autocomplete Manager handling the autocompletion
    678         * @param comparator Class to decide what values are offered on autocompletion
    679         * @return The created adapter
    680         */
    681         protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionItem> comparator) {
    682            // get the combo box' editor component
    683            final JTextComponent editor = values.getEditorComponent();
    684            // Refresh the values model when focus is gained
    685            FocusAdapter focus = new FocusAdapter() {
    686                @Override
    687                public void focusGained(FocusEvent e) {
    688                    Logging.trace("Focus gained by {0}, e={1}", values, e);
    689                    String key = keys.getEditor().getItem().toString();
    690                    List<AutoCompletionItem> correctItems = autocomplete.getTagValues(getAutocompletionKeys(key), comparator);
    691                    ComboBoxModel<AutoCompletionItem> currentModel = values.getModel();
    692                    final int size = correctItems.size();
    693                    boolean valuesOK = size == currentModel.getSize()
    694                            && IntStream.range(0, size).allMatch(i -> Objects.equals(currentModel.getElementAt(i), correctItems.get(i)));
    695                    if (!valuesOK) {
    696                        values.getModel().removeAllElements();
    697                        values.getModel().addAllElements(correctItems);
    698                    }
    699                    if (!Objects.equals(key, objKey)) {
    700                        values.getEditor().selectAll();
    701                        objKey = key;
    702                    }
    703                }
    704            };
    705            editor.addFocusListener(focus);
    706            return focus;
    707         }
    708 
    709         protected void addUpdateIconListener() {
    710             keys.addActionListener(ignore -> updateOkButtonIcon());
    711             values.addActionListener(ignore -> updateOkButtonIcon());
    712         }
    713 
    714         private void updateOkButtonIcon() {
     682         * Updates the values model if the key has changed
     683         *
     684         * @param autocomplete the autocompletion manager
     685         * @param comparator sorting order for the items in the combo dropdown
     686         */
     687        protected void updateValueModel(AutoCompletionManager autocomplete, Comparator<AutoCompletionItem> comparator) {
     688            String key = keys.getText();
     689            if (!key.equals(currentValuesModelKey)) {
     690                Logging.debug("updateValueModel: lazy loading values for key ''{0}''", key);
     691                // key has changed, reload model
     692                String savedText = values.getText();
     693                values.getModel().removeAllElements();
     694                values.getModel().addAllElements(autocomplete.getTagValues(getAutocompletionKeys(key), comparator));
     695                values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
     696                values.setSelectedItemText(savedText);
     697                values.getEditor().selectAll();
     698                currentValuesModelKey = key;
     699            }
     700        }
     701
     702        protected void addEventListeners() {
     703            // OK on Enter in values
     704            values.getEditor().addActionListener(e -> buttonAction(0, null));
     705            // update values orientation according to key
     706            keys.getEditorComponent().addFocusListener(this);
     707            // update the "values" data model before an autocomplete or list dropdown
     708            values.getEditorComponent().addAutoCompListener(this);
     709            values.addPopupMenuListener(this);
     710            // set the initial focus to either combobox
     711            addWindowListener(new WindowAdapter() {
     712                @Override
     713                public void windowOpened(WindowEvent e) {
     714                    if (initialFocusOnKey) {
     715                        keys.requestFocus();
     716                    } else {
     717                        values.requestFocus();
     718                    }
     719                }
     720            });
     721        }
     722
     723        @Override
     724        public void autoCompPerformed(AutoCompEvent e) {
     725        }
     726
     727        @Override
     728        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
     729        }
     730
     731        @Override
     732        public void popupMenuCanceled(PopupMenuEvent e) {
     733        }
     734
     735        @Override
     736        public void focusGained(FocusEvent e) {
     737        }
     738
     739        @Override
     740        public void focusLost(FocusEvent e) {
     741            // update the values combobox orientation if the key changed
     742            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
     743        }
     744
     745        protected void updateOkButtonIcon() {
    715746            if (buttons.isEmpty()) {
    716747                return;
     
    746777    protected class AddTagsDialog extends AbstractTagsDialog {
    747778        private final List<JosmAction> recentTagsActions = new ArrayList<>();
    748         protected final transient FocusAdapter focus;
    749779        private final JPanel mainPanel;
    750780        private JPanel recentTagsPanel;
     
    752782        // Counter of added commands for possible undo
    753783        private int commandCount;
     784        private final transient AutoCompletionManager autocomplete;
    754785
    755786        protected AddTagsDialog() {
     
    759790            configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */);
    760791
    761             mainPanel = new JPanel(new GridBagLayout());
    762             keys = new AutoCompComboBox<>();
    763             values = new AutoCompComboBox<>();
    764             keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
    765             values.getModel().setComparator(Comparator.naturalOrder());
    766             keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
    767             values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
    768             keys.setAutocompleteEnabled(AUTOCOMPLETE_KEYS.get());
    769             values.setAutocompleteEnabled(AUTOCOMPLETE_VALUES.get());
    770 
     792            mainPanel = new JPanel(new GridBagLayout()) {
     793                /**
     794                 * This hack allows the comboboxes to have their own orientation.
     795                 *
     796                 * The problem is that
     797                 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
     798                 * {@code applyComponentOrientation} very late in the dialog construction process
     799                 * thus overwriting the orientation the components have chosen for themselves.
     800                 *
     801                 * This stops the propagation of {@code applyComponentOrientation}, thus all
     802                 * components may (and have to) set their own orientation.
     803                 */
     804                @Override
     805                public void applyComponentOrientation(ComponentOrientation o) {
     806                    setComponentOrientation(o);
     807                }
     808            };
    771809            mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.",
    772810                "This will change up to {0} objects.", sel.size(), sel.size())
    773811                +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
    774812
     813            keys = new AutoCompComboBox<>();
     814            keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
     815            keys.setEditable(true);
     816            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
     817            keys.setAutocompleteEnabled(AUTOCOMPLETE_KEYS.get());
     818
     819            mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
     820            mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol());
     821
     822            values = new AutoCompComboBox<>();
     823            values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
     824            values.setEditable(true);
     825            values.getModel().setComparator(Comparator.naturalOrder());
     826            values.setAutocompleteEnabled(AUTOCOMPLETE_VALUES.get());
     827
     828            mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
     829
    775830            cacheRecentTags();
    776             AutoCompletionManager autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
     831            autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
    777832            List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR);
    778833
     
    780835            keyList.removeIf(item -> containsDataKey(item.getValue()));
    781836
    782             keys.getModel().removeAllElements();
    783837            keys.getModel().addAllElements(keyList);
    784             keys.setEditable(true);
    785 
    786             mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
    787 
    788             mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol());
    789             values.setEditable(true);
    790             mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
     838
     839            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
    791840
    792841            // pre-fill first recent tag for which the key is not already present
     
    795844                    .findFirst()
    796845                    .ifPresent(tag -> {
    797                         keys.setSelectedItem(tag.getKey());
    798                         values.setSelectedItem(tag.getValue());
     846                        keys.setSelectedItemText(tag.getKey());
     847                        values.setSelectedItemText(tag.getValue());
    799848                    });
    800849
    801             focus = addFocusAdapter(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
    802             // fire focus event in advance or otherwise the popup list will be too small at first
    803             focus.focusGained(new FocusEvent(this, FocusEvent.FOCUS_GAINED));
    804 
    805             addUpdateIconListener();
     850
     851            keys.addActionListener(ignore -> updateOkButtonIcon());
     852            values.addActionListener(ignore -> updateOkButtonIcon());
    806853
    807854            // Add tag on Shift-Enter
     
    813860                        performTagAdding();
    814861                        refreshRecentTags();
    815                         selectKeysComboBox();
     862                        keys.requestFocus();
    816863                    }
    817864                });
     
    820867
    821868            mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill());
     869            mainPanel.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
     870
    822871            setContent(mainPanel, false);
    823872
    824             selectKeysComboBox();
     873            addEventListeners();
    825874
    826875            popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) {
     
    847896            rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get());
    848897            popupMenu.add(rememberLastTags);
     898        }
     899
     900        @Override
     901        public void autoCompBefore(AutoCompEvent e) {
     902            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
     903        }
     904
     905        @Override
     906        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
     907            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
    849908        }
    850909
     
    9721031                    @Override
    9731032                    public void actionPerformed(ActionEvent e) {
    974                         keys.setSelectedItem(t.getKey());
     1033                        keys.setSelectedItemText(t.getKey());
    9751034                        // fix #7951, #8298 - update list of values before setting value (?)
    976                         focus.focusGained(new FocusEvent(AddTagsDialog.this, FocusEvent.FOCUS_GAINED));
    977                         values.setSelectedItem(t.getValue());
    978                         selectValuesCombobox();
     1035                        updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
     1036                        values.setSelectedItemText(t.getValue());
     1037                        values.requestFocus();
    9791038                    }
    9801039                };
     
    9891048                        performTagAdding();
    9901049                        refreshRecentTags();
    991                         selectKeysComboBox();
     1050                        keys.requestFocus();
    9921051                    }
    9931052                };
     
    10371096                                performTagAdding();
    10381097                                refreshRecentTags();
    1039                                 selectKeysComboBox();
     1098                                keys.requestFocus();
    10401099                            } else if (e.getClickCount() > 1) {
    10411100                                // add tags and close window on double-click
  • trunk/src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java

    r18174 r18221  
    108108
    109109        hcbUploadComment.setToolTipText(tr("Enter an upload comment"));
    110         hcbUploadComment.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
     110        hcbUploadComment.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
    111111        JTextField editor = hcbUploadComment.getEditorComponent();
    112112        editor.getDocument().putProperty("tag", "comment");
     
    147147
    148148        hcbUploadSource.setToolTipText(tr("Enter a source"));
    149         hcbUploadSource.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
     149        hcbUploadSource.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
    150150        JTextField editor = hcbUploadSource.getEditorComponent();
    151151        editor.getDocument().putProperty("tag", "source");
  • trunk/src/org/openstreetmap/josm/gui/io/OpenChangesetComboBoxModel.java

    r18208 r18221  
    55import java.util.List;
    66
    7 import javax.swing.DefaultComboBoxModel;
    8 
    97import org.openstreetmap.josm.data.osm.Changeset;
    108import org.openstreetmap.josm.data.osm.ChangesetCache;
     
    1210import org.openstreetmap.josm.data.osm.ChangesetCacheListener;
    1311import org.openstreetmap.josm.gui.util.GuiHelper;
     12import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    1413import org.openstreetmap.josm.tools.Utils;
    1514
     
    1918 *
    2019 */
    21 public class OpenChangesetComboBoxModel extends DefaultComboBoxModel<Changeset> implements ChangesetCacheListener {
     20public class OpenChangesetComboBoxModel extends JosmComboBoxModel<Changeset> implements ChangesetCacheListener {
    2221    private final transient List<Changeset> changesets;
    2322    private transient Changeset selectedChangeset;
     
    7978
    8079    @Override
    81     public int getIndexOf(Object anObject) {
     80    public int getIndexOf(Changeset anObject) {
    8281        return changesets.indexOf(anObject);
    8382    }
  • trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java

    r18196 r18221  
    4040import javax.swing.JPanel;
    4141import javax.swing.JSeparator;
    42 import javax.swing.MutableComboBoxModel;
    4342import javax.swing.SwingConstants;
    4443import javax.swing.event.ChangeEvent;
     
    7170import org.openstreetmap.josm.gui.layer.gpx.GpxDataHelper;
    7271import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     72import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    7373import org.openstreetmap.josm.gui.widgets.JosmTextField;
    7474import org.openstreetmap.josm.spi.preferences.Config;
     
    8686public class CorrelateGpxWithImages extends AbstractAction implements ExpertModeChangeListener, Destroyable {
    8787
    88     private static MutableComboBoxModel<GpxDataWrapper> gpxModel;
     88    private static JosmComboBoxModel<GpxDataWrapper> gpxModel;
    8989    private static boolean forceTags;
    9090
     
    424424     */
    425425    private void constructGpxModel(NoGpxDataWrapper nogdw) {
    426         gpxModel = new DefaultComboBoxModel<>();
     426        gpxModel = new JosmComboBoxModel<>();
    427427        GpxDataWrapper defaultItem = null;
    428428        for (AbstractModifiableLayer cur : MainApplication.getLayerManager().getLayersOfType(AbstractModifiableLayer.class)) {
  • trunk/src/org/openstreetmap/josm/gui/preferences/display/LanguagePreference.java

    r17648 r18221  
    1212
    1313import javax.swing.Box;
    14 import javax.swing.DefaultComboBoxModel;
    1514import javax.swing.DefaultListCellRenderer;
    1615import javax.swing.JLabel;
     
    2524import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
    2625import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     26import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    2727import org.openstreetmap.josm.spi.preferences.Config;
    2828import org.openstreetmap.josm.tools.GBC;
     
    8282    }
    8383
    84     private static class LanguageComboBoxModel extends DefaultComboBoxModel<Locale> {
     84    private static class LanguageComboBoxModel extends JosmComboBoxModel<Locale> {
    8585        private final List<Locale> data = new ArrayList<>();
    8686
  • trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java

    r18193 r18221  
    22package org.openstreetmap.josm.gui.tagging.ac;
    33
    4 import java.awt.datatransfer.Clipboard;
    5 import java.awt.datatransfer.StringSelection;
    6 import java.awt.datatransfer.Transferable;
    7 import java.awt.event.FocusEvent;
    8 import java.awt.event.FocusListener;
    9 import java.awt.event.KeyEvent;
    10 import java.awt.event.KeyListener;
    114import java.awt.im.InputContext;
    125import java.util.Collection;
     
    147import java.util.LinkedList;
    158import java.util.Locale;
    16 import java.util.Objects;
    17 import java.util.regex.Pattern;
    18 
    19 import javax.swing.JTextField;
    20 import javax.swing.SwingUtilities;
    21 import javax.swing.text.AbstractDocument;
    22 import javax.swing.text.AttributeSet;
    23 import javax.swing.text.BadLocationException;
    24 import javax.swing.text.DocumentFilter;
    25 import javax.swing.text.JTextComponent;
    26 import javax.swing.text.StyleConstants;
    27 
    28 import org.openstreetmap.josm.gui.MainApplication;
    29 import org.openstreetmap.josm.gui.MapFrame;
    30 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
     9
     10import javax.swing.ComboBoxEditor;
     11
    3112import org.openstreetmap.josm.gui.widgets.JosmComboBox;
    32 import org.openstreetmap.josm.spi.preferences.Config;
    3313import org.openstreetmap.josm.tools.Logging;
    3414
     
    4525 * @since 18173
    4626 */
    47 public class AutoCompComboBox<E> extends JosmComboBox<E> implements KeyListener {
    48 
    49     /** a regex that matches numbers */
    50     private static final Pattern IS_NUMBER = Pattern.compile("^\\d+$");
    51     /** true if the combobox should autocomplete */
    52     private boolean autocompleteEnabled = true;
    53     /** the editor will not accept text longer than this. -1 to disable */
    54     private int maxTextLength = -1;
     27public class AutoCompComboBox<E> extends JosmComboBox<E> implements AutoCompListener {
     28
    5529    /** force a different keyboard input locale for the editor */
    5630    private boolean useFixedLocale;
    57 
    58     /** Whether to autocomplete numbers */
    59     private final boolean AUTOCOMPLETE_NUMBERS = !Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true);
    60 
    6131    private final transient InputContext privateInputContext = InputContext.getInstance();
    62 
    63     static final class InnerFocusListener implements FocusListener {
    64         private final JTextComponent editorComponent;
    65 
    66         InnerFocusListener(JTextComponent editorComponent) {
    67             this.editorComponent = editorComponent;
    68         }
    69 
    70         @Override
    71         public void focusLost(FocusEvent e) {
    72             MapFrame map = MainApplication.getMap();
    73             if (map != null) {
    74                 map.keyDetector.setEnabled(true);
    75             }
    76         }
    77 
    78         @Override
    79         public void focusGained(FocusEvent e) {
    80             MapFrame map = MainApplication.getMap();
    81             if (map != null) {
    82                 map.keyDetector.setEnabled(false);
    83             }
    84             // save unix system selection (middle mouse paste)
    85             Clipboard sysSel = ClipboardUtils.getSystemSelection();
    86             if (sysSel != null) {
    87                 Transferable old = ClipboardUtils.getClipboardContent(sysSel);
    88                 editorComponent.selectAll();
    89                 if (old != null) {
    90                     sysSel.setContents(old, null);
    91                 }
    92             } else if (e != null && e.getOppositeComponent() != null) {
    93                 // Select all characters when the change of focus occurs inside JOSM only.
    94                 // When switching from another application, it is annoying, see #13747
    95                 editorComponent.selectAll();
    96             }
    97         }
    98     }
    99 
    100     /**
    101      * A {@link DocumentFilter} to limit the text length in the editor.
    102      */
    103     private class MaxLengthDocumentFilter extends DocumentFilter {
    104         @Override
    105         public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
    106                 throws BadLocationException {
    107             if (mustInsertOrReplace(fb, 0, string, attr)) {
    108                 super.insertString(fb, offset, string, attr);
    109             }
    110         }
    111 
    112         @Override
    113         public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attr)
    114                 throws BadLocationException {
    115             if (mustInsertOrReplace(fb, length, string, attr)) {
    116                 super.replace(fb, offset, length, string, attr);
    117             }
    118         }
    119 
    120         private boolean mustInsertOrReplace(FilterBypass fb, int length, String string, AttributeSet attr) {
    121             int newLen = fb.getDocument().getLength() - length + ((string == null) ? 0 : string.length());
    122             return (maxTextLength == -1 || newLen <= maxTextLength ||
    123                     // allow longer text while composing characters or it will be hard to compose
    124                     // the last characters before the limit
    125                     ((attr != null) && attr.isDefined(StyleConstants.ComposedTextAttribute)));
    126         }
    127     }
    12832
    12933    /**
     
    14145    public AutoCompComboBox(AutoCompComboBoxModel<E> model) {
    14246        super(model);
    143         Objects.requireNonNull(model, "A model cannot be null.");
     47        setEditor(new AutoCompComboBoxEditor<E>());
    14448        setEditable(true);
    145         final JTextComponent editorComponent = getEditorComponent();
    146         editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
    147         editorComponent.addKeyListener(this);
    148         ((AbstractDocument) editorComponent.getDocument()).setDocumentFilter(new MaxLengthDocumentFilter());
     49        getEditorComponent().setModel(model);
     50        getEditorComponent().addAutoCompListener(this);
    14951    }
    15052
     
    15254     * Returns the {@link AutoCompComboBoxModel} currently used.
    15355     *
    154      * @return the model
     56     * @return the model or null
    15557     */
    15658    @Override
     
    15961    }
    16062
    161     /**
    162      * Autocompletes what the user typed in.
    163      * <p>
    164      * Gets the user input from the editor, finds the best matching item in the model, selects it in
    165      * the list, sets the editor text, and highlights the autocompleted part. If there is no
    166      * matching item, removes the list selection.
    167      */
    168     private void autocomplete() {
    169         JTextField editor = getEditorComponent();
    170         String prefix = editor.getText();
    171         if (!AUTOCOMPLETE_NUMBERS && IS_NUMBER.matcher(prefix).matches())
    172             return;
    173 
    174         E item = getModel().findBestCandidate(prefix);
    175         if (item != null) {
    176             String text = item.toString();
    177             // This calls setItem() if the selected item changed
    178             // See: javax.swing.plaf.basic.BasicComboBoxUI.Handler.contentsChanged(ListDataEvent e)
    179             setSelectedItem(item);
    180             // set manually in case the selected item didn't change
    181             editor.setText(text);
    182             // select the autocompleted suffix in the editor
    183             editor.select(prefix.length(), text.length());
    184             // copy the whole autocompleted string to the unix system-wide selection (aka
    185             // middle-click), else only the selected suffix would be copied
    186             copyToSysSel(text);
    187         } else {
    188             setSelectedItem(null);
    189             // avoid setItem because it selects the whole text (on windows only)
    190             editor.setText(prefix);
    191         }
    192     }
    193 
    194     /**
    195      * Copies a String to the UNIX system-wide selection (aka middle-click).
    196      *
    197      * @param s the string to copy
    198      */
    199     void copyToSysSel(String s) {
    200         Clipboard sysSel = ClipboardUtils.getSystemSelection();
    201         if (sysSel != null) {
    202             Transferable transferable = new StringSelection(s);
    203             sysSel.setContents(transferable, null);
    204         }
    205     }
    206 
    207     /**
    208      * Sets the maximum text length.
    209      *
    210      * @param length the maximum text length in number of characters
    211      */
    212     public void setMaxTextLength(int length) {
    213         maxTextLength = length;
     63    @Override
     64    public void setEditor(ComboBoxEditor newEditor) {
     65        if (editor != null) {
     66            editor.getEditorComponent().removePropertyChangeListener(this);
     67        }
     68        super.setEditor(newEditor);
     69        if (editor != null) {
     70            // listen to orientation changes in the editor
     71            editor.getEditorComponent().addPropertyChangeListener(this);
     72        }
     73    }
     74
     75    /**
     76     * Returns the editor component
     77     *
     78     * @return the editor component
     79     * @see ComboBoxEditor#getEditorComponent()
     80     * @since 18221
     81     */
     82    @Override
     83    @SuppressWarnings("unchecked")
     84    public AutoCompTextField<E> getEditorComponent() {
     85        return getEditor() == null ? null : (AutoCompTextField<E>) getEditor().getEditorComponent();
     86    }
     87
     88    /**
     89     * Selects the autocompleted item in the dropdown.
     90     *
     91     * @param item the item selected for autocomplete
     92     */
     93    private void autocomplete(Object item) {
     94        // Save the text in case item is null, because setSelectedItem will erase it.
     95        String savedText = getText();
     96        setSelectedItem(item);
     97        setText(savedText);
    21498    }
    21599
     
    263147
    264148    /**
    265      * Returns {@code true} if autocompletion is enabled.
    266      *
    267      * @return {@code true} if autocompletion is enabled.
    268      */
    269     public final boolean isAutocompleteEnabled() {
    270         return autocompleteEnabled;
    271     }
    272 
    273     /**
    274149     * Enables or disables the autocompletion.
    275150     *
     
    279154     */
    280155    public boolean setAutocompleteEnabled(boolean enabled) {
    281         boolean oldEnabled = this.autocompleteEnabled;
    282         this.autocompleteEnabled = enabled;
    283         return oldEnabled;
     156        return getEditorComponent().setAutocompleteEnabled(enabled);
    284157    }
    285158
     
    319192    }
    320193
    321     /*
    322      * The KeyListener interface
    323      */
    324 
    325     /**
    326      * Listens to key events and eventually schedules an autocomplete.
    327      *
    328      * @param e the key event
    329      */
    330     @Override
    331     public void keyTyped(KeyEvent e) {
    332         if (autocompleteEnabled
    333                 // and selection is at the end
    334                 && getEditorComponent().getSelectionEnd() == getEditorComponent().getText().length()
    335                 // and something visible was typed
    336                 && !Character.isISOControl(e.getKeyChar())) {
    337             // We got the event before the editor component could see it. Let the editor do its job first.
    338             SwingUtilities.invokeLater(() -> autocomplete());
    339         }
    340     }
    341 
    342     @Override
    343     public void keyPressed(KeyEvent e) {
    344     }
    345 
    346     @Override
    347     public void keyReleased(KeyEvent e) {
     194    /** AutoCompListener Interface */
     195
     196    @Override
     197    public void autoCompBefore(AutoCompEvent e) {
     198    }
     199
     200    @Override
     201    public void autoCompPerformed(AutoCompEvent e) {
     202        autocomplete(e.getItem());
    348203    }
    349204}
  • trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java

    r18173 r18221  
    22package org.openstreetmap.josm.gui.tagging.ac;
    33
    4 import java.util.ArrayList;
    5 import java.util.Collection;
    64import java.util.Comparator;
    7 import java.util.Iterator;
    8 import java.util.List;
    95import java.util.Objects;
    10 import java.util.function.Function;
    116
    12 import javax.swing.AbstractListModel;
    13 import javax.swing.MutableComboBoxModel;
    14 
    15 import org.openstreetmap.josm.data.preferences.ListProperty;
    16 import org.openstreetmap.josm.spi.preferences.Config;
     7import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
    178
    189/**
     
    2314 * @since 18173
    2415 */
    25 public class AutoCompComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> {
     16public class AutoCompComboBoxModel<E> extends JosmComboBoxModel<E> {
    2617
    2718    /**
     
    3324     */
    3425    private Comparator<E> comparator;
    35     /** The maximum number of elements to hold, -1 for no limit. Used for histories. */
    36     private int maxSize = -1;
    37 
    38     /** the elements shown in the dropdown */
    39     protected ArrayList<E> elements = new ArrayList<>();
    40     /** the selected element in the dropdown or null */
    41     protected Object selected;
    4226
    4327    /**
     
    7458
    7559    /**
    76      * Sets the maximum number of elements.
    77      *
    78      * @param size The maximal number of elements in the model.
    79      */
    80     public void setSize(int size) {
    81         maxSize = size;
    82     }
    83 
    84     /**
    85      * Returns a copy of the element list.
    86      * @return a copy of the data
    87      */
    88     public Collection<E> asCollection() {
    89         return new ArrayList<>(elements);
    90     }
    91 
    92     //
    93     // interface java.lang.Iterable
    94     //
    95 
    96     @Override
    97     public Iterator<E> iterator() {
    98         return elements.iterator();
    99     }
    100 
    101     //
    102     // interface javax.swing.MutableComboBoxModel
    103     //
    104 
    105     /**
    106      * Adds an element to the end of the model. Does nothing if max size is already reached.
    107      */
    108     @Override
    109     public void addElement(E element) {
    110         if (element != null && (maxSize == -1 || getSize() < maxSize)) {
    111             elements.add(element);
    112         }
    113     }
    114 
    115     @Override
    116     public void removeElement(Object elem) {
    117         elements.remove(elem);
    118     }
    119 
    120     @Override
    121     public void removeElementAt(int index) {
    122         Object elem = getElementAt(index);
    123         if (elem == selected) {
    124             if (index == 0) {
    125                 setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1));
    126             } else {
    127                 setSelectedItem(getElementAt(index - 1));
    128             }
    129         }
    130         elements.remove(index);
    131         fireIntervalRemoved(this, index, index);
    132     }
    133 
    134     /**
    135      * Adds an element at a specific index.
    136      *
    137      * @param element The element to add
    138      * @param index Location to add the element
    139      */
    140     @Override
    141     public void insertElementAt(E element, int index) {
    142         if (maxSize != -1 && maxSize <= getSize()) {
    143             removeElementAt(getSize() - 1);
    144         }
    145         elements.add(index, element);
    146     }
    147 
    148     //
    149     // javax.swing.ComboBoxModel
    150     //
    151 
    152     /**
    153      * Set the value of the selected item. The selected item may be null.
    154      *
    155      * @param elem The combo box value or null for no selection.
    156      */
    157     @Override
    158     public void setSelectedItem(Object elem) {
    159         if ((selected != null && !selected.equals(elem)) ||
    160             (selected == null && elem != null)) {
    161             selected = elem;
    162             fireContentsChanged(this, -1, -1);
    163         }
    164     }
    165 
    166     @Override
    167     public Object getSelectedItem() {
    168         return selected;
    169     }
    170 
    171     //
    172     // javax.swing.ListModel
    173     //
    174 
    175     @Override
    176     public int getSize() {
    177         return elements.size();
    178     }
    179 
    180     @Override
    181     public E getElementAt(int index) {
    182         if (index >= 0 && index < elements.size())
    183             return elements.get(index);
    184         else
    185             return null;
    186     }
    187 
    188     //
    189     // end interfaces
    190     //
    191 
    192     /**
    193      * Adds all elements from the collection.
    194      *
    195      * @param elems The elements to add.
    196      */
    197     public void addAllElements(Collection<E> elems) {
    198         elems.forEach(e -> addElement(e));
    199     }
    200 
    201     /**
    202      * Adds all elements from the collection of string representations.
    203      *
    204      * @param strings The string representation of the elements to add.
    205      * @param buildE A {@link java.util.function.Function} that builds an {@code <E>} from a
    206      *               {@code String}.
    207      */
    208     public void addAllElements(Collection<String> strings, Function<String, E> buildE) {
    209         strings.forEach(s -> addElement(buildE.apply(s)));
    210     }
    211 
    212     /**
    213      * Adds an element to the top of the list.
    214      * <p>
    215      * If the element is already in the model, moves it to the top.  If the model gets too big,
    216      * deletes the last element.
    217      *
    218      * @param newElement the element to add
    219      * @return The element that is at the top now.
    220      */
    221     public E addTopElement(E newElement) {
    222         // if the element is already at the top, do nothing
    223         if (newElement.equals(getElementAt(0)))
    224             return getElementAt(0);
    225 
    226         removeElement(newElement);
    227         insertElementAt(newElement, 0);
    228         return newElement;
    229     }
    230 
    231     /**
    232      * Empties the list.
    233      */
    234     public void removeAllElements() {
    235         if (!elements.isEmpty()) {
    236             int firstIndex = 0;
    237             int lastIndex = elements.size() - 1;
    238             elements.clear();
    239             selected = null;
    240             fireIntervalRemoved(this, firstIndex, lastIndex);
    241         } else {
    242             selected = null;
    243         }
    244     }
    245 
    246     /**
    24760     * Finds the best candidate for autocompletion.
    24861     * <p>
     
    26376            .orElse(null);
    26477    }
    265 
    266     /**
    267      * Gets a preference loader and saver.
    268      *
    269      * @param readE A {@link Function} that builds an {@code <E>} from a {@link String}.
    270      * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
    271      * @return The {@link Preferences} instance.
    272      */
    273     public Preferences prefs(Function<String, E> readE, Function<E, String> writeE) {
    274         return new Preferences(readE, writeE);
    275     }
    276 
    277     /**
    278      * Loads and saves the model to the JOSM preferences.
    279      * <p>
    280      * Obtainable through {@link #prefs}.
    281      */
    282     public final class Preferences {
    283 
    284         /** A {@link Function} that builds an {@code <E>} from a {@code String}. */
    285         private Function<String, E> readE;
    286         /** A {@code Function} that serializes {@code <E>} to a {@code String}. */
    287         private Function<E, String> writeE;
    288 
    289         /**
    290          * Private constructor
    291          *
    292          * @param readE A {@link Function} that builds an {@code <E>} from a {@code String}.
    293          * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
    294          */
    295         private Preferences(Function<String, E> readE, Function<E, String> writeE) {
    296             this.readE = readE;
    297             this.writeE = writeE;
    298         }
    299 
    300         /**
    301          * Loads the model from the JOSM preferences.
    302          * @param key The preferences key
    303          */
    304         public void load(String key) {
    305             removeAllElements();
    306             addAllElements(Config.getPref().getList(key), readE);
    307         }
    308 
    309         /**
    310          * Loads the model from the JOSM preferences.
    311          *
    312          * @param key The preferences key
    313          * @param defaults A list of default values.
    314          */
    315         public void load(String key, List<String> defaults) {
    316             removeAllElements();
    317             addAllElements(Config.getPref().getList(key, defaults), readE);
    318         }
    319 
    320         /**
    321          * Loads the model from the JOSM preferences.
    322          *
    323          * @param prop The property holding the strings.
    324          */
    325         public void load(ListProperty prop) {
    326             removeAllElements();
    327             addAllElements(prop.get(), readE);
    328         }
    329 
    330         /**
    331          * Returns the model elements as list of strings.
    332          *
    333          * @return a list of strings
    334          */
    335         public List<String> asStringList() {
    336             List<String> list = new ArrayList<>(getSize());
    337             forEach(element -> list.add(writeE.apply(element)));
    338             return list;
    339         }
    340 
    341         /**
    342          * Saves the model to the JOSM preferences.
    343          *
    344         * @param key The preferences key
    345         */
    346         public void save(String key) {
    347             Config.getPref().putList(key, asStringList());
    348         }
    349 
    350         /**
    351          * Saves the model to the JOSM preferences.
    352          *
    353          * @param prop The property to write to.
    354          */
    355         public void save(ListProperty prop) {
    356             prop.put(asStringList());
    357         }
    358     }
    35978}
  • trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java

    r18211 r18221  
    346346
    347347    /**
     348     * Returns all cached {@link AutoCompletionItem}s for given keys.
     349     *
     350     * @param keys retrieve the items for these keys
     351     * @return the currently cached items, sorted by priority and alphabet
     352     * @since 18221
     353     */
     354    public List<AutoCompletionItem> getAllForKeys(List<String> keys) {
     355        Map<String, AutoCompletionPriority> map = new HashMap<>();
     356
     357        for (String key : keys) {
     358            for (String value : TaggingPresets.getPresetValues(key)) {
     359                map.merge(value, AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
     360            }
     361            for (String value : getDataValues(key)) {
     362                map.merge(value, AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith);
     363            }
     364            for (String value : getUserInputValues(key)) {
     365                map.merge(value, AutoCompletionPriority.UNKNOWN, AutoCompletionPriority::mergeWith);
     366            }
     367        }
     368        return map.entrySet().stream().map(e -> new AutoCompletionItem(e.getKey(), e.getValue())).sorted().collect(Collectors.toList());
     369    }
     370
     371    /**
    348372     * Returns the currently cached tag keys.
    349373     * @return a set of tag keys
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java

    r18208 r18221  
    77
    88import java.awt.Component;
     9import java.awt.ComponentOrientation;
    910import java.awt.Dimension;
    1011import java.awt.GridBagLayout;
     
    1617import java.util.EnumSet;
    1718import java.util.LinkedHashSet;
    18 import java.util.LinkedList;
    1919import java.util.List;
    2020import java.util.Map;
     
    9191public class TaggingPreset extends AbstractAction implements ActiveLayerChangeListener, AdaptableAction, Predicate<IPrimitive> {
    9292
     93    /** The user pressed the "Apply" button */
    9394    public static final int DIALOG_ANSWER_APPLY = 1;
     95    /** The user pressed the "New Relation" button */
    9496    public static final int DIALOG_ANSWER_NEW_RELATION = 2;
     97    /** The user pressed the "Cancel" button */
    9598    public static final int DIALOG_ANSWER_CANCEL = 3;
    9699
     100    /** The action key for optional tooltips */
    97101    public static final String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text";
    98102
     
    120124     */
    121125    public String iconName;
     126    /**
     127     * Translation context for name
     128     */
    122129    public String name_context;
    123130    /**
     
    126133     */
    127134    public String locale_name;
     135    /**
     136     * Show the preset name if true
     137     */
    128138    public boolean preset_name_label;
    129139
     
    132142     */
    133143    public transient Set<TaggingPresetType> types;
     144    /**
     145     * The list of preset items
     146     */
    134147    public final transient List<TaggingPresetItem> data = new ArrayList<>(2);
     148    /**
     149     * The roles for this relation (if we are editing a relation). See:
     150     * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Tags">JOSM wiki</a>
     151     */
    135152    public transient Roles roles;
     153    /**
     154     * The name_template custom name formatter. See:
     155     * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a>
     156     */
    136157    public transient TemplateEntry nameTemplate;
     158    /** The name_template_filter */
    137159    public transient Match nameTemplateFilter;
     160    /** The match_expression */
    138161    public transient Match matchExpression;
    139162
     
    145168    /** The completable future task of asynchronous icon loading */
    146169    private CompletableFuture<Void> iconFuture;
     170
     171    /** Support functions */
     172    protected TaggingPresetItemGuiSupport itemGuiSupport;
    147173
    148174    /**
     
    277303    }
    278304
    279     public void setName_template(String pattern) throws SAXException {
     305    /**
     306     * Sets the name_template custom name formatter.
     307     *
     308     * @param template The format template
     309     * @throws SAXException on template parse error
     310     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a>
     311     */
     312    public void setName_template(String template) throws SAXException {
    280313        try {
    281             this.nameTemplate = new TemplateParser(pattern).parse();
     314            this.nameTemplate = new TemplateParser(template).parse();
    282315        } catch (ParseError e) {
    283             Logging.error("Error while parsing " + pattern + ": " + e.getMessage());
     316            Logging.error("Error while parsing " + template + ": " + e.getMessage());
    284317            throw new SAXException(e);
    285318        }
    286319    }
    287320
     321    /**
     322     * Sets the name_template_filter.
     323     *
     324     * @param filter The search pattern
     325     * @throws SAXException on search patern parse error
     326     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a>
     327     */
    288328    public void setName_template_filter(String filter) throws SAXException {
    289329        try {
     
    295335    }
    296336
     337    /**
     338     * Sets the match_expression additional criteria for matching primitives.
     339     *
     340     * @param filter The search pattern
     341     * @throws SAXException on search patern parse error
     342     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a>
     343     */
    297344    public void setMatch_expression(String filter) throws SAXException {
    298345        try {
     
    321368    public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
    322369        PresetPanel p = new PresetPanel();
    323         List<Link> l = new LinkedList<>();
    324370
    325371        final JPanel pp = new JPanel();
     
    355401
    356402        boolean presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(this);
    357         final TaggingPresetItemGuiSupport itemGuiSupport = TaggingPresetItemGuiSupport.create(
    358                 presetInitiallyMatches, selected, this::getChangedTags);
    359         JPanel items = new JPanel(new GridBagLayout());
     403        itemGuiSupport = TaggingPresetItemGuiSupport.create(presetInitiallyMatches, selected, this::getChangedTags);
     404
     405        JPanel itemPanel = new JPanel(new GridBagLayout()) {
     406            /**
     407             * This hack allows the items to have their own orientation.
     408             *
     409             * The problem is that
     410             * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
     411             * {@code applyComponentOrientation} very late in the dialog construction process thus
     412             * overwriting the orientation the components have chosen for themselves.
     413             *
     414             * This stops the propagation of {@code applyComponentOrientation}, thus all
     415             * {@code TaggingPresetItem}s may (and have to) set their own orientation.
     416             */
     417            @Override
     418            public void applyComponentOrientation(ComponentOrientation o) {
     419                setComponentOrientation(o);
     420            }
     421        };
     422        JPanel linkPanel = new JPanel(new GridBagLayout());
    360423        TaggingPresetItem previous = null;
    361424        for (TaggingPresetItem i : data) {
    362425            if (i instanceof Link) {
    363                 l.add((Link) i);
     426                i.addToPanel(linkPanel, itemGuiSupport);
    364427                p.hasElements = true;
    365428            } else {
     
    367430                    PresetLink link = (PresetLink) i;
    368431                    if (!(previous instanceof PresetLink && Objects.equals(((PresetLink) previous).text, link.text))) {
    369                         items.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
     432                        itemPanel.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
    370433                    }
    371434                }
    372                 if (i.addToPanel(items, itemGuiSupport)) {
     435                if (i.addToPanel(itemPanel, itemGuiSupport)) {
    373436                    p.hasElements = true;
    374437                }
     
    376439            previous = i;
    377440        }
    378         p.add(items, GBC.eol().fill());
     441        p.add(itemPanel, GBC.eol().fill());
     442        p.add(linkPanel, GBC.eol().fill());
     443
    379444        if (selected.isEmpty() && !supportsRelation()) {
    380             GuiHelper.setEnabledRec(items, false);
     445            GuiHelper.setEnabledRec(itemPanel, false);
    381446        }
    382447
     
    384449            itemGuiSupport.addListener((source, key, newValue) ->
    385450                    TaggingPresetValidation.validateAsync(selected.iterator().next(), validationLabel, getChangedTags()));
    386         }
    387 
    388         // add Link
    389         for (Link link : l) {
    390             link.addToPanel(p, itemGuiSupport);
    391451        }
    392452
     
    396456        p.add(tb, GBC.std(1, 0).anchor(GBC.LINE_END));
    397457
    398         // Trigger initial updates
     458        // Trigger initial updates once and only once
     459        itemGuiSupport.setEnabled(true);
    399460        itemGuiSupport.fireItemValueModified(null, null, null);
     461
    400462        return p;
    401463    }
     
    410472    }
    411473
     474    /**
     475     * Suggests a relation role for this primitive
     476     *
     477     * @param osm The primitive
     478     * @return the suggested role or null
     479     */
    412480    public String suggestRoleForOsmPrimitive(OsmPrimitive osm) {
    413481        if (roles != null && osm != null) {
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java

    r18208 r18221  
    88import java.util.Arrays;
    99import java.util.Collection;
     10import java.util.Collections;
    1011import java.util.EnumSet;
    1112import java.util.List;
     
    2122import org.openstreetmap.josm.data.osm.Tag;
    2223import org.openstreetmap.josm.data.preferences.BooleanProperty;
     24import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
    2325import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
    2426import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
     
    5557        AutoCompletionManager.of(data).populateWithTagValues(list, keys);
    5658        field.setAutoCompletionList(list);
     59    }
     60
     61    /**
     62     * Returns all cached {@link AutoCompletionItem}s for given keys.
     63     *
     64     * @param keys retrieve the items for these keys
     65     * @return the currently cached items, sorted by priority and alphabet
     66     * @since 18221
     67     */
     68    protected List<AutoCompletionItem> getAllForKeys(List<String> keys) {
     69        DataSet data = OsmDataManager.getInstance().getEditDataSet();
     70        if (data == null) {
     71            return Collections.emptyList();
     72        }
     73        return AutoCompletionManager.of(data).getAllForKeys(keys);
    5774    }
    5875
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java

    r18208 r18221  
    22package org.openstreetmap.josm.gui.tagging.presets;
    33
     4import java.awt.ComponentOrientation;
    45import java.util.Arrays;
    56import java.util.Collection;
     
    1112import org.openstreetmap.josm.data.osm.Tagged;
    1213import org.openstreetmap.josm.data.osm.search.SearchCompiler;
     14import org.openstreetmap.josm.gui.widgets.OrientationAction;
    1315import org.openstreetmap.josm.tools.ListenerList;
    1416import org.openstreetmap.josm.tools.Utils;
     
    2628    private final Supplier<Collection<Tag>> changedTagsSupplier;
    2729    private final ListenerList<ChangeListener> listeners = ListenerList.create();
     30
     31    /** whether to fire events or not */
     32    private boolean enabled = false;
     33
     34    /**
     35     * Returns whether firing of events is enabled
     36     *
     37     * @return true if firing of events is enabled
     38     */
     39    public boolean isEnabled() {
     40        return enabled;
     41    }
     42
     43    /**
     44     * Enables or disables the firing of events
     45     *
     46     * @param enabled fires if true
     47     * @return the old state of enabled
     48     */
     49    public boolean setEnabled(boolean enabled) {
     50        boolean oldEnabled = this.enabled;
     51        this.enabled = enabled;
     52        return oldEnabled;
     53    }
    2854
    2955    /**
     
    120146    }
    121147
     148    /**
     149     * Returns the default component orientation by the user's locale
     150     *
     151     * @return the default component orientation
     152     */
     153    public ComponentOrientation getDefaultComponentOrientation() {
     154        return OrientationAction.getDefaultComponentOrientation();
     155    }
     156
    122157    @Override
    123158    public boolean evaluateCondition(SearchCompiler.Match condition) {
     
    140175     */
    141176    public void fireItemValueModified(TaggingPresetItem source, String key, String newValue) {
    142         listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
     177        if (enabled)
     178            listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
    143179    }
    144180}
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java

    r18077 r18221  
    8686        if (icon != null) {
    8787            JPanel checkPanel = IconTextCheckBox.wrap(check, locale_text, getIcon());
     88            checkPanel.applyComponentOrientation(support.getDefaultComponentOrientation());
    8889            p.add(checkPanel, GBC.eol()); // Do not fill, see #15104
    8990        } else {
     91            check.applyComponentOrientation(support.getDefaultComponentOrientation());
    9092            p.add(check, GBC.eol()); // Do not fill, see #15104
    9193        }
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java

    r17829 r18221  
    4646        }
    4747
     48        panel.applyComponentOrientation(support.getDefaultComponentOrientation());
    4849        p.add(panel, GBC.eol());
    4950        return false;
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java

    r17651 r18221  
    66import java.awt.Color;
    77import java.awt.Cursor;
     8import java.awt.Insets;
    89import java.awt.event.ActionEvent;
    910import java.awt.event.ActionListener;
     11import java.awt.event.ComponentAdapter;
     12import java.awt.event.ComponentEvent;
     13import java.util.Arrays;
     14import java.util.Comparator;
    1015
    1116import javax.swing.AbstractAction;
    1217import javax.swing.JButton;
    1318import javax.swing.JColorChooser;
     19import javax.swing.JComponent;
    1420import javax.swing.JPanel;
    1521
     22import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
    1623import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
    1724import org.openstreetmap.josm.gui.MainApplication;
    1825import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors;
    19 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
    20 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
     26import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
     27import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
     28import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
    2129import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
    2230import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
     
    3644    public boolean editable = true; // NOSONAR
    3745    /** The length of the combo box (number of characters allowed). */
    38     public short length; // NOSONAR
     46    public int length; // NOSONAR
    3947
    4048    protected JosmComboBox<PresetListEntry> combobox;
     49    protected AutoCompComboBoxModel<PresetListEntry> dropDownModel;
     50    protected AutoCompComboBoxModel<AutoCompletionItem> autoCompModel;
     51
     52    class ComponentListener extends ComponentAdapter {
     53        @Override
     54        public void componentResized(ComponentEvent e) {
     55            // Make multi-line JLabels the correct size
     56            // Only needed if there is any short_description
     57            JComponent component = (JComponent) e.getSource();
     58            int width = component.getWidth();
     59            if (width == 0)
     60                width = 200;
     61            Insets insets = component.getInsets();
     62            width -= insets.left + insets.right + 10;
     63            ComboMultiSelectListCellRenderer renderer = (ComboMultiSelectListCellRenderer) combobox.getRenderer();
     64            renderer.setWidth(width);
     65            combobox.setRenderer(null); // needed to make prop change fire
     66            combobox.setRenderer(renderer);
     67        }
     68    }
    4169
    4270    /**
     
    4876
    4977    @Override
    50     protected void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
     78    protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
    5179        if (!usage.unused()) {
    5280            for (String s : usage.values) {
     
    5987        presetListEntries.add(new PresetListEntry(""));
    6088
    61         combobox = new JosmComboBox<>(presetListEntries.toArray(new PresetListEntry[0]));
    62         component = combobox;
    63         combobox.setRenderer(getListCellRenderer());
    64         combobox.setEditable(true); // fix incorrect height, see #6157
    65         combobox.reinitialize(presetListEntries);
    66         combobox.setEditable(editable); // see #6157
    67         AutoCompletingTextField tf = new AutoCompletingTextField();
    68         initAutoCompletionField(tf, key);
     89        dropDownModel = new AutoCompComboBoxModel<PresetListEntry>(Comparator.naturalOrder());
     90        autoCompModel = new AutoCompComboBoxModel<AutoCompletionItem>(Comparator.naturalOrder());
     91        presetListEntries.forEach(dropDownModel::addElement);
     92
     93        combobox = new JosmComboBox<>(dropDownModel);
     94        AutoCompComboBoxEditor<AutoCompletionItem> editor = new AutoCompComboBoxEditor<>();
     95        combobox.setEditor(editor);
     96
     97        // The default behaviour of JComboBox is to size the editor according to the tallest item in
     98        // the dropdown list.  We don't want that to happen because we want to show taller items in
     99        // the list than in the editor.  We can't use
     100        // {@code combobox.setPrototypeDisplayValue(new PresetListEntry(" "));} because that would
     101        // set a fixed cell height in JList.
     102        combobox.setPreferredHeight(combobox.getPreferredSize().height);
     103
     104        // a custom cell renderer capable of displaying a short description text along with the
     105        // value
     106        combobox.setRenderer(new ComboMultiSelectListCellRenderer(combobox, combobox.getRenderer(), 200, key));
     107        combobox.setEditable(editable);
     108
     109        getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement);
     110        getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD)));
     111
     112        AutoCompTextField<AutoCompletionItem> tf = editor.getEditorComponent();
     113        tf.setModel(autoCompModel);
     114
    69115        if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) {
    70             tf.setHint(key);
     116            combobox.setHint(key);
    71117        }
    72118        if (length > 0) {
    73             tf.setMaxChars((int) length);
     119            tf.setMaxTextLength(length);
    74120        }
    75         AutoCompletionList acList = tf.getAutoCompletionList();
    76         if (acList != null) {
    77             acList.add(getDisplayValues(), AutoCompletionPriority.IS_IN_STANDARD);
    78         }
    79         combobox.setEditor(tf);
    80         combobox.setSelectedItem(getItemToSelect(def, support, false));
    81121
    82122        if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) {
     
    93133            p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL));
    94134        }
     135
     136        Object itemToSelect = getItemToSelect(default_, support, false);
     137        combobox.setSelectedItemText(itemToSelect == null ? null : itemToSelect.toString());
    95138        combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
     139        combobox.addComponentListener(new ComponentListener());
     140        return combobox;
    96141    }
    97142
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java

    r17919 r18221  
    66
    77import java.awt.Component;
    8 import java.awt.Dimension;
    9 import java.awt.Font;
    108import java.lang.reflect.Method;
    119import java.lang.reflect.Modifier;
     
    1412import java.util.Collection;
    1513import java.util.Collections;
    16 import java.util.Comparator;
    1714import java.util.List;
    1815import java.util.Objects;
     
    2017import java.util.concurrent.CopyOnWriteArraySet;
    2118import java.util.stream.Collectors;
    22 import java.util.stream.IntStream;
    2319
    2420import javax.swing.JComponent;
     
    3228import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
    3329import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
     30import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
     31import org.openstreetmap.josm.gui.widgets.OrientationAction;
    3432import org.openstreetmap.josm.tools.GBC;
    3533import org.openstreetmap.josm.tools.Logging;
     
    4038 */
    4139public abstract class ComboMultiSelect extends KeyedItem {
    42 
    43     private static final Renderer RENDERER = new Renderer();
    4440
    4541    /**
     
    9389    public boolean values_searchable; // NOSONAR
    9490
    95     protected JComponent component;
    9691    protected final Set<PresetListEntry> presetListEntries = new CopyOnWriteArraySet<>();
    9792    private boolean initialized;
     
    9994    protected Object originalValue;
    10095
    101     private static final class Renderer implements ListCellRenderer<PresetListEntry> {
    102 
    103         private final JLabel lbl = new JLabel();
     96    /**
     97     * A list cell renderer that paints a short text in the current value pane and and a longer text
     98     * in the dropdown list.
     99     */
     100    static class ComboMultiSelectListCellRenderer extends JosmListCellRenderer<PresetListEntry> {
     101        int width;
     102        private String key;
     103
     104        ComboMultiSelectListCellRenderer(Component component, ListCellRenderer<? super PresetListEntry> renderer, int width, String key) {
     105            super(component, renderer);
     106            this.key = key;
     107            setWidth(width);
     108        }
     109
     110        /**
     111         * Sets the width to format the dropdown list to
     112         *
     113         * Note: This is not the width of the list, but the width to which we format any multi-line
     114         * label in the list.  We cannot use the list's width because at the time the combobox
     115         * measures its items, it is not guaranteed that the list is already sized, the combobox may
     116         * not even be layed out yet.  Set this to {@code combobox.getWidth()}
     117         *
     118         * @param width the width
     119         */
     120        public void setWidth(int width) {
     121            if (width <= 0)
     122                width = 200;
     123            this.width = width - 20;
     124        }
    104125
    105126        @Override
    106         public Component getListCellRendererComponent(JList<? extends PresetListEntry> list, PresetListEntry item, int index,
    107                 boolean isSelected, boolean cellHasFocus) {
    108 
    109             if (list == null || item == null) {
    110                 return lbl;
    111             }
    112 
    113             if (index == -1) {
    114                 // Take the longest element for the preferred width (#19321)
    115                 // We do not want the editor to have the maximum height of all entries. Return a dummy with bogus height.
    116                 IntStream.range(0, list.getModel().getSize())
    117                         .mapToObj(i -> getListCellRendererComponent(list, list.getModel().getElementAt(i), i, isSelected, cellHasFocus))
    118                         .map(Component::getPreferredSize)
    119                         .max(Comparator.comparingInt(dim -> dim.width))
    120                         .ifPresent(dim -> lbl.setPreferredSize(new Dimension(dim.width, 10)));
    121                 return lbl;
    122             }
    123 
    124             // Only return cached size, item is not shown
    125             if (!list.isShowing() && item.preferredWidth != -1 && item.preferredHeight != -1) {
    126                 lbl.setPreferredSize(new Dimension(item.preferredWidth, item.preferredHeight));
    127                 return lbl;
    128             }
    129 
    130             lbl.setPreferredSize(null);
    131 
    132             if (isSelected) {
    133                 lbl.setBackground(list.getSelectionBackground());
    134                 lbl.setForeground(list.getSelectionForeground());
     127        public JLabel getListCellRendererComponent(
     128            JList<? extends PresetListEntry> list, PresetListEntry value, int index, boolean isSelected, boolean cellHasFocus) {
     129
     130            JLabel l = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
     131            if (index != -1) {
     132                // index -1 is set when measuring the size of the cell and when painting the
     133                // editor-ersatz of a readonly combobox. fixes #6157
     134                l.setText(value.getListDisplay(width));
     135            }
     136            String tt = value.value;
     137            if (tt != null && !tt.isEmpty()) {
     138                l.setToolTipText(tr("Sets the key ''{0}'' to the value ''{1}''.", key, tt));
    135139            } else {
    136                 lbl.setBackground(list.getBackground());
    137                 lbl.setForeground(list.getForeground());
    138             }
    139 
    140             lbl.setOpaque(true);
    141             lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
    142             lbl.setText("<html>" + item.getListDisplay() + "</html>");
    143             lbl.setIcon(item.getIcon());
    144             lbl.setEnabled(list.isEnabled());
    145 
    146             // Cache size
    147             item.preferredWidth = (short) lbl.getPreferredSize().width;
    148             item.preferredHeight = (short) lbl.getPreferredSize().height;
    149 
    150             return lbl;
     140                l.setToolTipText(tr("Clears the key ''{0}''.", key));
     141            }
     142            return l;
    151143        }
    152144    }
     
    188180    protected abstract Object getSelectedItem();
    189181
    190     protected abstract void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support);
     182    protected abstract JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support);
    191183
    192184    @Override
     
    219211        label.setToolTipText(getKeyTooltipText());
    220212        label.setComponentPopupMenu(getPopupMenu());
     213        label.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
    221214        p.add(label, GBC.std().insets(0, 0, 10, 0));
    222         addToPanelAnchor(p, default_, support);
     215        JComponent component = addToPanelAnchor(p, default_, support);
    223216        label.setLabelFor(component);
    224217        component.setToolTipText(getKeyTooltipText());
     218        component.applyComponentOrientation(OrientationAction.getValueOrientation(key));
    225219
    226220        return true;
     
    455449    }
    456450
    457     protected ListCellRenderer<PresetListEntry> getListCellRenderer() {
    458         return RENDERER;
    459     }
    460 
    461451    @Override
    462452    public MatchType getDefaultMatch() {
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java

    r17609 r18221  
    1818        JLabel label = new JLabel(locale_text);
    1919        addIcon(label);
     20        label.applyComponentOrientation(support.getDefaultComponentOrientation());
    2021        p.add(label, GBC.eol().fill(GBC.HORIZONTAL));
    2122        return true;
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Link.java

    r17609 r18221  
    3636    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
    3737        initializeLocaleText(tr("More information about this feature"));
    38         Optional.ofNullable(buildUrlLabel()).ifPresent(label -> p.add(label, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL)));
     38        UrlLabel label = buildUrlLabel();
     39        if (label != null) {
     40            label.applyComponentOrientation(support.getDefaultComponentOrientation());
     41            p.add(label, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL));
     42        }
    3943        return false;
    4044    }
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java

    r17639 r18221  
    88import java.util.TreeSet;
    99
     10import javax.swing.JComponent;
    1011import javax.swing.JList;
    1112import javax.swing.JPanel;
    1213import javax.swing.JScrollPane;
    13 import javax.swing.ListCellRenderer;
    1414import javax.swing.ListModel;
    1515
     
    3131
    3232    @Override
    33     protected void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
     33    protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
    3434        list = new ConcatenatingJList(delimiter, presetListEntries.toArray(new PresetListEntry[0]));
    35         component = list;
    36         ListCellRenderer<PresetListEntry> renderer = getListCellRenderer();
     35        ComboMultiSelectListCellRenderer renderer = new ComboMultiSelectListCellRenderer(list, list.getCellRenderer(), 200, key);
    3736        list.setCellRenderer(renderer);
    38         list.setSelectedItem(getItemToSelect(def, support, true));
     37        Object itemToSelect = getItemToSelect(def, support, true);
     38        list.setSelectedItem(itemToSelect == null ? null : new PresetListEntry(itemToSelect.toString()));
    3939        JScrollPane sp = new JScrollPane(list);
    4040        // if a number of rows has been specified in the preset,
     
    4747        list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
    4848        p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
     49        return list;
    4950    }
    5051
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java

    r17609 r18221  
    1919    @Override
    2020    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     21        JLabel label = new JLabel(locale_text);
     22        label.applyComponentOrientation(support.getDefaultComponentOrientation());
    2123        initializeLocaleText(tr("Optional Attributes:"));
    2224        p.add(new JLabel(" "), GBC.eol()); // space
    23         p.add(new JLabel(locale_text), GBC.eol());
     25        p.add(label, GBC.eol());
    2426        p.add(new JLabel(" "), GBC.eol()); // space
    2527        return false;
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java

    r18170 r18221  
    6262            JLabel lbl = new TaggingPresetLabel(t);
    6363            lbl.addMouseListener(new TaggingPresetMouseAdapter(t, support.getSelected()));
     64            lbl.applyComponentOrientation(support.getDefaultComponentOrientation());
    6465            p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL));
    6566        }
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java

    r18208 r18221  
    3636    public String locale_short_description; // NOSONAR
    3737
    38     /** Cached width (currently only for Combo) to speed up preset dialog initialization */
    39     public short preferredWidth = -1; // NOSONAR
    40     /** Cached height (currently only for Combo) to speed up preset dialog initialization */
    41     public short preferredHeight = -1; // NOSONAR
    42 
    4338    /**
    4439     * Constructs a new {@code PresetListEntry}, uninitialized.
     
    5752
    5853    /**
    59      * Returns HTML formatted contents.
     54     * Returns the contents displayed in the dropdown list.
     55     *
     56     * This is the contents that would be displayed in the current view plus a short description to
     57     * aid the user.  The whole contents is wrapped to {@code width}.
     58     *
     59     * @param width the width in px
    6060     * @return HTML formatted contents
    6161     */
    62     public String getListDisplay() {
    63         if (value.equals(KeyedItem.DIFFERENT))
    64             return "<b>" + Utils.escapeReservedCharactersHTML(KeyedItem.DIFFERENT) + "</b>";
     62    public String getListDisplay(int width) {
     63        if (value.equals(KeyedItem.DIFFERENT)) {
     64            return "<b>" + KeyedItem.DIFFERENT + "</b>";
     65        }
    6566
    66         String displayValue = Utils.escapeReservedCharactersHTML(getDisplayValue());
    6767        String shortDescription = getShortDescription(true);
     68        String displayValue = getDisplayValue();
    6869
    69         if (displayValue.isEmpty() && Utils.isEmpty(shortDescription))
    70             return "&nbsp;";
     70        if (shortDescription.isEmpty()) {
     71            if (displayValue.isEmpty()) {
     72                return " ";
     73            }
     74            return displayValue;
     75        }
    7176
    72         final StringBuilder res = new StringBuilder("<b>").append(displayValue).append("</b>");
    73         if (!Utils.isEmpty(shortDescription)) {
    74             // wrap in table to restrict the text width
    75             res.append("<div style=\"width:300px; padding:0 0 5px 5px\">")
    76                .append(shortDescription)
    77                .append("</div>");
    78         }
    79         return res.toString();
     77        // RTL not supported in HTML. See: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4866977
     78        return String.format("<html><div style=\"width: %d\"><b>%s</b><p style=\"padding-left: 10\">%s</p></div></html>",
     79                width,
     80                displayValue,
     81                Utils.escapeReservedCharactersHTML(shortDescription));
    8082    }
    8183
     
    8991
    9092    /**
    91      * Returns the value to display.
     93     * Returns the contents of the current item view.
    9294     * @return the value to display
    9395     */
     
    102104     */
    103105    public String getShortDescription(boolean translated) {
    104         return translated
     106        String shortDesc = translated
    105107                ? Utils.firstNonNull(locale_short_description, tr(short_description))
    106108                        : short_description;
     109        return shortDesc == null ? "" : shortDesc;
    107110    }
    108111
     
    113116            return KeyedItem.DIFFERENT;
    114117        String displayValue = getDisplayValue();
    115         return displayValue != null ? displayValue.replaceAll("<.*>", "") : ""; // remove additional markup, e.g. <br>
     118        return displayValue != null ? displayValue.replaceAll("\\s*<.*>\\s*", " ") : ""; // remove additional markup, e.g. <br>
    116119    }
    117120
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java

    r17609 r18221  
    201201                i.addToPanel(proles);
    202202            }
     203            proles.applyComponentOrientation(support.getDefaultComponentOrientation());
    203204            p.add(proles, GBC.eol());
    204205        }
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java

    r18208 r18221  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
     6import java.util.ArrayList;
    67import java.awt.Color;
    78import java.awt.Component;
     
    2425
    2526import org.openstreetmap.josm.data.osm.Tag;
    26 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
     27import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
     28import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
     29import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
     30import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
    2731import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
    2832import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
     
    3135import org.openstreetmap.josm.gui.widgets.JosmComboBox;
    3236import org.openstreetmap.josm.gui.widgets.JosmTextField;
     37import org.openstreetmap.josm.gui.widgets.OrientationAction;
    3338import org.openstreetmap.josm.tools.GBC;
    3439import org.openstreetmap.josm.tools.Logging;
     
    7075    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
    7176
     77        AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>();
     78        List<String> keys = new ArrayList<>();
     79        keys.add(key);
     80        if (alternative_autocomplete_keys != null) {
     81            for (String k : alternative_autocomplete_keys.split(",", -1)) {
     82                keys.add(k);
     83            }
     84        }
     85        getAllForKeys(keys).forEach(model::addElement);
     86
     87        AutoCompTextField<AutoCompletionItem> textField;
     88        AutoCompComboBoxEditor<AutoCompletionItem> editor = null;
     89
    7290        // find out if our key is already used in the selection.
    7391        Usage usage = determineTextUsage(support.getSelected(), key);
    74         AutoCompletingTextField textField = new AutoCompletingTextField();
    75         if (alternative_autocomplete_keys != null) {
    76             initAutoCompletionField(textField, (key + ',' + alternative_autocomplete_keys).split(",", -1));
     92
     93        if (usage.unused() || usage.hasUniqueValue()) {
     94            textField = new AutoCompTextField<>();
    7795        } else {
    78             initAutoCompletionField(textField, key);
     96            editor = new AutoCompComboBoxEditor<>();
     97            textField = editor.getEditorComponent();
     98        }
     99        textField.setModel(model);
     100        value = textField;
     101
     102        if (length > 0) {
     103            textField.setMaxTextLength(length);
    79104        }
    80105        if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) {
    81106            textField.setHint(key);
    82         }
    83         if (length > 0) {
    84             textField.setMaxChars((int) length);
    85107        }
    86108        if (usage.unused()) {
     
    111133            value = textField;
    112134            originalValue = usage.getFirst();
    113         } else {
    114             // the objects have different values
     135        }
     136        if (editor != null) {
     137            // The selected primitives have different values for this key.   <b>Note:</b> this
     138            // cannot be an AutoCompComboBox because the values in the dropdown are different from
     139            // those we autocomplete on.
    115140            JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0]));
    116141            comboBox.setEditable(true);
    117             comboBox.setEditor(textField);
     142            comboBox.setEditor(editor);
    118143            comboBox.getEditor().setItem(DIFFERENT);
    119144            value = comboBox;
     
    180205        p.add(label, GBC.std().insets(0, 0, 10, 0));
    181206        p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
     207        label.applyComponentOrientation(support.getDefaultComponentOrientation());
    182208        value.setToolTipText(getKeyTooltipText());
     209        value.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
    183210        return true;
    184211    }
     
    253280    }
    254281
    255     private void setupListeners(AutoCompletingTextField textField, TaggingPresetItemGuiSupport support) {
     282    private void setupListeners(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetItemGuiSupport support) {
    256283        // value_templates don't work well with multiple selected items because,
    257284        // as the command queue is currently implemented, we can only save
     
    266293                Logging.trace("Evaluating value_template {0} for key {1} from {2} with new value {3} => {4}",
    267294                        valueTemplate, key, source, newValue, valueTemplateText);
    268                 textField.setItem(valueTemplateText);
     295                textField.setText(valueTemplateText);
    269296                if (originalValue != null && !originalValue.equals(valueTemplateText)) {
    270297                    textField.setForeground(Color.RED);
  • trunk/src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java

    r18211 r18221  
    55
    66import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
     7import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
    78import org.openstreetmap.josm.tools.Logging;
    89import org.openstreetmap.josm.tools.Utils;
     
    1415 * @since 5765
    1516 */
    16 public abstract class AbstractIdTextField<T extends AbstractTextComponentValidator> extends JosmTextField {
     17public abstract class AbstractIdTextField<T extends AbstractTextComponentValidator> extends AutoCompTextField<String> {
    1718
    1819    protected final transient T validator;
  • trunk/src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java

    r18173 r18221  
    22package org.openstreetmap.josm.gui.widgets;
    33
     4
    45import java.awt.Component;
     6import java.awt.ComponentOrientation;
    57import java.awt.Dimension;
    6 import java.awt.event.MouseAdapter;
    7 import java.awt.event.MouseEvent;
     8import java.awt.Graphics;
     9import java.awt.GraphicsConfiguration;
     10import java.awt.Insets;
     11import java.awt.Point;
     12import java.awt.Rectangle;
     13import java.awt.Toolkit;
    814import java.beans.PropertyChangeEvent;
    915import java.beans.PropertyChangeListener;
    10 import java.util.Arrays;
    11 import java.util.Collection;
    12 import java.util.List;
    13 import java.util.stream.Collectors;
    14 import java.util.stream.IntStream;
    1516
    1617import javax.swing.ComboBoxEditor;
    17 import javax.swing.ComboBoxModel;
    18 import javax.swing.DefaultComboBoxModel;
    1918import javax.swing.JComboBox;
    2019import javax.swing.JList;
     20import javax.swing.JScrollPane;
    2121import javax.swing.JTextField;
    22 import javax.swing.plaf.basic.ComboPopup;
     22import javax.swing.ListCellRenderer;
     23import javax.swing.border.Border;
     24import javax.swing.event.PopupMenuEvent;
     25import javax.swing.event.PopupMenuListener;
    2326import javax.swing.text.JTextComponent;
    2427
    25 import org.openstreetmap.josm.gui.util.GuiHelper;
     28import org.openstreetmap.josm.spi.preferences.Config;
    2629
    2730/**
    28  * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br>
    29  * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
     31 * Base class for all comboboxes in JOSM.
     32 * <p>
     33 * This combobox will show as many rows as possible without covering the combox itself. It makes
     34 * sure the list will never go outside the screen (see #7917). You may limit the number of rows
     35 * shown with the configuration: {@code gui.combobox.maximum-row-count}.
     36 * <p>
     37 * This combobox uses a {@link JosmTextField} for its editor component.
     38 *
    3039 * @param <E> the type of the elements of this combo box
    31  *
    3240 * @since 5429 (creation)
    3341 * @since 7015 (generics for Java 7)
    3442 */
    35 public class JosmComboBox<E> extends JComboBox<E> {
    36 
    37     private final ContextMenuHandler handler = new ContextMenuHandler();
    38 
    39     /**
    40      * Creates a <code>JosmComboBox</code> with a default data model.
     43public class JosmComboBox<E> extends JComboBox<E> implements PopupMenuListener, PropertyChangeListener {
     44    /**
     45     * Limits the number of rows that this combobox will show.
     46     */
     47    public static final String PROP_MAXIMUM_ROW_COUNT = "gui.combobox.maximum-row-count";
     48
     49    /** the configured maximum row count or null */
     50    private Integer configMaximumRowCount = null;
     51
     52    /**
     53     * The preferred height of the combobox when closed.  Use if the items in the list dropdown are
     54     * taller than the item in the editor, as in some comboboxes in the preset dialog.  -1 to use
     55     * the height of the tallest item in the list.
     56     */
     57    private int preferredHeight = -1;
     58
     59    /** greyed text to display in the editor when the selected value is empty */
     60    private String hint;
     61
     62    /**
     63     * Creates a {@code JosmComboBox} with a {@link JosmComboBoxModel} data model.
    4164     * The default data model is an empty list of objects.
    4265     * Use <code>addItem</code> to add items. By default the first item
    4366     * in the data model becomes selected.
    44      *
    45      * @see DefaultComboBoxModel
    4667     */
    4768    public JosmComboBox() {
    48         init(null);
    49     }
    50 
    51     /**
    52      * Creates a <code>JosmComboBox</code> with a default data model and
     69        super(new JosmComboBoxModel<E>());
     70        init();
     71    }
     72
     73    /**
     74     * Creates a {@code JosmComboBox} with a {@link JosmComboBoxModel} data model and
    5375     * the specified prototype display value.
    5476     * The default data model is an empty list of objects.
     
    6082     *      displaying a scroll bar
    6183     *
    62      * @see DefaultComboBoxModel
    6384     * @since 5450
    64      */
     85     * @deprecated use {@link #setPrototypeDisplayValue} instead.
     86     */
     87    @Deprecated
    6588    public JosmComboBox(E prototypeDisplayValue) {
    66         init(prototypeDisplayValue);
    67     }
    68 
    69     /**
    70      * Creates a <code>JosmComboBox</code> that takes its items from an
    71      * existing <code>ComboBoxModel</code>. Since the
    72      * <code>ComboBoxModel</code> is provided, a combo box created using
    73      * this constructor does not create a default combo box model and
    74      * may impact how the insert, remove and add methods behave.
    75      *
    76      * @param aModel the <code>ComboBoxModel</code> that provides the
    77      *      displayed list of items
    78      * @see DefaultComboBoxModel
    79      */
    80     public JosmComboBox(ComboBoxModel<E> aModel) {
     89        super(new JosmComboBoxModel<E>());
     90        setPrototypeDisplayValue(prototypeDisplayValue);
     91        init();
     92    }
     93
     94    /**
     95     * Creates a {@code JosmComboBox} that takes it items from an existing {@link JosmComboBoxModel}
     96     * data model.
     97     *
     98     * @param aModel the model that provides the displayed list of items
     99     */
     100    public JosmComboBox(JosmComboBoxModel<E> aModel) {
    81101        super(aModel);
    82         List<E> list = IntStream.range(0, aModel.getSize())
    83                 .mapToObj(aModel::getElementAt)
    84                 .collect(Collectors.toList());
    85         init(findPrototypeDisplayValue(list));
    86     }
    87 
    88     /**
    89      * Creates a <code>JosmComboBox</code> that contains the elements
     102        init();
     103    }
     104
     105    /**
     106     * Creates a {@code JosmComboBox} that takes it items from an existing {@link JosmComboBoxModel}
     107     * data model and sets the specified prototype display value.
     108     *
     109     * @param aModel the model that provides the displayed list of items
     110     * @param prototypeDisplayValue use this item to size the combobox (may be null)
     111     * @deprecated use {@link #setPrototypeDisplayValue} instead.
     112     */
     113    @Deprecated
     114    public JosmComboBox(JosmComboBoxModel<E> aModel, E prototypeDisplayValue) {
     115        super(aModel);
     116        setPrototypeDisplayValue(prototypeDisplayValue);
     117        init();
     118    }
     119
     120    /**
     121     * Creates a {@code JosmComboBox} that contains the elements
    90122     * in the specified array. By default the first item in the array
    91123     * (and therefore the data model) becomes selected.
    92124     *
    93125     * @param items  an array of objects to insert into the combo box
    94      * @see DefaultComboBoxModel
    95126     */
    96127    public JosmComboBox(E[] items) {
    97         super(items);
    98         init(findPrototypeDisplayValue(Arrays.asList(items)));
     128        super(new JosmComboBoxModel<E>());
     129        init();
     130        for (E elem : items) {
     131            getModel().addElement(elem);
     132        }
     133    }
     134
     135    private void init() {
     136        configMaximumRowCount = Config.getPref().getInt(PROP_MAXIMUM_ROW_COUNT, 9999);
     137        setEditor(new JosmComboBoxEditor());
     138        // listen when the popup shows up so we can maximize its height
     139        addPopupMenuListener(this);
     140    }
     141
     142    /**
     143     * Returns the {@link JosmComboBoxModel} currently used.
     144     *
     145     * @return the model or null
     146     */
     147    @Override
     148    public JosmComboBoxModel<E> getModel() {
     149        return (JosmComboBoxModel<E>) dataModel;
     150    }
     151
     152    @Override
     153    public void setEditor(ComboBoxEditor newEditor) {
     154        if (editor != null) {
     155            editor.getEditorComponent().removePropertyChangeListener(this);
     156        }
     157        super.setEditor(newEditor);
     158        if (editor != null) {
     159            // listen to orientation changes in the editor
     160            editor.getEditorComponent().addPropertyChangeListener(this);
     161        }
    99162    }
    100163
     
    105168     * @since 9484
    106169     */
    107     public JTextField getEditorComponent() {
    108         return (JTextField) getEditor().getEditorComponent();
     170    public JosmTextField getEditorComponent() {
     171        return (JosmTextField) (editor == null ? null : editor.getEditorComponent());
    109172    }
    110173
     
    116179     */
    117180    public String getText() {
    118         return getEditorComponent().getText();
     181        JosmTextField tf = getEditorComponent();
     182        return tf == null ? null : tf.getText();
    119183    }
    120184
     
    126190     */
    127191    public void setText(String value) {
    128         getEditorComponent().setText(value);
    129     }
    130 
    131     /**
    132      * Finds the prototype display value to use among the given possible candidates.
    133      * @param possibleValues The possible candidates that will be iterated.
    134      * @return The value that needs the largest display height on screen.
    135      * @since 5558
    136      */
    137     protected final E findPrototypeDisplayValue(Collection<E> possibleValues) {
    138         E result = null;
    139         int maxHeight = -1;
    140         if (possibleValues != null) {
    141             // Remind old prototype to restore it later
    142             E oldPrototype = getPrototypeDisplayValue();
    143             // Get internal JList to directly call the renderer
    144             @SuppressWarnings("rawtypes")
    145             JList list = getList();
    146             try {
    147                 // Index to give to renderer
    148                 int i = 0;
    149                 for (E value : possibleValues) {
    150                     if (value != null) {
    151                         // With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize()
    152                         // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
    153                         // So we explicitly call the renderer by simulating a correct index for the current value
    154                         @SuppressWarnings("unchecked")
    155                         Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
    156                         if (c != null) {
    157                             // Get the real preferred size for the current value
    158                             Dimension dim = c.getPreferredSize();
    159                             if (dim.height > maxHeight) {
    160                                 // Larger ? This is our new prototype
    161                                 maxHeight = dim.height;
    162                                 result = value;
    163                             }
    164                         }
    165                     }
    166                     i++;
     192        JosmTextField tf = getEditorComponent();
     193        if (tf != null)
     194            tf.setText(value);
     195    }
     196
     197    /**
     198     * Selects an item and/or sets text
     199     *
     200     * Selects the item whose {@code toString()} equals {@code text}. If an item could not be found,
     201     * selects nothing and sets the text anyway.
     202     *
     203     * @param text the text to select and set
     204     * @return the item or null
     205     */
     206    public E setSelectedItemText(String text) {
     207        E item = getModel().find(text);
     208        setSelectedItem(item);
     209        if (text == null || !text.equals(getText()))
     210            setText(text);
     211        return item;
     212    }
     213
     214    /* Hint handling */
     215
     216    /**
     217     * Returns the hint text
     218     * @return the hint text
     219     */
     220    public String getHint() {
     221        return hint;
     222    }
     223
     224    /**
     225     * Sets the hint to display when no text has been entered.
     226     *
     227     * @param hint the hint to set
     228     * @return the old hint
     229     * @since 18221
     230     */
     231    public String setHint(String hint) {
     232        String old = hint;
     233        this.hint = hint;
     234        JosmTextField tf = getEditorComponent();
     235        if (tf != null)
     236            tf.setHint(hint);
     237        return old;
     238    }
     239
     240    @Override
     241    public void setComponentOrientation(ComponentOrientation o) {
     242        if (o.isLeftToRight() != getComponentOrientation().isLeftToRight()) {
     243            super.setComponentOrientation(o);
     244            getEditorComponent().setComponentOrientation(o);
     245            // the button doesn't move over without this
     246            revalidate();
     247        }
     248    }
     249
     250    /**
     251     * Return true if the combobox should display the hint text.
     252     *
     253     * @return whether to display the hint text
     254     * @since 18221
     255     */
     256    public boolean displayHint() {
     257        return !isEditable() && hint != null && !hint.isEmpty() && getText().isEmpty(); // && !isFocusOwner();
     258    }
     259
     260    /**
     261     * Overrides the calculated height.  See: {@link #setPreferredHeight(int)}.
     262     *
     263     * @since 18221
     264     */
     265    @Override
     266    public Dimension getPreferredSize() {
     267        Dimension d = super.getPreferredSize();
     268        if (preferredHeight != -1)
     269            d.height = preferredHeight;
     270        return d;
     271    }
     272
     273    /**
     274     * Sets the preferred height of the combobox editor.
     275     * <p>
     276     * A combobox editor is automatically sized to accomodate the widest and the tallest items in
     277     * the list.  In the Preset dialogs we show more of an item in the list than in the editor, so
     278     * the editor becomes too big.  With this method we can set the editor height to a fixed value.
     279     * <p>
     280     * Set this to -1 to get the default behaviour back.
     281     *
     282     * See also: #6157
     283     *
     284     * @param height the preferred height or -1
     285     * @return the old preferred height
     286     * @see #setPreferredSize
     287     * @since 18221
     288     */
     289    public int setPreferredHeight(int height) {
     290        int old = preferredHeight;
     291        preferredHeight = height;
     292        return old;
     293    }
     294
     295    /**
     296     * Get the dropdown list component
     297     *
     298     * @return the list or null
     299     */
     300    @SuppressWarnings("unchecked")
     301    public JList<E> getList() {
     302        Object popup = getUI().getAccessibleChild(this, 0);
     303        if (popup != null && popup instanceof javax.swing.plaf.basic.ComboPopup) {
     304            return ((javax.swing.plaf.basic.ComboPopup) popup).getList();
     305        }
     306        return null;
     307    }
     308
     309    // get the popup list
     310
     311    /**
     312     * Draw the hint text for read-only comboboxes.
     313     * <p>
     314     * The obvious way -- to call {@code setText(hint)} and {@code setForeground(gray)} on the
     315     * {@code JLabel} returned by the list cell renderer -- unfortunately does not work out well
     316     * because many UIs change the foreground color or the enabled state of the {@code JLabel} after
     317     * the list cell renderer has returned ({@code BasicComboBoxUI}).  Other UIs don't honor the
     318     * label color at all ({@code SynthLabelUI}).
     319     * <p>
     320     * We use the same approach as in {@link JosmTextField}. The only problem we face is to get the
     321     * coordinates of the text inside the combobox.  Fortunately even read-only comboboxes have a
     322     * (partially configured) editor component, although they don't use it.  We configure that editor
     323     * just enough to call {@link JTextField#modelToView modelToView} and
     324     * {@link javax.swing.JComponent#getBaseline getBaseline} on it, thus obtaining the text
     325     * coordinates.
     326     *
     327     * @see javax.swing.plaf.basic.BasicComboBoxUI#paintCurrentValue
     328     * @see javax.swing.plaf.synth.SynthLabelUI#paint
     329     */
     330    @Override
     331    protected void paintComponent(Graphics g) {
     332        super.paintComponent(g);
     333        JosmTextField editor = getEditorComponent();
     334        if (displayHint() && editor != null) {
     335            if (editor.getSize().width == 0) {
     336                Dimension dimen = getSize();
     337                Insets insets = getInsets();
     338                // a fake configuration not too far from reality
     339                editor.setSize(dimen.width - insets.left - insets.right,
     340                               dimen.height - insets.top - insets.bottom);
     341            }
     342            editor.drawHint(g);
     343        }
     344    }
     345
     346    /**
     347     * Empties the internal undo manager, if any.
     348     * <p>
     349     * Used in the {@link org.openstreetmap.josm.gui.io.UploadDialog UploadDialog}.
     350     * @since 14977
     351     */
     352    public final void discardAllUndoableEdits() {
     353        getEditorComponent().discardAllUndoableEdits();
     354    }
     355
     356    /**
     357     * Limits the popup height.
     358     * <p>
     359     * Limits the popup height to the available screen space either below or above the combobox,
     360     * whichever is bigger. To find the maximum number of rows that fit the screen, it does the
     361     * reverse of the calculation done in
     362     * {@link javax.swing.plaf.basic.BasicComboPopup#getPopupLocation}.
     363     *
     364     * @see javax.swing.plaf.basic.BasicComboBoxUI#getAccessibleChild
     365     */
     366    @Override
     367    public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
     368        // Get the combobox bounds.
     369        Rectangle bounds = new Rectangle(getLocationOnScreen(), getSize());
     370
     371        // Get the screen bounds of the screen (of a multi-screen setup) we are on.
     372        Rectangle screenBounds;
     373        GraphicsConfiguration gc = getGraphicsConfiguration();
     374        Toolkit toolkit = Toolkit.getDefaultToolkit();
     375        if (gc != null) {
     376            Insets screenInsets = toolkit.getScreenInsets(gc);
     377            screenBounds = gc.getBounds();
     378            screenBounds.x += screenInsets.left;
     379            screenBounds.y += screenInsets.top;
     380            screenBounds.width -= (screenInsets.left + screenInsets.right);
     381            screenBounds.height -= (screenInsets.top + screenInsets.bottom);
     382        } else {
     383            screenBounds = new Rectangle(new Point(), toolkit.getScreenSize());
     384        }
     385        int freeAbove = bounds.y - screenBounds.y;
     386        int freeBelow = (screenBounds.y + screenBounds.height) - (bounds.y + bounds.height);
     387
     388        try {
     389            // First try an implementation-dependent method to get the exact number.
     390            JList<E> jList = getList();
     391
     392            // Calculate the free space available on screen
     393            Insets insets = jList.getInsets();
     394            // A small fudge factor that accounts for the displacement of the popup relative to the
     395            // combobox and the popup shadow.
     396            int fudge = 4;
     397            int free = Math.max(freeAbove, freeBelow) - (insets.top + insets.bottom) - fudge;
     398            if (jList.getParent() instanceof JScrollPane) {
     399                JScrollPane scroller = (JScrollPane) jList.getParent();
     400                Border border = scroller.getViewportBorder();
     401                if (border != null) {
     402                    insets = border.getBorderInsets(null);
     403                    free -= insets.top + insets.bottom;
    167404                }
    168             } finally {
    169                 // Restore original prototype
    170                 setPrototypeDisplayValue(oldPrototype);
    171             }
    172         }
    173         return result;
    174     }
    175 
    176     @SuppressWarnings("unchecked")
    177     protected final JList<Object> getList() {
    178         return IntStream.range(0, getUI().getAccessibleChildrenCount(this))
    179                 .mapToObj(i -> getUI().getAccessibleChild(this, i))
    180                 .filter(child -> child instanceof ComboPopup)
    181                 .findFirst()
    182                 .map(child -> ((ComboPopup) child).getList())
    183                 .orElse(null);
    184     }
    185 
    186     /**
    187      * Set the prototypeCellValue property and calculate the height of the dropdown.
    188      */
    189     @Override
    190     public void setPrototypeDisplayValue(E prototype) {
    191         if (prototype != null) {
    192             super.setPrototypeDisplayValue(prototype);
    193             int screenHeight = GuiHelper.getScreenSize().height;
    194             // Compute maximum number of visible items based on the preferred size of the combo box.
    195             // This assumes that items have the same height as the combo box, which is not granted by the look and feel
    196             int maxsize = (screenHeight/getPreferredSize().height) / 2;
    197             // If possible, adjust the maximum number of items with the real height of items
    198             // It is not granted this works on every platform (tested OK on Windows)
    199             JList<Object> list = getList();
    200             if (list != null) {
    201                 if (!prototype.equals(list.getPrototypeCellValue())) {
    202                     list.setPrototypeCellValue(prototype);
    203                 }
    204                 int height = list.getFixedCellHeight();
    205                 if (height > 0) {
    206                     maxsize = (screenHeight/height) / 2;
     405                border = scroller.getBorder();
     406                if (border != null) {
     407                    insets = border.getBorderInsets(null);
     408                    free -= insets.top + insets.bottom;
    207409                }
    208410            }
    209             setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
    210         }
    211     }
    212 
    213     protected final void init(E prototype) {
    214         init(prototype, true);
    215     }
    216 
    217     protected final void init(E prototype, boolean registerPropertyChangeListener) {
    218         setPrototypeDisplayValue(prototype);
    219         // Handle text contextual menus for editable comboboxes
    220         if (registerPropertyChangeListener) {
    221             addPropertyChangeListener("editable", handler);
    222             addPropertyChangeListener("editor", handler);
    223         }
    224     }
    225 
    226     protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener {
    227 
    228         private JTextComponent component;
    229         private PopupMenuLauncher launcher;
    230 
    231         @Override
    232         public void propertyChange(PropertyChangeEvent evt) {
    233             if ("editable".equals(evt.getPropertyName())) {
    234                 if (evt.getNewValue().equals(Boolean.TRUE)) {
    235                     enableMenu();
    236                 } else {
    237                     disableMenu();
    238                 }
    239             } else if ("editor".equals(evt.getPropertyName())) {
    240                 disableMenu();
    241                 if (isEditable()) {
    242                     enableMenu();
    243                 }
     411
     412            // Calculate how many rows fit into the free space.  Rows may have variable heights.
     413            int rowCount = Math.min(configMaximumRowCount, getItemCount());
     414            ListCellRenderer<? super E> r = jList.getCellRenderer();  // must take this from list, not combo: flatlaf bug
     415            int i, h = 0;
     416            for (i = 0; i < rowCount; ++i) {
     417                Component c = r.getListCellRendererComponent(jList, getModel().getElementAt(i), i, false, false);
     418                h += c.getPreferredSize().height;
     419                if (h >= free)
     420                    break;
    244421            }
    245         }
    246 
    247         private void enableMenu() {
    248             if (launcher == null && editor != null) {
    249                 Component editorComponent = editor.getEditorComponent();
    250                 if (editorComponent instanceof JTextComponent) {
    251                     component = (JTextComponent) editorComponent;
    252                     component.addMouseListener(this);
    253                     launcher = TextContextualPopupMenu.enableMenuFor(component, true);
    254                 }
    255             }
    256         }
    257 
    258         private void disableMenu() {
    259             if (launcher != null) {
    260                 TextContextualPopupMenu.disableMenuFor(component, launcher);
    261                 launcher = null;
    262                 component.removeMouseListener(this);
    263                 component = null;
    264             }
    265         }
    266 
    267         private void discardAllUndoableEdits() {
    268             if (launcher != null) {
    269                 launcher.discardAllUndoableEdits();
    270             }
    271         }
    272 
    273         @Override
    274         public void mousePressed(MouseEvent e) {
    275             processEvent(e);
    276         }
    277 
    278         @Override
    279         public void mouseReleased(MouseEvent e) {
    280             processEvent(e);
    281         }
    282 
    283         private void processEvent(MouseEvent e) {
    284             if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) {
    285                 launcher.getMenu().setVisible(false);
    286             }
    287         }
    288     }
    289 
    290     /**
    291      * Reinitializes this {@link JosmComboBox} to the specified values. This may be needed if a custom renderer is used.
    292      * @param values The values displayed in the combo box.
    293      * @since 5558
    294      */
    295     public final void reinitialize(Collection<E> values) {
    296         init(findPrototypeDisplayValue(values), false);
    297         discardAllUndoableEdits();
    298     }
    299 
    300     /**
    301      * Empties the internal undo manager, if any.
    302      * @since 14977
    303      */
    304     public final void discardAllUndoableEdits() {
    305         handler.discardAllUndoableEdits();
     422            setMaximumRowCount(i);
     423            // Logging.debug("free = {0}, h = {1}, i = {2}, bounds = {3}, screenBounds = {4}", free, h, i, bounds, screenBounds);
     424        } catch (Exception ex) {
     425            setMaximumRowCount(8); // the default
     426        }
     427    }
     428
     429    @Override
     430    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
     431        // Who cares?
     432    }
     433
     434    @Override
     435    public void popupMenuCanceled(PopupMenuEvent e) {
     436        // Who cares?
     437    }
     438
     439    @Override
     440    public void propertyChange(PropertyChangeEvent evt) {
     441        // follow our editor's orientation
     442        if ("componentOrientation".equals(evt.getPropertyName())) {
     443            setComponentOrientation((ComponentOrientation) evt.getNewValue());
     444        }
    306445    }
    307446}
  • trunk/src/org/openstreetmap/josm/gui/widgets/JosmTextField.java

    r18211 r18221  
    33
    44import java.awt.Color;
     5import java.awt.ComponentOrientation;
     6import java.awt.Font;
    57import java.awt.FontMetrics;
    68import java.awt.Graphics;
    79import java.awt.Graphics2D;
    810import java.awt.Insets;
     11import java.awt.Point;
    912import java.awt.RenderingHints;
     13import java.awt.event.ComponentEvent;
     14import java.awt.event.ComponentListener;
    1015import java.awt.event.FocusEvent;
    1116import java.awt.event.FocusListener;
    12 
    13 import javax.swing.BorderFactory;
     17import java.beans.PropertyChangeEvent;
     18import java.beans.PropertyChangeListener;
     19
    1420import javax.swing.Icon;
    1521import javax.swing.JTextField;
    16 import javax.swing.border.Border;
     22import javax.swing.RepaintManager;
     23import javax.swing.JMenuItem;
     24import javax.swing.JPopupMenu;
     25import javax.swing.UIManager;
     26import javax.swing.text.BadLocationException;
    1727import javax.swing.text.Document;
    1828
     
    3141 * @since 5886
    3242 */
    33 public class JosmTextField extends JTextField implements Destroyable, FocusListener {
     43public class JosmTextField extends JTextField implements Destroyable, ComponentListener, FocusListener, PropertyChangeListener {
    3444
    3545    private final PopupMenuLauncher launcher;
    3646    private String hint;
    3747    private Icon icon;
    38     private int leftInsets;
     48    private Point iconPos;
     49    private Insets originalMargin;
     50    private OrientationAction orientationAction;
    3951
    4052    /**
     
    7890        super(doc, text, columns);
    7991        launcher = TextContextualPopupMenu.enableMenuFor(this, undoRedo);
     92
     93        // There seems to be a bug in Swing 8 that components with Bidi enabled are smaller than
     94        // without. (eg. 23px vs 21px in height, maybe a font thing).  Usually Bidi starts disabled
     95        // but gets enabled whenever RTL text is loaded.  To avoid trashing the layout we enable
     96        // Bidi by default.  See also {@link #drawHint()}.
     97        getDocument().putProperty("i18n", Boolean.TRUE);
     98
     99        // the menu and hotkey to change text orientation
     100        orientationAction = new OrientationAction(this);
     101        orientationAction.addPropertyChangeListener(this);
     102        JPopupMenu menu = launcher.getMenu();
     103        menu.addSeparator();
     104        menu.add(new JMenuItem(orientationAction));
     105        getInputMap().put(OrientationAction.getShortcutKey(), orientationAction);
     106
    80107        // Fix minimum size when columns are specified
    81108        if (columns > 0) {
     
    83110        }
    84111        addFocusListener(this);
     112        addComponentListener(this);
    85113        // Workaround for Java bug 6322854
    86114        JosmPasswordField.workaroundJdkBug6322854(this);
     115        originalMargin = getMargin();
    87116    }
    88117
     
    148177     * Sets the hint to display when no text has been entered.
    149178     * @param hint the hint to set
    150      * @since 7505
    151      */
    152     public final void setHint(String hint) {
     179     * @return the old hint
     180     * @since 18221 (signature)
     181     */
     182    public String setHint(String hint) {
     183        String old = hint;
    153184        this.hint = hint;
     185        return old;
     186    }
     187
     188    /**
     189     * Return true if the textfield should display the hint text.
     190     *
     191     * @return whether to display the hint text
     192     * @since 18221
     193     */
     194    public boolean displayHint() {
     195        return !Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner();
    154196    }
    155197
     
    170212    public void setIcon(Icon icon) {
    171213        this.icon = icon;
     214        if (icon == null) {
     215            setMargin(originalMargin);
     216        }
     217        positionIcon();
     218    }
     219
     220    private void positionIcon() {
    172221        if (icon != null) {
    173             this.leftInsets = getInsets().left;
    174             Border original = getBorder();
    175             Border margin = BorderFactory.createEmptyBorder(0, icon.getIconWidth(), 0, 0);
    176             setBorder(original == null ? margin : BorderFactory.createCompoundBorder(original, margin));
     222            Insets margin = (Insets) originalMargin.clone();
     223            int hGap = (getHeight() - icon.getIconHeight()) / 2;
     224            if (getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) {
     225                margin.right += icon.getIconWidth() + 2 * hGap;
     226                iconPos = new Point(getWidth() - icon.getIconWidth() - hGap, hGap);
     227            } else {
     228                margin.left += icon.getIconWidth() + 2 * hGap;
     229                iconPos = new Point(hGap, hGap);
     230            }
     231            setMargin(margin);
     232        }
     233    }
     234
     235    @Override
     236    public void setComponentOrientation(ComponentOrientation o) {
     237        if (o.isLeftToRight() != getComponentOrientation().isLeftToRight()) {
     238            super.setComponentOrientation(o);
     239            positionIcon();
    177240        }
    178241    }
     
    186249    }
    187250
    188     @Override
    189     public void paint(Graphics g) {
    190         super.paint(g);
     251    /**
     252     * Returns the color for hint texts.
     253     * @return the Color for hint texts
     254     */
     255    public static Color getHintTextColor() {
     256        Color color = UIManager.getColor("TextField[Disabled].textForeground"); // Nimbus?
     257        if (color == null)
     258            color = UIManager.getColor("TextField.inactiveForeground");
     259        if (color == null)
     260            color = Color.GRAY;
     261        return color;
     262    }
     263
     264    /**
     265     * Returns the font for hint texts.
     266     * @return the font for hint texts
     267     */
     268    public static Font getHintFont() {
     269        return UIManager.getFont("TextField.font");
     270    }
     271
     272    @Override
     273    public void paintComponent(Graphics g) {
     274        super.paintComponent(g);
    191275        if (icon != null) {
    192             int h = getHeight() - icon.getIconHeight();
    193             icon.paintIcon(this, g, Math.min(leftInsets, h / 2), h / 2);
    194         }
    195         if (!Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner()) {
    196             // Taken from http://stackoverflow.com/a/24571681/2257172
    197             int h = getHeight();
    198             if (g instanceof Graphics2D) {
    199                 ((Graphics2D) g).setRenderingHint(
    200                         RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    201             }
    202             Insets ins = getInsets();
    203             FontMetrics fm = g.getFontMetrics();
    204             int c0 = getBackground().getRGB();
    205             int c1 = getForeground().getRGB();
    206             int m = 0xfefefefe;
    207             int c2 = ((c0 & m) >>> 1) + ((c1 & m) >>> 1);
    208             g.setColor(new Color(c2, true));
    209             g.drawString(hint, ins.left, h / 2 + fm.getAscent() / 2 - 2);
    210         }
     276            icon.paintIcon(this, g, iconPos.x, iconPos.y);
     277        }
     278        if (displayHint()) {
     279            // Logging.debug("drawing textfield hint: {0}", getHint());
     280            drawHint(g);
     281        }
     282    }
     283
     284    /**
     285     * Draws the hint text over the editor component.
     286     *
     287     * @param g the graphics context
     288     */
     289    public void drawHint(Graphics g) {
     290        int x;
     291        try {
     292            x = modelToView(0).x;
     293        } catch (BadLocationException exc) {
     294            return; // can't happen
     295        }
     296        // Taken from http://stackoverflow.com/a/24571681/2257172
     297        if (g instanceof Graphics2D) {
     298            ((Graphics2D) g).setRenderingHint(
     299                    RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
     300        }
     301        g.setColor(getHintTextColor());
     302        g.setFont(getHintFont());
     303        if (getComponentOrientation().isLeftToRight()) {
     304            g.drawString(getHint(), x, getBaseline(getWidth(), getHeight()));
     305        } else {
     306            FontMetrics metrics = g.getFontMetrics(g.getFont());
     307            int dx = metrics.stringWidth(getHint());
     308            g.drawString(getHint(), x - dx, getBaseline(getWidth(), getHeight()));
     309        }
     310        // Needed to avoid endless repaint loop if we accidentally draw over the insets.  This may
     311        // easily happen because a change in text orientation invalidates the textfield and
     312        // following that the preferred size gets smaller. (Bug in Swing?)
     313        RepaintManager.currentManager(this).markCompletelyClean(this);
    211314    }
    212315
     
    217320            map.keyDetector.setEnabled(false);
    218321        }
    219         repaint();
     322        if (e != null && e.getOppositeComponent() != null) {
     323            // Select all characters when the change of focus occurs inside JOSM only.
     324            // When switching from another application, it is annoying, see #13747
     325            selectAll();
     326        }
     327        positionIcon();
     328        repaint(); // get rid of hint
    220329    }
    221330
     
    226335            map.keyDetector.setEnabled(true);
    227336        }
    228         repaint();
     337        repaint(); // paint hint
    229338    }
    230339
     
    234343        TextContextualPopupMenu.disableMenuFor(this, launcher);
    235344    }
     345
     346    @Override
     347    public void componentResized(ComponentEvent e) {
     348        positionIcon();
     349    }
     350
     351    @Override
     352    public void componentMoved(ComponentEvent e) {
     353    }
     354
     355    @Override
     356    public void componentShown(ComponentEvent e) {
     357    }
     358
     359    @Override
     360    public void componentHidden(ComponentEvent e) {
     361    }
     362
     363    @Override
     364    public void propertyChange(PropertyChangeEvent evt) {
     365        // command from the menu / shortcut key
     366        if ("orientationAction".equals(evt.getPropertyName())) {
     367            setComponentOrientation((ComponentOrientation) evt.getNewValue());
     368        }
     369    }
    236370}
  • trunk/test/unit/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditorTest.java

    r18037 r18221  
    55
    66import org.junit.jupiter.api.Test;
     7import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
    78
    89/**
    910 * Unit tests of {@link MultiValueCellEditor} class.
    1011 */
     12@BasicPreferences
    1113class MultiValueCellEditorTest {
    1214    /**
  • trunk/test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java

    r17275 r18221  
    22package org.openstreetmap.josm.gui.tagging.presets.items;
    33
    4 import static org.junit.jupiter.api.Assertions.assertEquals;
     4import static org.junit.jupiter.api.Assertions.assertTrue;
    55
    66import org.junit.jupiter.api.BeforeAll;
     
    2626    @Test
    2727    void testTicket12416() {
    28         assertEquals("&nbsp;", new PresetListEntry("").getListDisplay());
     28        assertTrue(new PresetListEntry("").getListDisplay(200).contains(" "));
    2929    }
    3030}
Note: See TracChangeset for help on using the changeset viewer.