001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.properties;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Color;
009import java.awt.Component;
010import java.awt.Font;
011import java.util.Collection;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Objects;
015import java.util.Optional;
016import java.util.concurrent.CopyOnWriteArrayList;
017import java.util.regex.Matcher;
018import java.util.regex.Pattern;
019
020import javax.swing.JLabel;
021import javax.swing.JTable;
022import javax.swing.UIManager;
023import javax.swing.table.DefaultTableCellRenderer;
024import javax.swing.table.TableCellRenderer;
025
026import org.openstreetmap.josm.data.osm.AbstractPrimitive;
027import org.openstreetmap.josm.data.preferences.BooleanProperty;
028import org.openstreetmap.josm.data.preferences.CachingProperty;
029import org.openstreetmap.josm.data.preferences.NamedColorProperty;
030
031/**
032 * Cell renderer of tags table.
033 * @since 6314
034 */
035public class PropertiesCellRenderer extends DefaultTableCellRenderer {
036
037    private static final CachingProperty<Color> SELECTED_FG
038            = new NamedColorProperty(marktr("Discardable key: selection Foreground"), Color.GRAY).cached();
039    private static final CachingProperty<Color> SELECTED_BG;
040    private static final CachingProperty<Color> NORMAL_FG
041            = new NamedColorProperty(marktr("Discardable key: foreground"), Color.GRAY).cached();
042    private static final CachingProperty<Color> NORMAL_BG;
043    private static final CachingProperty<Boolean> DISCARDABLE
044            = new BooleanProperty("display.discardable-keys", false).cached();
045
046    // Matches ISO-639 two and three letters language codes
047    private static final Pattern LANGUAGE_NAMES = Pattern.compile("name:(\\p{Lower}{2,3})");
048
049    static {
050        SELECTED_BG = new NamedColorProperty(marktr("Discardable key: selection Background"),
051                Optional.ofNullable(UIManager.getColor("Table.selectionBackground")).orElse(Color.BLUE)).cached();
052        NORMAL_BG = new NamedColorProperty(marktr("Discardable key: background"),
053                Optional.ofNullable(UIManager.getColor("Table.background")).orElse(Color.WHITE)).cached();
054    }
055
056    private final Collection<TableCellRenderer> customRenderer = new CopyOnWriteArrayList<>();
057
058    private static void setColors(Component c, String key, boolean isSelected) {
059
060        if (AbstractPrimitive.getDiscardableKeys().contains(key)) {
061            c.setForeground((isSelected ? SELECTED_FG : NORMAL_FG).get());
062            c.setBackground((isSelected ? SELECTED_BG : NORMAL_BG).get());
063        } else {
064            c.setForeground(UIManager.getColor("Table."+(isSelected ? "selectionF" : "f")+"oreground"));
065            c.setBackground(UIManager.getColor("Table."+(isSelected ? "selectionB" : "b")+"ackground"));
066        }
067    }
068
069    @Override
070    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
071        for (TableCellRenderer renderer : customRenderer) {
072            final Component component = renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
073            if (component != null) {
074                return component;
075            }
076        }
077        if (value == null)
078            return this;
079        Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
080        if (c instanceof JLabel) {
081            String str = null;
082            if (value instanceof String) {
083                str = (String) value;
084            } else if (value instanceof Map<?, ?>) {
085                Map<?, ?> v = (Map<?, ?>) value;
086                if (v.size() != 1) {    // Multiple values: give user a short summary of the values
087                    Integer blankCount;
088                    Integer otherCount;
089                    if (v.get("") == null) {
090                        blankCount = 0;
091                        otherCount = v.size();
092                    } else {
093                        blankCount = (Integer) v.get("");
094                        otherCount = v.size()-1;
095                    }
096                    StringBuilder sb = new StringBuilder("<");
097                    if (otherCount == 1) {
098                        // Find the non-blank value in the map
099                        v.entrySet().stream().filter(entry -> !Objects.equals(entry.getKey(), ""))
100                            /* I18n: properties display partial string joined with comma, first is count, second is value */
101                            .findAny().ifPresent(entry -> sb.append(tr("{0} ''{1}''", entry.getValue().toString(), entry.getKey())));
102                    } else {
103                        /* I18n: properties display partial string joined with comma */
104                        sb.append(trn("{0} different", "{0} different", otherCount, otherCount));
105                    }
106                    if (blankCount > 0) {
107                        /* I18n: properties display partial string joined with comma */
108                        sb.append(trn(", {0} unset", ", {0} unset", blankCount, blankCount));
109                    }
110                    sb.append('>');
111                    str = sb.toString();
112                    c.setFont(c.getFont().deriveFont(Font.ITALIC));
113
114                } else { // One value: display the value
115                    str = (String) v.entrySet().iterator().next().getKey();
116                }
117            }
118            boolean knownNameKey = false;
119            if (column == 0 && str != null) {
120                Matcher m = LANGUAGE_NAMES.matcher(str);
121                if (m.matches()) {
122                    String code = m.group(1);
123                    String label = new Locale(code).getDisplayLanguage();
124                    knownNameKey = !code.equals(label);
125                    if (knownNameKey) {
126                        str = new StringBuilder("<html><body>").append(str)
127                                .append(" <i>&lt;").append(label).append("&gt;</i></body></html>").toString();
128                    }
129                }
130            }
131            ((JLabel) c).putClientProperty("html.disable", knownNameKey ? null : Boolean.TRUE); // Fix #8730
132            ((JLabel) c).setText(str);
133            if (DISCARDABLE.get()) {
134                String key = null;
135                if (column == 0) {
136                    key = str;
137                } else if (column == 1) {
138                    Object value0 = table.getModel().getValueAt(row, 0);
139                    if (value0 instanceof String) {
140                        key = (String) value0;
141                    }
142                }
143                setColors(c, key, isSelected);
144            }
145        }
146        return c;
147    }
148
149    /**
150     * Adds a custom table cell renderer to render cells of the tags table.
151     *
152     * If the renderer is not capable performing a {@link TableCellRenderer#getTableCellRendererComponent},
153     * it should return {@code null} to fall back to the
154     * {@link PropertiesCellRenderer#getTableCellRendererComponent default implementation}.
155     * @param renderer the renderer to add
156     * @since 9149
157     */
158    public void addCustomRenderer(TableCellRenderer renderer) {
159        customRenderer.add(renderer);
160    }
161
162    /**
163     * Removes a custom table cell renderer.
164     * @param renderer the renderer to remove
165     * @since 9149
166     */
167    public void removeCustomRenderer(TableCellRenderer renderer) {
168        customRenderer.remove(renderer);
169    }
170}