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_MAPPAINT: 104 if (info.getSource() != null) 105 return tr("Paint style {0}: {1}", tr(I18n.escape(info.getSource())), tr(info.getName())); 106 // fall through 107 default: 108 if (info.getSource() != null) 109 return tr(I18n.escape(info.getSource())) + " - " + tr(I18n.escape(info.getName())); 110 else 111 return tr(I18n.escape(info.getName())); 112 } 113 } 114 115 /** 116 * Get the color value to display. 117 * Either value (if set) or default value. 118 * @return the color value to display 119 */ 120 public Color getDisplayColor() { 121 return Optional.ofNullable(info.getValue()).orElse(info.getDefaultValue()); 122 } 123 124 /** 125 * Check if color has been customized by the user or not. 126 * @return true if the color is at its default value, false if it is customized by the user. 127 */ 128 public boolean isDefault() { 129 return info.getValue() == null || Objects.equals(info.getValue(), info.getDefaultValue()); 130 } 131 132 /** 133 * Convert to a {@link NamedColorProperty}. 134 * @return a {@link NamedColorProperty} 135 */ 136 public NamedColorProperty toProperty() { 137 return new NamedColorProperty(info.getCategory(), info.getSource(), 138 info.getName(), info.getDefaultValue()); 139 } 140 } 141 142 private static class ColorTableModel extends AbstractTableModel { 143 144 private final List<ColorEntry> data; 145 private final List<ColorEntry> deleted; 146 147 ColorTableModel() { 148 this.data = new ArrayList<>(); 149 this.deleted = new ArrayList<>(); 150 } 151 152 public void addEntry(ColorEntry entry) { 153 data.add(entry); 154 } 155 156 public void removeEntry(int row) { 157 deleted.add(data.get(row)); 158 data.remove(row); 159 fireTableRowsDeleted(row, row); 160 } 161 162 public ColorEntry getEntry(int row) { 163 return data.get(row); 164 } 165 166 public List<ColorEntry> getData() { 167 return data; 168 } 169 170 public List<ColorEntry> getDeleted() { 171 return deleted; 172 } 173 174 public void clear() { 175 data.clear(); 176 deleted.clear(); 177 } 178 179 @Override 180 public int getRowCount() { 181 return data.size(); 182 } 183 184 @Override 185 public int getColumnCount() { 186 return 2; 187 } 188 189 @Override 190 public Object getValueAt(int rowIndex, int columnIndex) { 191 return columnIndex == 0 ? data.get(rowIndex) : data.get(rowIndex).getDisplayColor(); 192 } 193 194 @Override 195 public String getColumnName(int column) { 196 return column == 0 ? tr("Name") : tr("Color"); 197 } 198 199 @Override 200 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { 201 if (columnIndex == 1 && aValue instanceof Color) { 202 data.get(rowIndex).info.setValue((Color) aValue); 203 fireTableRowsUpdated(rowIndex, rowIndex); 204 } 205 } 206 } 207 208 /** 209 * Set the colors to be shown in the preference table. This method creates a table model if 210 * none exists and overwrites all existing values. 211 * @param colorMap the map holding the colors 212 * (key = preference key, value = {@link ColorInfo} instance) 213 */ 214 public void setColors(Map<String, ColorInfo> colorMap) { 215 if (tableModel == null) { 216 tableModel = new ColorTableModel(); 217 } 218 tableModel.clear(); 219 220 // fill model with colors: 221 colorMap.entrySet().stream() 222 .map(e -> new ColorEntry(e.getKey(), e.getValue())) 223 .sorted((e1, e2) -> { 224 int cat = Integer.compare( 225 getCategoryPriority(e1.info.getCategory()), 226 getCategoryPriority(e2.info.getCategory())); 227 if (cat != 0) return cat; 228 return Collator.getInstance().compare(e1.getDisplay(), e2.getDisplay()); 229 }) 230 .forEach(tableModel::addEntry); 231 232 if (this.colors != null) { 233 this.colors.repaint(); 234 } 235 } 236 237 private static int getCategoryPriority(String category) { 238 switch (category) { 239 case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1; 240 case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2; 241 default: return 3; 242 } 243 } 244 245 /** 246 * Returns a map with the colors in the table (key = preference key, value = color info). 247 * @return a map holding the colors. 248 */ 249 public Map<String, ColorInfo> getColors() { 250 return tableModel.getData().stream().collect(Collectors.toMap(e -> e.key, e -> e.info)); 251 } 252 253 @Override 254 public void addGui(final PreferenceTabbedPane gui) { 255 fixColorPrefixes(); 256 setColors(Preferences.main().getAllNamedColors()); 257 258 colorEdit = new JButton(tr("Choose")); 259 colorEdit.addActionListener(e -> { 260 int sel = colors.getSelectedRow(); 261 ColorEntry ce = tableModel.getEntry(sel); 262 JColorChooser chooser = new JColorChooser(ce.getDisplayColor()); 263 int answer = JOptionPane.showConfirmDialog( 264 gui, chooser, 265 tr("Choose a color for {0}", ce.getDisplay()), 266 JOptionPane.OK_CANCEL_OPTION, 267 JOptionPane.PLAIN_MESSAGE); 268 if (answer == JOptionPane.OK_OPTION) { 269 colors.setValueAt(chooser.getColor(), sel, 1); 270 } 271 }); 272 defaultSet = new JButton(tr("Reset to default")); 273 defaultSet.addActionListener(e -> { 274 int sel = colors.getSelectedRow(); 275 ColorEntry ce = tableModel.getEntry(sel); 276 Color c = ce.info.getDefaultValue(); 277 if (c != null) { 278 colors.setValueAt(c, sel, 1); 279 } 280 }); 281 JButton defaultAll = new JButton(tr("Set all to default")); 282 defaultAll.addActionListener(e -> { 283 List<ColorEntry> data = tableModel.getData(); 284 for (int i = 0; i < data.size(); ++i) { 285 ColorEntry ce = data.get(i); 286 Color c = ce.info.getDefaultValue(); 287 if (c != null) { 288 colors.setValueAt(c, i, 1); 289 } 290 } 291 }); 292 remove = new JButton(tr("Remove")); 293 remove.addActionListener(e -> { 294 int sel = colors.getSelectedRow(); 295 tableModel.removeEntry(sel); 296 }); 297 remove.setEnabled(false); 298 colorEdit.setEnabled(false); 299 defaultSet.setEnabled(false); 300 301 colors = new JTable(tableModel); 302 colors.addMouseListener(new MouseAdapter() { 303 @Override 304 public void mousePressed(MouseEvent me) { 305 if (me.getClickCount() == 2) { 306 colorEdit.doClick(); 307 } 308 } 309 }); 310 colors.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 311 colors.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() { 312 @Override 313 public Component getTableCellRendererComponent( 314 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 315 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 316 if (value != null && comp instanceof JLabel) { 317 JLabel label = (JLabel) comp; 318 ColorEntry e = (ColorEntry) value; 319 label.setText(e.getDisplay()); 320 if (!e.isDefault()) { 321 label.setFont(label.getFont().deriveFont(Font.BOLD)); 322 } else { 323 label.setFont(label.getFont().deriveFont(Font.PLAIN)); 324 } 325 return label; 326 } 327 return comp; 328 } 329 }); 330 colors.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer() { 331 @Override 332 public Component getTableCellRendererComponent( 333 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 334 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 335 if (value != null && comp instanceof JLabel) { 336 JLabel label = (JLabel) comp; 337 Color c = (Color) value; 338 label.setText(ColorHelper.color2html(c)); 339 GuiHelper.setBackgroundReadable(label, c); 340 label.setOpaque(true); 341 return label; 342 } 343 return comp; 344 } 345 }); 346 colors.getColumnModel().getColumn(1).setWidth(100); 347 colors.setToolTipText(tr("Colors used by different objects in JOSM.")); 348 colors.setPreferredScrollableViewportSize(new Dimension(100, 112)); 349 350 colors.getSelectionModel().addListSelectionListener(this); 351 colors.getModel().addTableModelListener(this); 352 353 JPanel panel = new JPanel(new GridBagLayout()); 354 panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 355 JScrollPane scrollpane = new JScrollPane(colors); 356 scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 357 panel.add(scrollpane, GBC.eol().fill(GBC.BOTH)); 358 JPanel buttonPanel = new JPanel(new GridBagLayout()); 359 panel.add(buttonPanel, GBC.eol().insets(5, 0, 5, 5).fill(GBC.HORIZONTAL)); 360 buttonPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); 361 buttonPanel.add(colorEdit, GBC.std().insets(0, 5, 0, 0)); 362 buttonPanel.add(defaultSet, GBC.std().insets(5, 5, 5, 0)); 363 buttonPanel.add(defaultAll, GBC.std().insets(0, 5, 0, 0)); 364 buttonPanel.add(remove, GBC.std().insets(0, 5, 0, 0)); 365 gui.getDisplayPreference().addSubTab(this, tr("Colors"), panel); 366 } 367 368 @SuppressWarnings("PMD.UnusedFormalParameter") 369 private static boolean isRemoveColor(ColorEntry ce) { 370 return false; 371 //COLOR_CATEGORY_LAYER is no longer supported and was the only one that could be removed. 372 //Maybe this is useful for other categories in the future. 373 //return NamedColorProperty.COLOR_CATEGORY_LAYER.equals(ce.info.getCategory()); 374 } 375 376 /** 377 * Add all missing color entries. 378 */ 379 private static void fixColorPrefixes() { 380 PaintColors.values(); 381 ConflictColors.getColors(); 382 Severity.getColors(); 383 MarkerLayer.DEFAULT_COLOR_PROPERTY.get(); 384 GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get(); 385 OsmDataLayer.getOutsideColor(); 386 MapScaler.getColor(); 387 MapStatus.getColors(); 388 ConflictDialog.getColor(); 389 } 390 391 @Override 392 public boolean ok() { 393 boolean ret = false; 394 for (ColorEntry d : tableModel.getDeleted()) { 395 d.toProperty().remove(); 396 } 397 for (ColorEntry e : tableModel.getData()) { 398 if (e.info.getValue() != null && e.toProperty().put(e.info.getValue()) 399 && NamedColorProperty.COLOR_CATEGORY_MAPPAINT.equals(e.info.getCategory())) { 400 ret = true; 401 } 402 } 403 OsmDataLayer.createHatchTexture(); 404 return ret; 405 } 406 407 @Override 408 public boolean isExpert() { 409 return false; 410 } 411 412 @Override 413 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 414 return gui.getDisplayPreference(); 415 } 416 417 @Override 418 public void valueChanged(ListSelectionEvent e) { 419 updateEnabledState(); 420 } 421 422 @Override 423 public void tableChanged(TableModelEvent e) { 424 updateEnabledState(); 425 } 426 427 private void updateEnabledState() { 428 int sel = colors.getSelectedRow(); 429 ColorEntry ce = sel >= 0 && sel < tableModel.getRowCount() ? tableModel.getEntry(sel) : null; 430 remove.setEnabled(ce != null && isRemoveColor(ce)); 431 colorEdit.setEnabled(ce != null); 432 defaultSet.setEnabled(ce != null && !ce.isDefault()); 433 } 434}