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