001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.display; 003 004import static java.awt.GridBagConstraints.BOTH; 005import static java.awt.GridBagConstraints.HORIZONTAL; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.Color; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.Font; 012import java.awt.GridBagLayout; 013import java.awt.event.MouseAdapter; 014import java.awt.event.MouseEvent; 015import java.text.Collator; 016import java.util.ArrayList; 017import java.util.List; 018import java.util.Map; 019import java.util.Objects; 020import java.util.Optional; 021import java.util.stream.Collectors; 022 023import javax.swing.BorderFactory; 024import javax.swing.Box; 025import javax.swing.JButton; 026import javax.swing.JColorChooser; 027import javax.swing.JLabel; 028import javax.swing.JOptionPane; 029import javax.swing.JPanel; 030import javax.swing.JScrollPane; 031import javax.swing.JTable; 032import javax.swing.ListSelectionModel; 033import javax.swing.event.ListSelectionEvent; 034import javax.swing.event.ListSelectionListener; 035import javax.swing.event.TableModelEvent; 036import javax.swing.event.TableModelListener; 037import javax.swing.table.AbstractTableModel; 038import javax.swing.table.DefaultTableCellRenderer; 039 040import org.openstreetmap.josm.data.Preferences; 041import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 042import org.openstreetmap.josm.data.preferences.ColorInfo; 043import org.openstreetmap.josm.data.preferences.NamedColorProperty; 044import org.openstreetmap.josm.data.validation.Severity; 045import org.openstreetmap.josm.gui.MapScaler; 046import org.openstreetmap.josm.gui.MapStatus; 047import org.openstreetmap.josm.gui.conflict.ConflictColors; 048import org.openstreetmap.josm.gui.dialogs.ConflictDialog; 049import org.openstreetmap.josm.gui.help.HelpUtil; 050import org.openstreetmap.josm.gui.layer.OsmDataLayer; 051import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper; 052import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 053import org.openstreetmap.josm.gui.preferences.ExtensibleTabPreferenceSetting; 054import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 055import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 056import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 057import org.openstreetmap.josm.gui.preferences.advanced.PreferencesTable; 058import org.openstreetmap.josm.gui.util.GuiHelper; 059import org.openstreetmap.josm.gui.util.TableHelper; 060import org.openstreetmap.josm.gui.widgets.FilterField; 061import org.openstreetmap.josm.tools.CheckParameterUtil; 062import org.openstreetmap.josm.tools.ColorHelper; 063import org.openstreetmap.josm.tools.GBC; 064import org.openstreetmap.josm.tools.I18n; 065import org.openstreetmap.josm.tools.ImageProvider; 066 067/** 068 * Color preferences. 069 * 070 * GUI preference to let the user customize named colors. 071 * @see NamedColorProperty 072 */ 073public class ColorPreference extends ExtensibleTabPreferenceSetting implements ListSelectionListener, TableModelListener { 074 075 /** 076 * Factory used to create a new {@code ColorPreference}. 077 */ 078 public static class Factory implements PreferenceSettingFactory { 079 @Override 080 public PreferenceSetting createPreferenceSetting() { 081 return new ColorPreference(); 082 } 083 } 084 085 ColorPreference() { 086 super(/* ICON(preferences/) */ "color", 087 tr("Colors"), tr("Change colors used in program dialogs and in map paint styles.")); 088 } 089 090 private ColorTableModel tableModel; 091 private JTable colors; 092 093 private JButton colorEdit; 094 private JButton defaultSet; 095 private JButton remove; 096 097 private static class ColorEntry { 098 String key; 099 ColorInfo info; 100 101 ColorEntry(String key, ColorInfo info) { 102 CheckParameterUtil.ensureParameterNotNull(key, "key"); 103 CheckParameterUtil.ensureParameterNotNull(info, "info"); 104 this.key = key; 105 this.info = info; 106 } 107 108 /** 109 * Get a description of the color based on the given info. 110 * @return a description of the color 111 */ 112 public String getDisplay() { 113 switch (info.getCategory()) { 114 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: 115 if (info.getSource() != null) 116 return tr("Paint style {0}: {1}", tr(I18n.escape(info.getSource())), tr(info.getName())); 117 // fall through 118 default: 119 if (info.getSource() != null) 120 return tr(I18n.escape(info.getSource())) + " - " + tr(I18n.escape(info.getName())); 121 else 122 return tr(I18n.escape(info.getName())); 123 } 124 } 125 126 /** 127 * Get the color value to display. 128 * Either value (if set) or default value. 129 * @return the color value to display 130 */ 131 public Color getDisplayColor() { 132 return Optional.ofNullable(info.getValue()).orElse(info.getDefaultValue()); 133 } 134 135 /** 136 * Check if color has been customized by the user or not. 137 * @return true if the color is at its default value, false if it is customized by the user. 138 */ 139 public boolean isDefault() { 140 return info.getValue() == null || Objects.equals(info.getValue(), info.getDefaultValue()); 141 } 142 143 /** 144 * Convert to a {@link NamedColorProperty}. 145 * @return a {@link NamedColorProperty} 146 */ 147 public NamedColorProperty toProperty() { 148 return new NamedColorProperty(info.getCategory(), info.getSource(), 149 info.getName(), info.getDefaultValue()); 150 } 151 152 @Override 153 public String toString() { 154 return "ColorEntry{" + getDisplay() + ' ' + ColorHelper.color2html(getDisplayColor()) + '}'; 155 } 156 } 157 158 private static class ColorTableModel extends AbstractTableModel { 159 160 private final List<ColorEntry> data; 161 private final List<ColorEntry> deleted; 162 163 ColorTableModel() { 164 this.data = new ArrayList<>(); 165 this.deleted = new ArrayList<>(); 166 } 167 168 public void addEntry(ColorEntry entry) { 169 data.add(entry); 170 } 171 172 public void removeEntry(int row) { 173 deleted.add(data.get(row)); 174 data.remove(row); 175 fireTableRowsDeleted(row, row); 176 } 177 178 public ColorEntry getEntry(int row) { 179 return data.get(row); 180 } 181 182 public List<ColorEntry> getData() { 183 return data; 184 } 185 186 public List<ColorEntry> getDeleted() { 187 return deleted; 188 } 189 190 public void clear() { 191 data.clear(); 192 deleted.clear(); 193 } 194 195 @Override 196 public int getRowCount() { 197 return data.size(); 198 } 199 200 @Override 201 public int getColumnCount() { 202 return 2; 203 } 204 205 @Override 206 public Object getValueAt(int rowIndex, int columnIndex) { 207 return columnIndex == 0 ? data.get(rowIndex) : data.get(rowIndex).getDisplayColor(); 208 } 209 210 @Override 211 public String getColumnName(int column) { 212 return column == 0 ? tr("Name") : tr("Color"); 213 } 214 215 @Override 216 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { 217 if (columnIndex == 1 && aValue instanceof Color) { 218 data.get(rowIndex).info.setValue((Color) aValue); 219 fireTableCellUpdated(rowIndex, columnIndex); 220 } 221 } 222 } 223 224 /** 225 * Set the colors to be shown in the preference table. This method creates a table model if 226 * none exists and overwrites all existing values. 227 * @param colorMap the map holding the colors 228 * (key = preference key, value = {@link ColorInfo} instance) 229 */ 230 public void setColors(Map<String, ColorInfo> colorMap) { 231 if (tableModel == null) { 232 tableModel = new ColorTableModel(); 233 } 234 tableModel.clear(); 235 236 // fill model with colors: 237 colorMap.entrySet().stream() 238 .map(e -> new ColorEntry(e.getKey(), e.getValue())) 239 .sorted((e1, e2) -> { 240 int cat = Integer.compare( 241 getCategoryPriority(e1.info.getCategory()), 242 getCategoryPriority(e2.info.getCategory())); 243 if (cat != 0) return cat; 244 return Collator.getInstance().compare(e1.getDisplay(), e2.getDisplay()); 245 }) 246 .forEach(tableModel::addEntry); 247 248 if (this.colors != null) { 249 this.colors.repaint(); 250 } 251 } 252 253 private static int getCategoryPriority(String category) { 254 switch (category) { 255 case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1; 256 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2; 257 default: return 3; 258 } 259 } 260 261 /** 262 * Returns a map with the colors in the table (key = preference key, value = color info). 263 * @return a map holding the colors. 264 */ 265 public Map<String, ColorInfo> getColors() { 266 return tableModel.getData().stream().collect(Collectors.toMap(e -> e.key, e -> e.info)); 267 } 268 269 @Override 270 public void addGui(final PreferenceTabbedPane gui) { 271 fixColorPrefixes(); 272 setColors(Preferences.main().getAllNamedColors()); 273 274 colorEdit = new JButton(tr("Choose"), ImageProvider.get("colorchooser", ImageProvider.ImageSizes.SMALLICON)); 275 colorEdit.addActionListener(e -> { 276 int sel = colors.getSelectedRow(); 277 ColorEntry ce = (ColorEntry) colors.getValueAt(sel, 0); 278 JColorChooser chooser = new JColorChooser(ce.getDisplayColor()); 279 int answer = JOptionPane.showConfirmDialog( 280 gui, chooser, 281 tr("Choose a color for {0}", ce.getDisplay()), 282 JOptionPane.OK_CANCEL_OPTION, 283 JOptionPane.PLAIN_MESSAGE); 284 if (answer == JOptionPane.OK_OPTION) { 285 colors.setValueAt(chooser.getColor(), sel, 1); 286 } 287 }); 288 defaultSet = new JButton(tr("Reset to default"), ImageProvider.get("undo", ImageProvider.ImageSizes.SMALLICON)); 289 defaultSet.addActionListener(e -> { 290 int sel = colors.getSelectedRow(); 291 ColorEntry ce = (ColorEntry) colors.getValueAt(sel, 0); 292 Color c = ce.info.getDefaultValue(); 293 if (c != null) { 294 colors.setValueAt(c, sel, 1); 295 } 296 }); 297 JButton defaultAll = new JButton(tr("Set all to default"), ImageProvider.get("undo", ImageProvider.ImageSizes.SMALLICON)); 298 defaultAll.addActionListener(e -> { 299 List<ColorEntry> data = tableModel.getData(); 300 for (ColorEntry ce : data) { 301 Color c = ce.info.getDefaultValue(); 302 if (c != null) { 303 ce.info.setValue(c); 304 } 305 } 306 tableModel.fireTableDataChanged(); 307 }); 308 remove = new JButton(tr("Remove"), ImageProvider.get("dialogs/delete", ImageProvider.ImageSizes.SMALLICON)); 309 remove.addActionListener(e -> { 310 int sel = colors.getSelectedRow(); 311 sel = colors.convertRowIndexToModel(sel); 312 tableModel.removeEntry(sel); 313 }); 314 remove.setEnabled(false); 315 colorEdit.setEnabled(false); 316 defaultSet.setEnabled(false); 317 318 colors = new JTable(tableModel); 319 TableHelper.setFont(colors, PreferencesTable.class); 320 colors.setAutoCreateRowSorter(true); 321 FilterField colorFilter = new FilterField().filter(colors, 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 panel.add(colorFilter, GBC.eol().insets(0, 0, 0, 5).fill(HORIZONTAL)); 376 JScrollPane scrollpane = new JScrollPane(colors); 377 panel.add(scrollpane, GBC.eol().fill(BOTH)); 378 JPanel buttonPanel = new JPanel(new GridBagLayout()); 379 panel.add(buttonPanel, GBC.eol().insets(5, 0, 5, 5).fill(HORIZONTAL)); 380 buttonPanel.add(Box.createHorizontalGlue(), GBC.std().fill(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 386 getTabPane().addTab(tr("Colors"), panel); 387 super.addGui(gui); 388 } 389 390 @SuppressWarnings({"PMD.UnusedFormalParameter", "UnusedVariable"}) 391 private static boolean isRemoveColor(ColorEntry ce) { 392 return false; 393 //COLOR_CATEGORY_LAYER is no longer supported and was the only one that could be removed. 394 //Maybe this is useful for other categories in the future. 395 //return NamedColorProperty.COLOR_CATEGORY_LAYER.equals(ce.info.getCategory()); 396 } 397 398 /** 399 * Add all missing color entries. 400 */ 401 private static void fixColorPrefixes() { 402 PaintColors.values(); 403 ConflictColors.getColors(); 404 Severity.getColors(); 405 MarkerLayer.DEFAULT_COLOR_PROPERTY.get(); 406 GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get(); 407 OsmDataLayer.getOutsideColor(); 408 MapScaler.getColor(); 409 MapStatus.getColors(); 410 ConflictDialog.getColor(); 411 } 412 413 @Override 414 public boolean ok() { 415 for (ColorEntry d : tableModel.getDeleted()) { 416 d.toProperty().remove(); 417 } 418 for (ColorEntry e : tableModel.getData()) { 419 if (e.info.getValue() != null) { 420 e.toProperty().put(e.info.getValue()); 421 } 422 } 423 OsmDataLayer.createHatchTexture(); 424 return false; 425 } 426 427 @Override 428 public boolean isExpert() { 429 return false; 430 } 431 432 @Override 433 public void valueChanged(ListSelectionEvent e) { 434 updateEnabledState(); 435 } 436 437 @Override 438 public void tableChanged(TableModelEvent e) { 439 updateEnabledState(); 440 } 441 442 private void updateEnabledState() { 443 int sel = colors.getSelectedRow(); 444 if (sel < 0 || sel >= colors.getRowCount()) { 445 return; 446 } 447 ColorEntry ce = (ColorEntry) colors.getValueAt(sel, 0); 448 remove.setEnabled(ce != null && isRemoveColor(ce)); 449 colorEdit.setEnabled(ce != null); 450 defaultSet.setEnabled(ce != null && !ce.isDefault()); 451 } 452 453 @Override 454 public String getHelpContext() { 455 return HelpUtil.ht("/Preferences/ColorPreference"); 456 } 457}