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 fireTableDataChanged(); 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 boolean isCellEditable(int rowIndex, int columnIndex) { 214 return false; 215 } 216 217 @Override 218 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { 219 if (columnIndex == 1 && aValue instanceof Color) { 220 data.get(rowIndex).info.setValue((Color) aValue); 221 fireTableRowsUpdated(rowIndex, rowIndex); 222 } 223 } 224 } 225 226 /** 227 * Set the colors to be shown in the preference table. This method creates a table model if 228 * none exists and overwrites all existing values. 229 * @param colorMap the map holding the colors 230 * (key = preference key, value = {@link ColorInfo} instance) 231 */ 232 public void setColors(Map<String, ColorInfo> colorMap) { 233 if (tableModel == null) { 234 tableModel = new ColorTableModel(); 235 } 236 tableModel.clear(); 237 238 // fill model with colors: 239 colorMap.entrySet().stream() 240 .map(e -> new ColorEntry(e.getKey(), e.getValue())) 241 .sorted((e1, e2) -> { 242 int cat = Integer.compare( 243 getCategroyPriority(e1.info.getCategory()), 244 getCategroyPriority(e2.info.getCategory())); 245 if (cat != 0) return cat; 246 return Collator.getInstance().compare(e1.getDisplay(), e2.getDisplay()); 247 }) 248 .forEach(tableModel::addEntry); 249 250 if (this.colors != null) { 251 this.colors.repaint(); 252 } 253 254 } 255 256 private static int getCategroyPriority(String category) { 257 switch (category) { 258 case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1; 259 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2; 260 case NamedColorProperty.COLOR_CATEGORY_LAYER: return 3; 261 default: return 4; 262 } 263 } 264 265 /** 266 * Returns a map with the colors in the table (key = preference key, value = color info). 267 * @return a map holding the colors. 268 */ 269 public Map<String, ColorInfo> getColors() { 270 return tableModel.getData().stream().collect(Collectors.toMap(e -> e.key, e -> e.info)); 271 } 272 273 @Override 274 public void addGui(final PreferenceTabbedPane gui) { 275 fixColorPrefixes(); 276 setColors(Main.pref.getAllNamedColors()); 277 278 colorEdit = new JButton(tr("Choose")); 279 colorEdit.addActionListener(e -> { 280 int sel = colors.getSelectedRow(); 281 ColorEntry ce = tableModel.getEntry(sel); 282 JColorChooser chooser = new JColorChooser(ce.getDisplayColor()); 283 int answer = JOptionPane.showConfirmDialog( 284 gui, chooser, 285 tr("Choose a color for {0}", ce.getDisplay()), 286 JOptionPane.OK_CANCEL_OPTION, 287 JOptionPane.PLAIN_MESSAGE); 288 if (answer == JOptionPane.OK_OPTION) { 289 colors.setValueAt(chooser.getColor(), sel, 1); 290 } 291 }); 292 defaultSet = new JButton(tr("Set to default")); 293 defaultSet.addActionListener(e -> { 294 int sel = colors.getSelectedRow(); 295 ColorEntry ce = tableModel.getEntry(sel); 296 Color c = ce.info.getDefaultValue(); 297 if (c != null) { 298 colors.setValueAt(c, sel, 1); 299 } 300 }); 301 JButton defaultAll = new JButton(tr("Set all to default")); 302 defaultAll.addActionListener(e -> { 303 List<ColorEntry> data = tableModel.getData(); 304 for (int i = 0; i < data.size(); ++i) { 305 ColorEntry ce = data.get(i); 306 Color c = ce.info.getDefaultValue(); 307 if (c != null) { 308 colors.setValueAt(c, i, 1); 309 } 310 } 311 }); 312 remove = new JButton(tr("Remove")); 313 remove.addActionListener(e -> { 314 int sel = colors.getSelectedRow(); 315 tableModel.removeEntry(sel); 316 }); 317 remove.setEnabled(false); 318 colorEdit.setEnabled(false); 319 defaultSet.setEnabled(false); 320 321 colors = new JTable(tableModel); 322 colors.addMouseListener(new MouseAdapter() { 323 @Override 324 public void mousePressed(MouseEvent me) { 325 if (me.getClickCount() == 2) { 326 colorEdit.doClick(); 327 } 328 } 329 }); 330 colors.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 331 colors.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() { 332 @Override 333 public Component getTableCellRendererComponent( 334 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 335 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 336 if (value != null && comp instanceof JLabel) { 337 JLabel label = (JLabel) comp; 338 ColorEntry e = (ColorEntry) value; 339 label.setText(e.getDisplay()); 340 if (!e.isDefault()) { 341 label.setFont(label.getFont().deriveFont(Font.BOLD)); 342 } else { 343 label.setFont(label.getFont().deriveFont(Font.PLAIN)); 344 } 345 return label; 346 } 347 return comp; 348 } 349 }); 350 colors.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer() { 351 @Override 352 public Component getTableCellRendererComponent( 353 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 354 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 355 if (value != null && comp instanceof JLabel) { 356 JLabel label = (JLabel) comp; 357 Color c = (Color) value; 358 label.setText(ColorHelper.color2html(c)); 359 GuiHelper.setBackgroundReadable(label, c); 360 label.setOpaque(true); 361 return label; 362 } 363 return comp; 364 } 365 }); 366 colors.getColumnModel().getColumn(1).setWidth(100); 367 colors.setToolTipText(tr("Colors used by different objects in JOSM.")); 368 colors.setPreferredScrollableViewportSize(new Dimension(100, 112)); 369 370 colors.getSelectionModel().addListSelectionListener(this); 371 colors.getModel().addTableModelListener(this); 372 373 JPanel panel = new JPanel(new GridBagLayout()); 374 panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 375 JScrollPane scrollpane = new JScrollPane(colors); 376 scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 377 panel.add(scrollpane, GBC.eol().fill(GBC.BOTH)); 378 JPanel buttonPanel = new JPanel(new GridBagLayout()); 379 panel.add(buttonPanel, GBC.eol().insets(5, 0, 5, 5).fill(GBC.HORIZONTAL)); 380 buttonPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); 381 buttonPanel.add(colorEdit, GBC.std().insets(0, 5, 0, 0)); 382 buttonPanel.add(defaultSet, GBC.std().insets(5, 5, 5, 0)); 383 buttonPanel.add(defaultAll, GBC.std().insets(0, 5, 0, 0)); 384 buttonPanel.add(remove, GBC.std().insets(0, 5, 0, 0)); 385 gui.getDisplayPreference().addSubTab(this, tr("Colors"), panel); 386 } 387 388 private boolean isRemoveColor(ColorEntry ce) { 389 return ce.info.getCategory().equals(NamedColorProperty.COLOR_CATEGORY_LAYER); 390 } 391 392 /** 393 * Add all missing color entries. 394 */ 395 private static void fixColorPrefixes() { 396 PaintColors.values(); 397 ConflictColors.getColors(); 398 Severity.getColors(); 399 MarkerLayer.getGenericColor(); 400 GpxDrawHelper.getGenericColor(); 401 OsmDataLayer.getOutsideColor(); 402 MapScaler.getColor(); 403 MapStatus.getColors(); 404 ConflictDialog.getColor(); 405 } 406 407 @Override 408 public boolean ok() { 409 boolean ret = false; 410 for (ColorEntry d : tableModel.getDeleted()) { 411 d.toProperty().remove(); 412 } 413 for (ColorEntry e : tableModel.getData()) { 414 if (e.info.getValue() != null) { 415 if (e.toProperty().put(e.info.getValue()) 416 && e.key.startsWith("mappaint.")) { 417 ret = true; 418 } 419 } 420 } 421 OsmDataLayer.createHatchTexture(); 422 return ret; 423 } 424 425 @Override 426 public boolean isExpert() { 427 return false; 428 } 429 430 @Override 431 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 432 return gui.getDisplayPreference(); 433 } 434 435 @Override 436 public void valueChanged(ListSelectionEvent e) { 437 updateEnabledState(); 438 } 439 440 @Override 441 public void tableChanged(TableModelEvent e) { 442 updateEnabledState(); 443 } 444 445 private void updateEnabledState() { 446 int sel = colors.getSelectedRow(); 447 ColorEntry ce = sel >= 0 ? tableModel.getEntry(sel) : null; 448 remove.setEnabled(ce != null && isRemoveColor(ce)); 449 colorEdit.setEnabled(ce != null); 450 defaultSet.setEnabled(ce != null && !ce.isDefault()); 451 } 452}