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}