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