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><").append(label).append("></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}