Ticket #25030: multiline_editing_support.patch

File multiline_editing_support.patch, 40.3 KB (added by LingM, 25 minutes ago)
  • src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRenderer.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRenderer.java src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRenderer.java
    index 58f9de53bc..06953cef48 100644
    import java.util.concurrent.CopyOnWriteArrayList;  
    1616
    1717import javax.swing.JLabel;
    1818import javax.swing.JTable;
     19import javax.swing.JTextArea;
     20import javax.swing.SwingConstants;
    1921import javax.swing.UIManager;
    2022import javax.swing.table.DefaultTableCellRenderer;
    2123import javax.swing.table.TableCellRenderer;
    … … public class PropertiesCellRenderer extends DefaultTableCellRenderer {  
    132134            }
    133135            ((JLabel) c).putClientProperty("html.disable", enableHTML ? null : Boolean.TRUE); // Fix #8730
    134136            ((JLabel) c).setText(str);
     137
     138            // Needed for multiline values and doesn't hurt single line ones
     139            if (column == 0) {
     140                ((JLabel) c).setVerticalAlignment(SwingConstants.TOP);
     141            } else if (!enableHTML) {
     142                JTextArea jta = new JTextArea(str);
     143                jta.setForeground(c.getForeground());
     144                jta.setBackground(c.getBackground());
     145
     146                c = jta;
     147            }
     148
    135149            if (DISCARDABLE.get()) {
    136150                String key = null;
    137151                if (column == 0) {
  • src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
    index de272a5f72..ec4afca39c 100644
    implements DataSelectionListener, ActiveLayerChangeListener, PropertyChangeListe  
    696696    }
    697697
    698698    private void autoresizeTagTable() {
     699        // resize table's rows and columns to fit content
    699700        if (Boolean.TRUE.equals(PROP_AUTORESIZE_TAGS_TABLE.get())) {
    700             // resize table's columns to fit content
    701701            TableHelper.computeColumnsWidth(tagTable);
    702702        }
     703        TableHelper.computeRowsHeight(tagTable);
    703704    }
    704705
    705706    private void updateUi(IPrimitive primitive) {
  • src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
    index 8124c7b60a..b6194c4c30 100644
    import static org.openstreetmap.josm.tools.I18n.tr;  
    66import static org.openstreetmap.josm.tools.I18n.trn;
    77
    88import java.awt.BorderLayout;
     9import java.awt.CardLayout;
     10import java.awt.Color;
    911import java.awt.Component;
    1012import java.awt.ComponentOrientation;
    1113import java.awt.Container;
    … … import java.util.stream.IntStream;  
    4244
    4345import javax.swing.AbstractAction;
    4446import javax.swing.Action;
     47import javax.swing.BorderFactory;
    4548import javax.swing.Box;
    4649import javax.swing.ButtonGroup;
    4750import javax.swing.ImageIcon;
    … … import javax.swing.JOptionPane;  
    5457import javax.swing.JPanel;
    5558import javax.swing.JPopupMenu;
    5659import javax.swing.JRadioButtonMenuItem;
     60import javax.swing.JScrollPane;
    5761import javax.swing.JTable;
    5862import javax.swing.KeyStroke;
    5963import javax.swing.ListCellRenderer;
    … … import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;  
    96100import org.openstreetmap.josm.gui.util.GuiHelper;
    97101import org.openstreetmap.josm.gui.util.WindowGeometry;
    98102import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
     103import org.openstreetmap.josm.gui.widgets.JosmTextArea;
    99104import org.openstreetmap.josm.gui.widgets.OrientationAction;
    100105import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
    101106import org.openstreetmap.josm.io.XmlWriter;
    … … import org.openstreetmap.josm.tools.Utils;  
    112117 * @since 5633
    113118 */
    114119public class TagEditHelper {
     120    /**
     121     * Default for {@link #MULTILINE_TAGS}.
     122     * Values taken from fields marked as textareas in https://github.com/openstreetmap/id-tagging-schema
     123     */
     124    private static final List<String> DEFAULT_MULTILINE_TAGS = Arrays.asList("defibrillator:location", "description", "fixme",
     125                                                                             "note", "comment", "inscription", "label");
     126
     127    /**
     128     * Keys of tags for which line breaks are valid characters
     129     */
     130    public static final ListProperty MULTILINE_TAGS = new ListProperty("properties.multiline-tags", DEFAULT_MULTILINE_TAGS);
    115131
    116132    private final JTable tagTable;
    117133    private final DefaultTableModel tagData;
    … … public class TagEditHelper {  
    461477     * @return the edited item with whitespaces removed
    462478     * @since 18173
    463479     */
    464     public static String getEditItem(AutoCompComboBox<AutoCompletionItem> cb) {
    465         return Utils.removeWhiteSpaces(cb.getEditorItemAsString());
     480    public static String getEditItem(JComponent cb) {
     481        if (cb instanceof JosmTextArea) {
     482            return Utils.removeWhiteSpaces(Utils.MULTILINE_WHITESPACE_PATTERN, ((JosmTextArea) cb).getText());
     483        } else {
     484            return Utils.removeWhiteSpaces(((AutoCompComboBox<AutoCompletionItem>) cb).getEditorItemAsString());
     485        }
    466486    }
    467487
    468488    /**
    … … public class TagEditHelper {  
    580600
    581601            final String selection = m.size() != 1 ? KeyedItem.DIFFERENT_I18N : m.entrySet().iterator().next().getKey();
    582602
    583             values = new AutoCompComboBox<>();
    584             values.getModel().setComparator(Comparator.naturalOrder());
    585             values.setRenderer(new TEHListCellRenderer(values, values.getRenderer(), valueCount.get(key)));
    586             values.setEditable(true);
    587             values.setPrototypeDisplayValue(new AutoCompletionItem(DUMMY));
    588             values.getModel().addAllElements(valueList);
    589             values.setSelectedItemText(selection);
     603            textCards = new CardLayout();
     604            textPanel = new JPanel(textCards);
     605
     606            // create a combobox for single line tags
     607            AutoCompComboBox<AutoCompletionItem> vals = new AutoCompComboBox<>();
     608            vals.getModel().setComparator(Comparator.naturalOrder());
     609            vals.setRenderer(new TEHListCellRenderer(vals, vals.getRenderer(), valueCount.get(key)));
     610            vals.setEditable(true);
     611            vals.setPrototypeDisplayValue(new AutoCompletionItem(DUMMY));
     612            vals.getModel().addAllElements(valueList);
     613            vals.setSelectedItemText(selection);
     614            values = vals;
     615
     616            // create a sub-panel to properly size the single line combobox
     617            JPanel singleLinePanel = new JPanel(new GridBagLayout());
     618            singleLinePanel.setBorder(BorderFactory.createLineBorder(Color.gray));
     619            singleLinePanel.add(values, GBC.eol().fill(GridBagConstraints.HORIZONTAL).anchor(GridBagConstraints.NORTH).weight(1, 1));
     620
     621            // create a text area for multiline tags
     622            values_multiline = new JosmTextArea();
     623            values_multiline.setText(selection);
     624            // Scroll multiline text area back to the top
     625            values_multiline.setCaretPosition(0);
     626            values_multiline.setRows(5);
     627            values_multiline.setFont(vals.getFont());
     628
     629            // add both cards
     630            textPanel.add(singleLinePanel, SINGLELINE);
     631            textPanel.add(new JScrollPane(values_multiline), MULTILINE);
    590632
    591633            p.add(Box.createVerticalStrut(5), GBC.eol());
    592             p.add(new JLabel(tr("Value")), GBC.std());
     634            p.add(new JLabel(tr("Value")), GBC.std().anchor(GridBagConstraints.NORTH));
    593635            p.add(Box.createHorizontalStrut(10), GBC.std());
    594             p.add(values, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
     636            p.add(textPanel, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
    595637            p.add(Box.createVerticalStrut(2), GBC.eol());
    596638
    597639            p.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
    598640            keys.applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
    599641            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
     642            values_multiline.applyComponentOrientation(values.getComponentOrientation());
     643
     644            // immediately show the multiline card if the key is multiline
     645            if (TagEditHelper.MULTILINE_TAGS.get().contains(key)) {
     646                multiline = true;
     647                textCards.show(textPanel, MULTILINE);
     648            }
    600649
    601650            setContent(mainPanel, false);
    602651
    … … public class TagEditHelper {  
    615664
    616665        @Override
    617666        public void performTagEdit() {
    618             String value = getEditItem(values);
     667            String value = getEditItem(multiline ? values_multiline : values);
    619668            value = Normalizer.normalize(value, Normalizer.Form.NFC);
    620669            if (value.isEmpty()) {
    621670                value = null; // delete the key
    … … public class TagEditHelper {  
    628677            }
    629678            if (key.equals(newkey) && KeyedItem.DIFFERENT_I18N.equals(value))
    630679                return;
    631             if (value != null && key.equals(getEditItem(keys)) && m.size() == 1 && m.containsKey(getEditItem(values)))
     680            if (value != null && key.equals(getEditItem(keys)) && m.size() == 1
     681                    && m.containsKey(getEditItem(multiline ? values_multiline : values)))
    632682                return; // see #22814: avoid to create a command that wouldn't change anything
    633683            if (key.equals(newkey) || value == null) {
    634684                UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, newkey, value));
    … … public class TagEditHelper {  
    669719    }
    670720
    671721    protected abstract class AbstractTagsDialog extends ExtendedDialog implements AutoCompListener, FocusListener, PopupMenuListener {
     722        // name to select which card to show for the value field
     723        protected static final String SINGLELINE = "singleline";
     724        protected static final String MULTILINE = "multiline";
     725
    672726        protected AutoCompComboBox<AutoCompletionItem> keys;
     727
     728        // maintain two separate components for single line and multiline values
    673729        protected AutoCompComboBox<AutoCompletionItem> values;
     730        protected JosmTextArea values_multiline;
     731
     732        // select which of the two to show with a card layout
     733        protected CardLayout textCards;
     734        protected JPanel textPanel;
     735        protected boolean multiline;
     736
    674737        protected boolean initialFocusOnKey = true;
    675738        /**
    676739         * The 'values' model is currently holding values for this key. Used for lazy-loading of values.
    … … public class TagEditHelper {  
    800863        public void focusLost(FocusEvent e) {
    801864            // update the values combobox orientation if the key changed
    802865            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
     866            values_multiline.applyComponentOrientation(values.getComponentOrientation());
    803867            // update the auto-completion setting for the given tag key
    804868            values.setAutocompleteEnabled(autocompleteValuesForKey(keys.getText()));
     869
     870            boolean previousMultiline = multiline;
     871            if (TagEditHelper.MULTILINE_TAGS.get().contains(keys.getText())) {
     872                multiline = true;
     873            } else {
     874                multiline = false;
     875
     876            }
     877
     878            if (previousMultiline != multiline) {
     879                // update which values field is shown and sync text between them
     880                if (multiline) {
     881                    values_multiline.setText(values.getText());
     882                    textCards.show(textPanel, MULTILINE);
     883                } else {
     884                    values.setText(values_multiline.getText());
     885                    textCards.show(textPanel, SINGLELINE);
     886                }
     887
     888                // user focused one values field after changing to/from a multiline key.
     889                // redirect the focus request to the text field that's now shown.
     890                if (e.getOppositeComponent() == values.getEditorComponent()) {
     891                    SwingUtilities.invokeLater(() -> values_multiline.requestFocusInWindow());
     892                } else if (e.getOppositeComponent() == values_multiline) {
     893                    SwingUtilities.invokeLater(() -> values.getEditorComponent().requestFocusInWindow());
     894                }
     895            }
    805896        }
    806897
    807898        protected void updateOkButtonIcon() {
    … … public class TagEditHelper {  
    881972            mainPanel.add(keys, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
    882973            mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol());
    883974
     975            textCards = new CardLayout();
     976            textPanel = new JPanel(textCards);
     977
     978            // create a combobox for single line tags
    884979            values = new AutoCompComboBox<>();
    885980            values.setPrototypeDisplayValue(new AutoCompletionItem(DUMMY));
    886981            values.setEditable(true);
    887982            values.getModel().setComparator(Comparator.naturalOrder());
    888983            values.setAutocompleteEnabled(AUTOCOMPLETE_VALUES.get());
    889984
    890             mainPanel.add(values, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
     985            // create a sub-panel to properly size the single line combobox
     986            JPanel singleLinePanel = new JPanel(new GridBagLayout());
     987            singleLinePanel.setBorder(BorderFactory.createLineBorder(Color.gray));
     988            singleLinePanel.add(values, GBC.eol().fill(GridBagConstraints.HORIZONTAL).anchor(GridBagConstraints.NORTH).weight(1, 1));
     989
     990            // create a text area for multiline tags
     991            values_multiline = new JosmTextArea();
     992            values_multiline.setRows(5);
     993            values_multiline.setFont(values.getFont());
     994
     995            // add both cards
     996            textPanel.add(singleLinePanel, SINGLELINE);
     997            textPanel.add(new JScrollPane(values_multiline), MULTILINE);
     998
     999            mainPanel.add(textPanel, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
    8911000
    8921001            cacheRecentTags();
    8931002            autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
    … … public class TagEditHelper {  
    9071016                    .ifPresent(tag -> {
    9081017                        keys.setSelectedItemText(tag.getKey());
    9091018                        values.setSelectedItemText(tag.getValue());
     1019                        values_multiline.setText(tag.getValue());
     1020                        // Scroll multiline text area back to the top
     1021                        values_multiline.setCaretPosition(0);
    9101022                    });
    9111023
     1024            // immediately show the multiline field if the key is multiline
     1025            if (TagEditHelper.MULTILINE_TAGS.get().contains(keys.getText())) {
     1026                multiline = true;
     1027                textCards.show(textPanel, MULTILINE);
     1028            }
     1029
    9121030
    9131031            keys.addActionListener(ignore -> updateOkButtonIcon());
    9141032            values.addActionListener(ignore -> updateOkButtonIcon());
    … … public class TagEditHelper {  
    10961214                        // fix #7951, #8298 - update list of values before setting value (?)
    10971215                        updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
    10981216                        values.setSelectedItemText(t.getValue());
    1099                         values.requestFocus();
     1217                        values_multiline.setText(t.getValue());
     1218
     1219                        if (TagEditHelper.MULTILINE_TAGS.get().contains(t.getKey())) {
     1220                            multiline = true;
     1221                            textCards.show(textPanel, MULTILINE);
     1222                            SwingUtilities.invokeLater(() -> values_multiline.requestFocusInWindow());
     1223                        } else {
     1224                            multiline = false;
     1225                            textCards.show(textPanel, SINGLELINE);
     1226                            SwingUtilities.invokeLater(() -> values.getEditorComponent().requestFocusInWindow());
     1227                        }
    11001228                    }
    11011229                };
    11021230                /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
    … … public class TagEditHelper {  
    12841412         */
    12851413        private void performTagAdding(Collection<OsmPrimitive> selection) {
    12861414            String key = getEditItem(keys);
    1287             String value = getEditItem(values);
     1415            String value = getEditItem(multiline ? values_multiline : values);
    12881416            if (key.isEmpty() || value.isEmpty())
    12891417                return;
    12901418            for (Tagged osm : selection) {
    … … public class TagEditHelper {  
    13101438        protected void clearEntries() {
    13111439            keys.getEditor().setItem("");
    13121440            values.getEditor().setItem("");
     1441            values_multiline.setText("");
    13131442        }
    13141443
    13151444        /**
  • src/org/openstreetmap/josm/gui/history/HistoryBrowser.java

    diff --git src/org/openstreetmap/josm/gui/history/HistoryBrowser.java src/org/openstreetmap/josm/gui/history/HistoryBrowser.java
    index 3c747f7fe1..8be3849e20 100644
    public class HistoryBrowser extends JPanel implements Destroyable, ChangeListene  
    176176    @Override
    177177    public void stateChanged(ChangeEvent e) {
    178178        tagInfoViewer.adjustWidths();
     179        tagInfoViewer.adjustHeights();
    179180    }
    180181}
  • src/org/openstreetmap/josm/gui/history/TagInfoViewer.java

    diff --git src/org/openstreetmap/josm/gui/history/TagInfoViewer.java src/org/openstreetmap/josm/gui/history/TagInfoViewer.java
    index 354bafcb52..4d90a3d0e4 100644
    public class TagInfoViewer extends HistoryViewerPanel {  
    123123        adjustWidths(current, maxWidth);
    124124    }
    125125
     126    /**
     127     * Use current data to adjust preferredHeight for both tables.
     128     */
     129    public void adjustHeights() {
     130        // Update row height so the content fits for both tables individually
     131        TableHelper.computeRowsHeight(reference);
     132        TableHelper.computeRowsHeight(current);
     133
     134        // Now set height to the max of the two so the rows align again
     135        for (int row = 0; row < reference.getRowCount(); row++) {
     136            int referenceHeight = reference.getRowHeight(row);
     137            int currentHeight = current.getRowHeight(row);
     138
     139            int maxHeight = Math.max(referenceHeight, currentHeight);
     140            reference.setRowHeight(row, maxHeight);
     141            current.setRowHeight(row, maxHeight);
     142        }
     143    }
     144
    126145    private static void adjustWidths(JTable table, int maxWidth) {
    127146        for (int column = 0; column < table.getColumnCount(); column++) {
    128147            TableHelper.adjustColumnWidth(table, column, maxWidth);
  • src/org/openstreetmap/josm/gui/history/TagTableCellRenderer.java

    diff --git src/org/openstreetmap/josm/gui/history/TagTableCellRenderer.java src/org/openstreetmap/josm/gui/history/TagTableCellRenderer.java
    index 6502e22337..d202e9f974 100644
    import static org.openstreetmap.josm.tools.I18n.tr;  
    55
    66import java.awt.Color;
    77import java.awt.Component;
     8import java.awt.ComponentOrientation;
    89
    910import javax.swing.BorderFactory;
    10 import javax.swing.JLabel;
    1111import javax.swing.JTable;
    12 import javax.swing.SwingConstants;
     12import javax.swing.JTextArea;
    1313import javax.swing.table.TableCellRenderer;
    1414
    1515import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
    … … import org.openstreetmap.josm.gui.util.GuiHelper;  
    1919 * The {@link TableCellRenderer} for a list of tags in {@link HistoryBrowser}
    2020 *
    2121 */
    22 public class TagTableCellRenderer extends JLabel implements TableCellRenderer {
     22public class TagTableCellRenderer extends JTextArea implements TableCellRenderer {
    2323    /**
    2424     * The background color for a selected row that has the focus.
    2525     */
    … … public class TagTableCellRenderer extends JLabel implements TableCellRenderer {  
    7373
    7474        setText(text);
    7575        setToolTipText(tooltip != null ? tooltip : text);
    76         setHorizontalAlignment(column == TagTableColumnModel.COLUMN_VERSION ? SwingConstants.TRAILING : SwingConstants.LEADING);
     76        // Abuse component orientation since JTextAreas don't have setHorizontalAlignment
     77        ComponentOrientation orientation;
     78        if (column == TagTableColumnModel.COLUMN_VERSION) {
     79            orientation = ComponentOrientation.RIGHT_TO_LEFT;
     80        } else {
     81            orientation = ComponentOrientation.UNKNOWN;
     82        }
     83        setComponentOrientation(orientation);
    7784        TwoColumnDiff.Item.DiffItemType diffItemType = model.getDiffItemType(key, column == TagTableColumnModel.COLUMN_VALUE);
    7885        GuiHelper.setBackgroundReadable(this, diffItemType.getColor(isSelected, table.hasFocus()));
    7986        return this;
  • src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java

    diff --git src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java
    index 32f83243c4..dbf0296729 100644
    import static org.openstreetmap.josm.tools.I18n.tr;  
    55
    66import java.awt.Color;
    77import java.awt.Component;
     8import java.awt.Container;
    89import java.awt.GridBagLayout;
    910import java.awt.Insets;
    1011import java.util.ArrayList;
    1112import java.util.Collection;
    1213import java.util.Collections;
    1314import java.util.List;
    14 import java.util.regex.Pattern;
    1515
    1616import javax.swing.AbstractButton;
    1717import javax.swing.BorderFactory;
    … … import javax.swing.JButton;  
    2020import javax.swing.JComponent;
    2121import javax.swing.JLabel;
    2222import javax.swing.JPanel;
     23import javax.swing.JScrollPane;
    2324import javax.swing.JToggleButton;
     25import javax.swing.JViewport;
     26import javax.swing.text.JTextComponent;
    2427
    2528import org.openstreetmap.josm.data.osm.Tag;
    2629import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
    … … import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;  
    3235import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    3336import org.openstreetmap.josm.gui.util.DocumentAdapter;
    3437import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     38import org.openstreetmap.josm.gui.widgets.JosmTextArea;
    3539import org.openstreetmap.josm.gui.widgets.JosmTextField;
    3640import org.openstreetmap.josm.gui.widgets.OrientationAction;
    3741import org.openstreetmap.josm.tools.GBC;
    … … import org.xml.sax.SAXException;  
    4650 * Text field type.
    4751 */
    4852public class Text extends KeyedItem {
    49     private static final Pattern MULTILINE_WHITESPACE_PATTERN = Pattern.compile("[\\s&&[^\n]]+");
    50 
    5153    private static int auto_increment_selected; // NOSONAR
    5254
    5355    /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */
    … … public class Text extends KeyedItem {  
    8284        }
    8385        getAllForKeys(keys).forEach(model::addElement);
    8486
    85         AutoCompTextField<AutoCompletionItem> textField;
     87        JTextComponent textField;
    8688        AutoCompComboBoxEditor<AutoCompletionItem> editor = null;
    8789
    8890        // find out if our key is already used in the selection.
    8991        Usage usage = determineTextUsage(support.getSelected(), key);
    9092
    9193        if (usage.unused() || usage.hasUniqueValue()) {
    92             textField = new AutoCompTextField<>();
     94            if (this.multiline) {
     95                JosmTextArea area = new JosmTextArea();
     96                area.setBorder((new JosmTextField()).getBorder());
     97                area.setRows(5);
     98
     99                textField = area;
     100            } else {
     101                textField = new AutoCompTextField<AutoCompletionItem>();
     102            }
    93103        } else {
    94104            editor = new AutoCompComboBoxEditor<>();
    95105            textField = editor.getEditorComponent();
    96106        }
    97         textField.setModel(model);
    98         value = textField;
    99107
    100         if (length > 0) {
    101             textField.setMaxTextLength(length);
     108        if (this.multiline) {
     109            value = new JScrollPane(textField);
     110        } else {
     111            value = textField;
     112        }
     113
     114        if (textField instanceof AutoCompTextField<?>) {
     115            AutoCompTextField<AutoCompletionItem> tf = (AutoCompTextField<AutoCompletionItem>) textField;
     116            tf.setModel(model);
     117            if (length > 0) {
     118                tf.setMaxTextLength(length);
     119            }
    102120        }
     121
    103122        if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) {
    104             textField.setHint(key);
     123            if (textField instanceof JosmTextField) {
     124                ((JosmTextField) textField).setHint(key);
     125            } else {
     126                ((JosmTextArea) textField).setHint(key);
     127            }
    105128        }
    106129        if (usage.unused()) {
    107130            if (auto_increment_selected != 0 && auto_increment != null) {
    … … public class Text extends KeyedItem {  
    123146                // selected osm primitives are tagged and filling default values feature is disabled
    124147                textField.setText("");
    125148            }
    126             value = textField;
    127149            originalValue = null;
    128150        } else if (usage.hasUniqueValue()) {
    129151            // all objects use the same value
    130152            textField.setText(usage.getFirst());
    131             value = textField;
     153            // Scroll multiline text areas back to the top
     154            textField.setCaretPosition(0);
     155
    132156            originalValue = usage.getFirst();
    133157        }
    134158        if (editor != null) {
    … … public class Text extends KeyedItem {  
    222246    private static String getValue(Component comp) {
    223247        if (comp instanceof JosmComboBox) {
    224248            return ((JosmComboBox<?>) comp).getEditorItemAsString();
    225         } else if (comp instanceof JosmTextField) {
    226             return ((JosmTextField) comp).getText();
    227         } else if (comp instanceof JPanel) {
    228             return getValue(((JPanel) comp).getComponent(0));
     249        } else if (comp instanceof JTextComponent) {
     250            return ((JTextComponent) comp).getText();
     251        } else if (comp instanceof JPanel || comp instanceof JScrollPane || comp instanceof JViewport) {
     252            return getValue(((Container) comp).getComponent(0));
    229253        } else {
    230254            return null;
    231255        }
    … … public class Text extends KeyedItem {  
    243267
    244268        if (this.normalize) {
    245269            if (this.multiline) {
    246                 v = Utils.removeWhiteSpaces(MULTILINE_WHITESPACE_PATTERN, v);
     270                v = Utils.removeWhiteSpaces(Utils.MULTILINE_WHITESPACE_PATTERN, v);
    247271            } else {
    248272                v = Utils.removeWhiteSpaces(v);
    249273            }
    … … public class Text extends KeyedItem {  
    285309        }
    286310    }
    287311
    288     private void setupListeners(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetItemGuiSupport support) {
     312    private void setupListeners(JTextComponent textField, TaggingPresetItemGuiSupport support) {
    289313        // value_templates don't work well with multiple selected items because,
    290314        // as the command queue is currently implemented, we can only save
    291315        // the same value to all selected primitives, which is probably not
  • src/org/openstreetmap/josm/gui/util/TableHelper.java

    diff --git src/org/openstreetmap/josm/gui/util/TableHelper.java src/org/openstreetmap/josm/gui/util/TableHelper.java
    index 51ede3f1d0..04a6d71523 100644
    public final class TableHelper {  
    7777        return maxwidth;
    7878    }
    7979
     80    static int getMaxHeight(JTable tbl, int row) {
     81        int maxheight = 0;
     82        for (int col = 0; col < tbl.getColumnCount(); col++) {
     83            TableCellRenderer tcr = tbl.getCellRenderer(row, col);
     84            Object val = tbl.getValueAt(row, col);
     85            Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col);
     86            maxheight = Math.max(comp.getPreferredSize().height, maxheight);
     87        }
     88        return maxheight;
     89    }
     90
    8091    /**
    8192     * adjust the preferred width of column col to the maximum preferred width of the cells (including header)
    8293     * @param tbl table
    … … public final class TableHelper {  
    106117        tbl.getColumnModel().getColumn(col).setPreferredWidth(Math.min(maxwidth+10, maxColumnWidth));
    107118    }
    108119
     120    /**
     121     * adjust the preferred height of the given row to the maximum preferred height of the cells
     122     * requires JTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
     123     * @param tbl table
     124     * @param row row index
     125     */
     126    public static void adjustRowHeight(JTable tbl, int row) {
     127        int maxheight = getMaxHeight(tbl, row);
     128        tbl.setRowHeight(row, maxheight);
     129    }
     130
    109131    /**
    110132     * adjust the table's columns to fit their content best
    111133     * requires JTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    … … public final class TableHelper {  
    118140        }
    119141    }
    120142
     143    /**
     144     * adjust the table's rows to fit their content best
     145     * requires JTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
     146     * @param tbl table
     147     */
     148    public static void computeRowsHeight(JTable tbl) {
     149        for (int row = 0; row < tbl.getRowCount(); row++) {
     150            adjustRowHeight(tbl, row);
     151        }
     152    }
     153
    121154    /**
    122155     * Returns a stream of all of the selected indices in the selection model, in increasing order.
    123156     *
  • src/org/openstreetmap/josm/gui/widgets/JosmTextArea.java

    diff --git src/org/openstreetmap/josm/gui/widgets/JosmTextArea.java src/org/openstreetmap/josm/gui/widgets/JosmTextArea.java
    index a01ef09e40..f5dddca9ca 100644
     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.gui.widgets;
    33
     4import java.awt.Color;
     5import java.awt.ComponentOrientation;
     6import java.awt.Font;
     7import java.awt.FontMetrics;
     8import java.awt.Graphics;
     9import java.awt.Graphics2D;
    410import java.awt.KeyboardFocusManager;
     11import java.awt.RenderingHints;
    512import java.awt.event.FocusEvent;
    613import java.awt.event.FocusListener;
     14import java.beans.PropertyChangeEvent;
     15import java.beans.PropertyChangeListener;
    716
     17import javax.swing.JMenuItem;
     18import javax.swing.JPopupMenu;
    819import javax.swing.JTextArea;
     20import javax.swing.RepaintManager;
     21import javax.swing.UIManager;
     22import javax.swing.text.BadLocationException;
    923import javax.swing.text.Document;
    1024
    1125import org.openstreetmap.josm.gui.MainApplication;
    1226import org.openstreetmap.josm.gui.MapFrame;
    1327import org.openstreetmap.josm.tools.Destroyable;
     28import org.openstreetmap.josm.tools.Logging;
     29import org.openstreetmap.josm.tools.Utils;
    1430
    1531/**
    1632 * Subclass of {@link JTextArea} that adds a "native" context menu (cut/copy/paste/select all).
    1733 * @since 5886
    1834 */
    19 public class JosmTextArea extends JTextArea implements Destroyable, FocusListener {
     35public class JosmTextArea extends JTextArea implements Destroyable, FocusListener, PropertyChangeListener {
    2036
    2137    private final PopupMenuLauncher launcher;
     38    private OrientationAction orientationAction;
     39    private String hint;
    2240
    2341    /**
    2442     * Constructs a new {@code JosmTextArea}. A default model is set, the initial string
    … … public class JosmTextArea extends JTextArea implements Destroyable, FocusListene  
    91109    public JosmTextArea(Document doc, String text, int rows, int columns) {
    92110        super(doc, text, rows, columns);
    93111        launcher = TextContextualPopupMenu.enableMenuFor(this, true);
     112
     113        // the menu and hotkey to change text orientation
     114        orientationAction = new OrientationAction(this);
     115        orientationAction.addPropertyChangeListener(this);
     116        JPopupMenu menu = launcher.getMenu();
     117        menu.addSeparator();
     118        menu.add(new JMenuItem(orientationAction));
     119        getInputMap().put(OrientationAction.getShortcutKey(), orientationAction);
     120
    94121        addFocusListener(this);
    95122    }
    96123
     124    /**
     125     * Replies the hint displayed when no text has been entered.
     126     * @return the hint
     127     */
     128    public final String getHint() {
     129        return hint;
     130    }
     131
     132    /**
     133     * Sets the hint to display when no text has been entered.
     134     * @param hint the hint to set
     135     * @return the old hint
     136     */
     137    public String setHint(String hint) {
     138        String old = this.hint;
     139        this.hint = hint;
     140        return old;
     141    }
     142
     143    /**
     144     * Return true if the text area should display the hint text.
     145     *
     146     * @return whether to display the hint text
     147     */
     148    public boolean displayHint() {
     149        return !Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner();
     150    }
     151
     152    /**
     153     * Returns the color for hint texts.
     154     * @return the Color for hint texts
     155     */
     156    public static Color getHintTextColor() {
     157        Color color = UIManager.getColor("TextField[Disabled].textForeground"); // Nimbus?
     158        if (color == null)
     159            color = UIManager.getColor("TextField.inactiveForeground");
     160        if (color == null)
     161            color = Color.GRAY;
     162        return color;
     163    }
     164
     165    /**
     166     * Returns the font for hint texts.
     167     * @return the font for hint texts
     168     */
     169    public static Font getHintFont() {
     170        return UIManager.getFont("TextField.font");
     171    }
     172
     173    @Override
     174    public void paintComponent(Graphics g) {
     175        super.paintComponent(g);
     176
     177        if (displayHint()) {
     178            drawHint(g);
     179        }
     180    }
     181
     182    /**
     183     * Draws the hint text over the editor component.
     184     *
     185     * @param g the graphics context
     186     */
     187    public void drawHint(Graphics g) {
     188        int x;
     189        try {
     190            x = (int) Math.round(modelToView2D(0).getX());
     191        } catch (BadLocationException exc) {
     192            Logging.trace(exc);
     193            return; // can't happen
     194        }
     195        // Taken from http://stackoverflow.com/a/24571681/2257172
     196        if (g instanceof Graphics2D) {
     197            ((Graphics2D) g).setRenderingHint(
     198                    RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
     199        }
     200        g.setColor(getHintTextColor());
     201        g.setFont(getHintFont());
     202        if (getComponentOrientation().isLeftToRight()) {
     203            g.drawString(getHint(), x, getBaseline(getWidth(), getHeight()));
     204        } else {
     205            FontMetrics metrics = g.getFontMetrics(g.getFont());
     206            int dx = metrics.stringWidth(getHint());
     207            g.drawString(getHint(), x - dx, getBaseline(getWidth(), getHeight()));
     208        }
     209        // Needed to avoid endless repaint loop if we accidentally draw over the insets. This may
     210        // easily happen because a change in text orientation invalidates the text area and
     211        // following that the preferred size gets smaller. (Bug in Swing?)
     212        RepaintManager.currentManager(this).markCompletelyClean(this);
     213    }
     214
    97215    /**
    98216     * Restore default behaviour of focus transfer with TAB, overridden by {@link JTextArea}.
    99217     * @return {@code this}
    … … public class JosmTextArea extends JTextArea implements Destroyable, FocusListene  
    112230        if (map != null) {
    113231            map.keyDetector.setEnabled(false);
    114232        }
     233
     234        repaint(); // get rid of hint
    115235    }
    116236
    117237    @Override
    … … public class JosmTextArea extends JTextArea implements Destroyable, FocusListene  
    120240        if (map != null) {
    121241            map.keyDetector.setEnabled(true);
    122242        }
     243
     244        repaint(); // paint hint
    123245    }
    124246
    125247    @Override
    … … public class JosmTextArea extends JTextArea implements Destroyable, FocusListene  
    127249        removeFocusListener(this);
    128250        TextContextualPopupMenu.disableMenuFor(this, launcher);
    129251    }
     252
     253    @Override
     254    public void propertyChange(PropertyChangeEvent evt) {
     255        // command from the menu / shortcut key
     256        if ("orientationAction".equals(evt.getPropertyName())) {
     257            setComponentOrientation((ComponentOrientation) evt.getNewValue());
     258        }
     259    }
    130260}
  • src/org/openstreetmap/josm/tools/Utils.java

    diff --git src/org/openstreetmap/josm/tools/Utils.java src/org/openstreetmap/josm/tools/Utils.java
    index fd55277504..b4c29766d1 100644
    public final class Utils {  
    7171
    7272    /** Pattern matching white spaces */
    7373    public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+", Pattern.UNICODE_CHARACTER_CLASS);
     74    public static final Pattern MULTILINE_WHITESPACE_PATTERN = Pattern.compile("[\\s&&[^\n]]+");
    7475
    7576    private static final long MILLIS_OF_SECOND = TimeUnit.SECONDS.toMillis(1);
    7677    private static final long MILLIS_OF_MINUTE = TimeUnit.MINUTES.toMillis(1);
  • test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRendererTest.java

    diff --git test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRendererTest.java test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesCellRendererTest.java
    index 45902efd40..b3bdbdfb9a 100644
    package org.openstreetmap.josm.gui.dialogs.properties;  
    33
    44import static org.junit.jupiter.api.Assertions.assertEquals;
    55
     6import java.awt.Component;
    67import java.util.function.IntFunction;
    78
    89import javax.swing.JLabel;
     10import javax.swing.JTextArea;
    911import javax.swing.JTable;
    1012import javax.swing.table.DefaultTableModel;
    1113
    … … class PropertiesCellRendererTest {  
    3234                new Object[]{"elevation", "314159"},
    3335        }, new Object[]{"key", "value"});
    3436        JTable table = new JTable(tableModel);
    35         IntFunction<String> getLabel = row -> ((JLabel) renderer.getTableCellRendererComponent(
    36                 table, table.getValueAt(row, 1), false, false, row, 1)).getText();
     37        IntFunction<String> getLabel = row -> {
     38            Component comp = renderer.getTableCellRendererComponent(
     39                table, table.getValueAt(row, 1), false, false, row, 1);
     40            if (comp instanceof JLabel) {
     41                return ((JLabel) comp).getText();
     42            } else {
     43                return ((JTextArea) comp).getText();
     44            }
     45        };
    3746        assertEquals("<html><body><span color='#FF0000'>\u25A0</span> red</body></html>", getLabel.apply(0));
    3847        assertEquals("<html><body><span color='#008000'>\u25A0</span> green</body></html>", getLabel.apply(1));
    3948        assertEquals("<html><body><span color='#123'>\u25A0</span> #123</body></html>", getLabel.apply(2));
  • test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java

    diff --git test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java
    index e03e925e57..8f2d3a5417 100644
    import java.util.Map;  
    1414
    1515import javax.swing.JComponent;
    1616import javax.swing.JPanel;
     17import javax.swing.JScrollPane;
     18import javax.swing.JViewport;
     19import javax.swing.text.JTextComponent;
    1720
    1821import org.junit.jupiter.api.Test;
    1922import org.junit.jupiter.params.ParameterizedTest;
    … … import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;  
    2427import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemTest;
    2528import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
    2629import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
     30import org.openstreetmap.josm.gui.widgets.JosmTextArea;
    2731import org.openstreetmap.josm.gui.widgets.JosmTextField;
    2832import org.openstreetmap.josm.testutils.annotations.Main;
    2933
    … … class TextTest implements TaggingPresetItemTest {  
    6670        final JPanel panel = new JPanel();
    6771        text.addToPanel(panel, TaggingPresetItemGuiSupport.create(false));
    6872        JComponent value = assertInstanceOf(JComponent.class, panel.getComponent(1));
    69         while (value instanceof JPanel) {
     73        while (value instanceof JPanel || value instanceof JScrollPane || value instanceof JViewport) {
    7074            value = (JComponent) value.getComponent(0);
    7175        }
    72         final JosmTextField textField = assertInstanceOf(JosmTextField.class, value,
    73             "Until we support multiline editing, this should be a text field");
     76        final JTextComponent textField;
     77        if (value instanceof JosmTextField) {
     78            textField = (JosmTextField) value;
     79        } else {
     80            textField = assertInstanceOf(JosmTextArea.class, value,
     81                "This should be either a JosmTextField or a JosmTextArea");
     82        }
    7483        textField.setText(toTest);
    7584        text.addCommands(changeCommands);
    7685        assertTrue(text.multiline);