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