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}