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.Main;
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            fireTableDataChanged();
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 boolean isCellEditable(int rowIndex, int columnIndex) {
214            return false;
215        }
216
217        @Override
218        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
219            if (columnIndex == 1 && aValue instanceof Color) {
220                data.get(rowIndex).info.setValue((Color) aValue);
221                fireTableRowsUpdated(rowIndex, rowIndex);
222            }
223        }
224    }
225
226    /**
227     * Set the colors to be shown in the preference table. This method creates a table model if
228     * none exists and overwrites all existing values.
229     * @param colorMap the map holding the colors
230     * (key = preference key, value = {@link ColorInfo} instance)
231     */
232    public void setColors(Map<String, ColorInfo> colorMap) {
233        if (tableModel == null) {
234            tableModel = new ColorTableModel();
235        }
236        tableModel.clear();
237
238        // fill model with colors:
239        colorMap.entrySet().stream()
240                .map(e -> new ColorEntry(e.getKey(), e.getValue()))
241                .sorted((e1, e2) -> {
242                    int cat = Integer.compare(
243                            getCategroyPriority(e1.info.getCategory()),
244                            getCategroyPriority(e2.info.getCategory()));
245                    if (cat != 0) return cat;
246                    return Collator.getInstance().compare(e1.getDisplay(), e2.getDisplay());
247                })
248                .forEach(tableModel::addEntry);
249
250        if (this.colors != null) {
251            this.colors.repaint();
252        }
253
254    }
255
256    private static int getCategroyPriority(String category) {
257        switch (category) {
258            case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1;
259            case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2;
260            case NamedColorProperty.COLOR_CATEGORY_LAYER: return 3;
261            default: return 4;
262        }
263    }
264
265    /**
266     * Returns a map with the colors in the table (key = preference key, value = color info).
267     * @return a map holding the colors.
268     */
269    public Map<String, ColorInfo> getColors() {
270        return tableModel.getData().stream().collect(Collectors.toMap(e -> e.key, e -> e.info));
271    }
272
273    @Override
274    public void addGui(final PreferenceTabbedPane gui) {
275        fixColorPrefixes();
276        setColors(Main.pref.getAllNamedColors());
277
278        colorEdit = new JButton(tr("Choose"));
279        colorEdit.addActionListener(e -> {
280            int sel = colors.getSelectedRow();
281            ColorEntry ce = tableModel.getEntry(sel);
282            JColorChooser chooser = new JColorChooser(ce.getDisplayColor());
283            int answer = JOptionPane.showConfirmDialog(
284                    gui, chooser,
285                    tr("Choose a color for {0}", ce.getDisplay()),
286                    JOptionPane.OK_CANCEL_OPTION,
287                    JOptionPane.PLAIN_MESSAGE);
288            if (answer == JOptionPane.OK_OPTION) {
289                colors.setValueAt(chooser.getColor(), sel, 1);
290            }
291        });
292        defaultSet = new JButton(tr("Set to default"));
293        defaultSet.addActionListener(e -> {
294            int sel = colors.getSelectedRow();
295            ColorEntry ce = tableModel.getEntry(sel);
296            Color c = ce.info.getDefaultValue();
297            if (c != null) {
298                colors.setValueAt(c, sel, 1);
299            }
300        });
301        JButton defaultAll = new JButton(tr("Set all to default"));
302        defaultAll.addActionListener(e -> {
303            List<ColorEntry> data = tableModel.getData();
304            for (int i = 0; i < data.size(); ++i) {
305                ColorEntry ce = data.get(i);
306                Color c = ce.info.getDefaultValue();
307                if (c != null) {
308                    colors.setValueAt(c, i, 1);
309                }
310            }
311        });
312        remove = new JButton(tr("Remove"));
313        remove.addActionListener(e -> {
314            int sel = colors.getSelectedRow();
315            tableModel.removeEntry(sel);
316        });
317        remove.setEnabled(false);
318        colorEdit.setEnabled(false);
319        defaultSet.setEnabled(false);
320
321        colors = new JTable(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        JScrollPane scrollpane = new JScrollPane(colors);
376        scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
377        panel.add(scrollpane, GBC.eol().fill(GBC.BOTH));
378        JPanel buttonPanel = new JPanel(new GridBagLayout());
379        panel.add(buttonPanel, GBC.eol().insets(5, 0, 5, 5).fill(GBC.HORIZONTAL));
380        buttonPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.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        gui.getDisplayPreference().addSubTab(this, tr("Colors"), panel);
386    }
387
388    private boolean isRemoveColor(ColorEntry ce) {
389        return ce.info.getCategory().equals(NamedColorProperty.COLOR_CATEGORY_LAYER);
390    }
391
392    /**
393     * Add all missing color entries.
394     */
395    private static void fixColorPrefixes() {
396        PaintColors.values();
397        ConflictColors.getColors();
398        Severity.getColors();
399        MarkerLayer.getGenericColor();
400        GpxDrawHelper.getGenericColor();
401        OsmDataLayer.getOutsideColor();
402        MapScaler.getColor();
403        MapStatus.getColors();
404        ConflictDialog.getColor();
405    }
406
407    @Override
408    public boolean ok() {
409        boolean ret = false;
410        for (ColorEntry d : tableModel.getDeleted()) {
411            d.toProperty().remove();
412        }
413        for (ColorEntry e : tableModel.getData()) {
414            if (e.info.getValue() != null) {
415                if (e.toProperty().put(e.info.getValue())
416                        && e.key.startsWith("mappaint.")) {
417                    ret = true;
418                }
419            }
420        }
421        OsmDataLayer.createHatchTexture();
422        return ret;
423    }
424
425    @Override
426    public boolean isExpert() {
427        return false;
428    }
429
430    @Override
431    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
432        return gui.getDisplayPreference();
433    }
434
435    @Override
436    public void valueChanged(ListSelectionEvent e) {
437        updateEnabledState();
438    }
439
440    @Override
441    public void tableChanged(TableModelEvent e) {
442        updateEnabledState();
443    }
444
445    private void updateEnabledState() {
446        int sel = colors.getSelectedRow();
447        ColorEntry ce = sel >= 0 ? tableModel.getEntry(sel) : null;
448        remove.setEnabled(ce != null && isRemoveColor(ce));
449        colorEdit.setEnabled(ce != null);
450        defaultSet.setEnabled(ce != null && !ce.isDefault());
451    }
452}