001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.display; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.GridBagLayout; 011import java.awt.event.MouseAdapter; 012import java.awt.event.MouseEvent; 013import java.text.Collator; 014import java.util.ArrayList; 015import java.util.List; 016import java.util.Map; 017import java.util.Objects; 018import java.util.Optional; 019import java.util.stream.Collectors; 020 021import javax.swing.BorderFactory; 022import javax.swing.Box; 023import javax.swing.JButton; 024import javax.swing.JColorChooser; 025import javax.swing.JLabel; 026import javax.swing.JOptionPane; 027import javax.swing.JPanel; 028import javax.swing.JScrollPane; 029import javax.swing.JTable; 030import javax.swing.ListSelectionModel; 031import javax.swing.event.ListSelectionEvent; 032import javax.swing.event.ListSelectionListener; 033import javax.swing.event.TableModelEvent; 034import javax.swing.event.TableModelListener; 035import javax.swing.table.AbstractTableModel; 036import javax.swing.table.DefaultTableCellRenderer; 037 038import org.openstreetmap.josm.Main; 039import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 040import org.openstreetmap.josm.data.preferences.ColorInfo; 041import org.openstreetmap.josm.data.preferences.NamedColorProperty; 042import org.openstreetmap.josm.data.validation.Severity; 043import org.openstreetmap.josm.gui.MapScaler; 044import org.openstreetmap.josm.gui.MapStatus; 045import org.openstreetmap.josm.gui.conflict.ConflictColors; 046import org.openstreetmap.josm.gui.dialogs.ConflictDialog; 047import org.openstreetmap.josm.gui.layer.OsmDataLayer; 048import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper; 049import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 050import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 051import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 052import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 053import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting; 054import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting; 055import org.openstreetmap.josm.gui.util.GuiHelper; 056import org.openstreetmap.josm.tools.CheckParameterUtil; 057import org.openstreetmap.josm.tools.ColorHelper; 058import org.openstreetmap.josm.tools.GBC; 059import org.openstreetmap.josm.tools.I18n; 060 061/** 062 * Color preferences. 063 * 064 * GUI preference to let the user customize named colors. 065 * @see NamedColorProperty 066 */ 067public class ColorPreference implements SubPreferenceSetting, ListSelectionListener, TableModelListener { 068 069 /** 070 * Factory used to create a new {@code ColorPreference}. 071 */ 072 public static class Factory implements PreferenceSettingFactory { 073 @Override 074 public PreferenceSetting createPreferenceSetting() { 075 return new ColorPreference(); 076 } 077 } 078 079 private ColorTableModel tableModel; 080 private JTable colors; 081 082 private JButton colorEdit; 083 private JButton defaultSet; 084 private JButton remove; 085 086 private static class ColorEntry { 087 String key; 088 ColorInfo info; 089 090 ColorEntry(String key, ColorInfo info) { 091 CheckParameterUtil.ensureParameterNotNull(key, "key"); 092 CheckParameterUtil.ensureParameterNotNull(info, "info"); 093 this.key = key; 094 this.info = info; 095 } 096 097 /** 098 * Get a description of the color based on the given info. 099 * @return a description of the color 100 */ 101 public String getDisplay() { 102 switch (info.getCategory()) { 103 case NamedColorProperty.COLOR_CATEGORY_LAYER: 104 String v = null; 105 if (info.getSource() != null) { 106 v = info.getSource(); 107 } 108 if (!info.getName().isEmpty()) { 109 if (v == null) { 110 v = tr(I18n.escape(info.getName())); 111 } else { 112 v += " - " + tr(I18n.escape(info.getName())); 113 } 114 } 115 return tr("Layer: {0}", v); 116 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: 117 if (info.getSource() != null) 118 return tr("Paint style {0}: {1}", tr(I18n.escape(info.getSource())), tr(info.getName())); 119 // fall through 120 default: 121 if (info.getSource() != null) 122 return tr(I18n.escape(info.getSource())) + " - " + tr(I18n.escape(info.getName())); 123 else 124 return tr(I18n.escape(info.getName())); 125 } 126 } 127 128 /** 129 * Get the color value to display. 130 * Either value (if set) or default value. 131 * @return the color value to display 132 */ 133 public Color getDisplayColor() { 134 return Optional.ofNullable(info.getValue()).orElse(info.getDefaultValue()); 135 } 136 137 /** 138 * Check if color has been customized by the user or not. 139 * @return true if the color is at its default value, false if it is customized by the user. 140 */ 141 public boolean isDefault() { 142 return info.getValue() == null || Objects.equals(info.getValue(), info.getDefaultValue()); 143 } 144 145 /** 146 * Convert to a {@link NamedColorProperty}. 147 * @return a {@link NamedColorProperty} 148 */ 149 public NamedColorProperty toProperty() { 150 return new NamedColorProperty(info.getCategory(), info.getSource(), 151 info.getName(), info.getDefaultValue()); 152 } 153 } 154 155 private static class ColorTableModel extends AbstractTableModel { 156 157 private final List<ColorEntry> data; 158 private final List<ColorEntry> deleted; 159 160 ColorTableModel() { 161 this.data = new ArrayList<>(); 162 this.deleted = new ArrayList<>(); 163 } 164 165 public void addEntry(ColorEntry entry) { 166 data.add(entry); 167 } 168 169 public void removeEntry(int row) { 170 deleted.add(data.get(row)); 171 data.remove(row); 172 fireTableRowsDeleted(row, row); 173 } 174 175 public ColorEntry getEntry(int row) { 176 return data.get(row); 177 } 178 179 public List<ColorEntry> getData() { 180 return data; 181 } 182 183 public List<ColorEntry> getDeleted() { 184 return deleted; 185 } 186 187 public void clear() { 188 data.clear(); 189 deleted.clear(); 190 } 191 192 @Override 193 public int getRowCount() { 194 return data.size(); 195 } 196 197 @Override 198 public int getColumnCount() { 199 return 2; 200 } 201 202 @Override 203 public Object getValueAt(int rowIndex, int columnIndex) { 204 return columnIndex == 0 ? data.get(rowIndex) : data.get(rowIndex).getDisplayColor(); 205 } 206 207 @Override 208 public String getColumnName(int column) { 209 return column == 0 ? tr("Name") : tr("Color"); 210 } 211 212 @Override 213 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { 214 if (columnIndex == 1 && aValue instanceof Color) { 215 data.get(rowIndex).info.setValue((Color) aValue); 216 fireTableRowsUpdated(rowIndex, rowIndex); 217 } 218 } 219 } 220 221 /** 222 * Set the colors to be shown in the preference table. This method creates a table model if 223 * none exists and overwrites all existing values. 224 * @param colorMap the map holding the colors 225 * (key = preference key, value = {@link ColorInfo} instance) 226 */ 227 public void setColors(Map<String, ColorInfo> colorMap) { 228 if (tableModel == null) { 229 tableModel = new ColorTableModel(); 230 } 231 tableModel.clear(); 232 233 // fill model with colors: 234 colorMap.entrySet().stream() 235 .map(e -> new ColorEntry(e.getKey(), e.getValue())) 236 .sorted((e1, e2) -> { 237 int cat = Integer.compare( 238 getCategroyPriority(e1.info.getCategory()), 239 getCategroyPriority(e2.info.getCategory())); 240 if (cat != 0) return cat; 241 return Collator.getInstance().compare(e1.getDisplay(), e2.getDisplay()); 242 }) 243 .forEach(tableModel::addEntry); 244 245 if (this.colors != null) { 246 this.colors.repaint(); 247 } 248 249 } 250 251 private static int getCategroyPriority(String category) { 252 switch (category) { 253 case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1; 254 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2; 255 case NamedColorProperty.COLOR_CATEGORY_LAYER: return 3; 256 default: return 4; 257 } 258 } 259 260 /** 261 * Returns a map with the colors in the table (key = preference key, value = color info). 262 * @return a map holding the colors. 263 */ 264 public Map<String, ColorInfo> getColors() { 265 return tableModel.getData().stream().collect(Collectors.toMap(e -> e.key, e -> e.info)); 266 } 267 268 @Override 269 public void addGui(final PreferenceTabbedPane gui) { 270 fixColorPrefixes(); 271 setColors(Main.pref.getAllNamedColors()); 272 273 colorEdit = new JButton(tr("Choose")); 274 colorEdit.addActionListener(e -> { 275 int sel = colors.getSelectedRow(); 276 ColorEntry ce = tableModel.getEntry(sel); 277 JColorChooser chooser = new JColorChooser(ce.getDisplayColor()); 278 int answer = JOptionPane.showConfirmDialog( 279 gui, chooser, 280 tr("Choose a color for {0}", ce.getDisplay()), 281 JOptionPane.OK_CANCEL_OPTION, 282 JOptionPane.PLAIN_MESSAGE); 283 if (answer == JOptionPane.OK_OPTION) { 284 colors.setValueAt(chooser.getColor(), sel, 1); 285 } 286 }); 287 defaultSet = new JButton(tr("Reset to default")); 288 defaultSet.addActionListener(e -> { 289 int sel = colors.getSelectedRow(); 290 ColorEntry ce = tableModel.getEntry(sel); 291 Color c = ce.info.getDefaultValue(); 292 if (c != null) { 293 colors.setValueAt(c, sel, 1); 294 } 295 }); 296 JButton defaultAll = new JButton(tr("Set all to default")); 297 defaultAll.addActionListener(e -> { 298 List<ColorEntry> data = tableModel.getData(); 299 for (int i = 0; i < data.size(); ++i) { 300 ColorEntry ce = data.get(i); 301 Color c = ce.info.getDefaultValue(); 302 if (c != null) { 303 colors.setValueAt(c, i, 1); 304 } 305 } 306 }); 307 remove = new JButton(tr("Remove")); 308 remove.addActionListener(e -> { 309 int sel = colors.getSelectedRow(); 310 tableModel.removeEntry(sel); 311 }); 312 remove.setEnabled(false); 313 colorEdit.setEnabled(false); 314 defaultSet.setEnabled(false); 315 316 colors = new JTable(tableModel); 317 colors.addMouseListener(new MouseAdapter() { 318 @Override 319 public void mousePressed(MouseEvent me) { 320 if (me.getClickCount() == 2) { 321 colorEdit.doClick(); 322 } 323 } 324 }); 325 colors.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 326 colors.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() { 327 @Override 328 public Component getTableCellRendererComponent( 329 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 330 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 331 if (value != null && comp instanceof JLabel) { 332 JLabel label = (JLabel) comp; 333 ColorEntry e = (ColorEntry) value; 334 label.setText(e.getDisplay()); 335 if (!e.isDefault()) { 336 label.setFont(label.getFont().deriveFont(Font.BOLD)); 337 } else { 338 label.setFont(label.getFont().deriveFont(Font.PLAIN)); 339 } 340 return label; 341 } 342 return comp; 343 } 344 }); 345 colors.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer() { 346 @Override 347 public Component getTableCellRendererComponent( 348 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 349 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 350 if (value != null && comp instanceof JLabel) { 351 JLabel label = (JLabel) comp; 352 Color c = (Color) value; 353 label.setText(ColorHelper.color2html(c)); 354 GuiHelper.setBackgroundReadable(label, c); 355 label.setOpaque(true); 356 return label; 357 } 358 return comp; 359 } 360 }); 361 colors.getColumnModel().getColumn(1).setWidth(100); 362 colors.setToolTipText(tr("Colors used by different objects in JOSM.")); 363 colors.setPreferredScrollableViewportSize(new Dimension(100, 112)); 364 365 colors.getSelectionModel().addListSelectionListener(this); 366 colors.getModel().addTableModelListener(this); 367 368 JPanel panel = new JPanel(new GridBagLayout()); 369 panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 370 JScrollPane scrollpane = new JScrollPane(colors); 371 scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 372 panel.add(scrollpane, GBC.eol().fill(GBC.BOTH)); 373 JPanel buttonPanel = new JPanel(new GridBagLayout()); 374 panel.add(buttonPanel, GBC.eol().insets(5, 0, 5, 5).fill(GBC.HORIZONTAL)); 375 buttonPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); 376 buttonPanel.add(colorEdit, GBC.std().insets(0, 5, 0, 0)); 377 buttonPanel.add(defaultSet, GBC.std().insets(5, 5, 5, 0)); 378 buttonPanel.add(defaultAll, GBC.std().insets(0, 5, 0, 0)); 379 buttonPanel.add(remove, GBC.std().insets(0, 5, 0, 0)); 380 gui.getDisplayPreference().addSubTab(this, tr("Colors"), panel); 381 } 382 383 private static boolean isRemoveColor(ColorEntry ce) { 384 return NamedColorProperty.COLOR_CATEGORY_LAYER.equals(ce.info.getCategory()); 385 } 386 387 /** 388 * Add all missing color entries. 389 */ 390 private static void fixColorPrefixes() { 391 PaintColors.values(); 392 ConflictColors.getColors(); 393 Severity.getColors(); 394 MarkerLayer.getGenericColor(); 395 GpxDrawHelper.getGenericColor(); 396 OsmDataLayer.getOutsideColor(); 397 MapScaler.getColor(); 398 MapStatus.getColors(); 399 ConflictDialog.getColor(); 400 } 401 402 @Override 403 public boolean ok() { 404 boolean ret = false; 405 for (ColorEntry d : tableModel.getDeleted()) { 406 d.toProperty().remove(); 407 } 408 for (ColorEntry e : tableModel.getData()) { 409 if (e.info.getValue() != null) { 410 if (e.toProperty().put(e.info.getValue()) 411 && e.key.startsWith("mappaint.")) { 412 ret = true; 413 } 414 } 415 } 416 OsmDataLayer.createHatchTexture(); 417 return ret; 418 } 419 420 @Override 421 public boolean isExpert() { 422 return false; 423 } 424 425 @Override 426 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 427 return gui.getDisplayPreference(); 428 } 429 430 @Override 431 public void valueChanged(ListSelectionEvent e) { 432 updateEnabledState(); 433 } 434 435 @Override 436 public void tableChanged(TableModelEvent e) { 437 updateEnabledState(); 438 } 439 440 private void updateEnabledState() { 441 int sel = colors.getSelectedRow(); 442 ColorEntry ce = sel >= 0 && sel < tableModel.getRowCount() ? tableModel.getEntry(sel) : null; 443 remove.setEnabled(ce != null && isRemoveColor(ce)); 444 colorEdit.setEnabled(ce != null); 445 defaultSet.setEnabled(ce != null && !ce.isDefault()); 446 } 447}