001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.advanced;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Color;
008import java.awt.Component;
009import java.awt.Font;
010import java.awt.GridBagLayout;
011import java.awt.event.MouseAdapter;
012import java.awt.event.MouseEvent;
013import java.util.ArrayList;
014import java.util.List;
015import java.util.Map;
016import java.util.Objects;
017
018import javax.swing.ButtonGroup;
019import javax.swing.DefaultCellEditor;
020import javax.swing.JComponent;
021import javax.swing.JLabel;
022import javax.swing.JOptionPane;
023import javax.swing.JPanel;
024import javax.swing.JRadioButton;
025import javax.swing.JTable;
026import javax.swing.UIManager;
027import javax.swing.table.DefaultTableCellRenderer;
028import javax.swing.table.DefaultTableModel;
029
030import org.openstreetmap.josm.data.preferences.NamedColorProperty;
031import org.openstreetmap.josm.gui.ExtendedDialog;
032import org.openstreetmap.josm.gui.util.GuiHelper;
033import org.openstreetmap.josm.gui.widgets.JosmTextField;
034import org.openstreetmap.josm.spi.preferences.ListListSetting;
035import org.openstreetmap.josm.spi.preferences.ListSetting;
036import org.openstreetmap.josm.spi.preferences.MapListSetting;
037import org.openstreetmap.josm.spi.preferences.Setting;
038import org.openstreetmap.josm.spi.preferences.StringSetting;
039import org.openstreetmap.josm.tools.GBC;
040
041/**
042 * Component for editing list of preferences as a table.
043 * @since 6021
044 */
045public class PreferencesTable extends JTable {
046    private final AllSettingsTableModel model;
047    private final transient List<PrefEntry> displayData;
048
049    /**
050     * Constructs a new {@code PreferencesTable}.
051     * @param displayData The list of preferences entries to display
052     */
053    public PreferencesTable(List<PrefEntry> displayData) {
054        this.displayData = displayData;
055        model = new AllSettingsTableModel();
056        setModel(model);
057        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
058        getColumnModel().getColumn(1).setCellRenderer(new SettingCellRenderer());
059        getColumnModel().getColumn(1).setCellEditor(new SettingCellEditor());
060
061        addMouseListener(new MouseAdapter() {
062            @Override public void mouseClicked(MouseEvent e) {
063                if (e.getClickCount() == 2) {
064                    editPreference(PreferencesTable.this);
065                }
066            }
067        });
068    }
069
070    /**
071     * This method should be called when displayed data was changed form external code
072     */
073    public void fireDataChanged() {
074        model.fireTableDataChanged();
075    }
076
077    /**
078     * The list of currently selected rows
079     * @return newly created list of PrefEntry
080     */
081    public List<PrefEntry> getSelectedItems() {
082        List<PrefEntry> entries = new ArrayList<>();
083        for (int row : getSelectedRows()) {
084            PrefEntry p = (PrefEntry) model.getValueAt(row, -1);
085            entries.add(p);
086        }
087        return entries;
088    }
089
090    /**
091     * Call this to edit selected row in preferences table
092     * @param gui - parent component for messagebox
093     * @return true if editing was actually performed during this call
094     */
095    public boolean editPreference(final JComponent gui) {
096        if (getSelectedRowCount() != 1) {
097            JOptionPane.showMessageDialog(
098                    gui,
099                    tr("Please select the row to edit."),
100                    tr("Warning"),
101                    JOptionPane.WARNING_MESSAGE
102                    );
103            return false;
104        }
105        final PrefEntry e = (PrefEntry) model.getValueAt(getSelectedRow(), 1);
106        Setting<?> stg = e.getValue();
107        boolean ok = false;
108        if (stg instanceof StringSetting) {
109            editCellAt(getSelectedRow(), 1);
110            Component editor = getEditorComponent();
111            if (editor != null) {
112                editor.requestFocus();
113            }
114        } else if (stg instanceof ListSetting) {
115            ok = doEditList(gui, e, (ListSetting) stg);
116        } else if (stg instanceof ListListSetting) {
117            ok = doEditListList(gui, e, (ListListSetting) stg);
118        } else if (stg instanceof MapListSetting) {
119            ok = doEditMapList(gui, e, (MapListSetting) stg);
120        }
121        return ok;
122    }
123
124    private static boolean doEditList(final JComponent gui, final PrefEntry e, ListSetting lSetting) {
125        ListEditor lEditor = new ListEditor(gui, e, lSetting);
126        lEditor.showDialog();
127        if (lEditor.getValue() == 1) {
128            List<String> data = lEditor.getData();
129            if (!lSetting.equalVal(data)) {
130                e.setValue(new ListSetting(data));
131                return true;
132            }
133        }
134        return false;
135    }
136
137    private static boolean doEditListList(final JComponent gui, final PrefEntry e, ListListSetting llSetting) {
138        ListListEditor llEditor = new ListListEditor(gui, e, llSetting);
139        llEditor.showDialog();
140        if (llEditor.getValue() == 1) {
141            List<List<String>> data = llEditor.getData();
142            if (!llSetting.equalVal(data)) {
143                e.setValue(new ListListSetting(data));
144                return true;
145            }
146        }
147        return false;
148    }
149
150    private static boolean doEditMapList(final JComponent gui, final PrefEntry e, MapListSetting mlSetting) {
151        MapListEditor mlEditor = new MapListEditor(gui, e, mlSetting);
152        mlEditor.showDialog();
153        if (mlEditor.getValue() == 1) {
154            List<Map<String, String>> data = mlEditor.getData();
155            if (!mlSetting.equalVal(data)) {
156                e.setValue(new MapListSetting(data));
157                return true;
158            }
159        }
160        return false;
161    }
162
163    /**
164     * Add new preference to the table
165     * @param gui - parent component for asking dialogs
166     * @return newly created entry or null if adding was cancelled
167     */
168    public PrefEntry addPreference(final JComponent gui) {
169        JPanel p = new JPanel(new GridBagLayout());
170        p.add(new JLabel(tr("Key")), GBC.std().insets(0, 0, 5, 0));
171        JosmTextField tkey = new JosmTextField("", 50);
172        p.add(tkey, GBC.eop().insets(5, 0, 0, 0).fill(GBC.HORIZONTAL));
173
174        p.add(new JLabel(tr("Select Setting Type:")), GBC.eol().insets(5, 15, 5, 0));
175
176        JRadioButton rbString = new JRadioButton(tr("Simple"));
177        JRadioButton rbList = new JRadioButton(tr("List"));
178        JRadioButton rbListList = new JRadioButton(tr("List of lists"));
179        JRadioButton rbMapList = new JRadioButton(tr("List of maps"));
180
181        ButtonGroup group = new ButtonGroup();
182        group.add(rbString);
183        group.add(rbList);
184        group.add(rbListList);
185        group.add(rbMapList);
186
187        p.add(rbString, GBC.eol());
188        p.add(rbList, GBC.eol());
189        p.add(rbListList, GBC.eol());
190        p.add(rbMapList, GBC.eol());
191
192        rbString.setSelected(true);
193
194        PrefEntry pe = null;
195        boolean ok = false;
196        if (askAddSetting(gui, p)) {
197            if (rbString.isSelected()) {
198                StringSetting sSetting = new StringSetting(null);
199                pe = new PrefEntry(tkey.getText(), sSetting, sSetting, false);
200                ok = doAddSimple(gui, pe, sSetting);
201            } else if (rbList.isSelected()) {
202                ListSetting lSetting = new ListSetting(null);
203                pe = new PrefEntry(tkey.getText(), lSetting, lSetting, false);
204                ok = doAddList(gui, pe, lSetting);
205            } else if (rbListList.isSelected()) {
206                ListListSetting llSetting = new ListListSetting(null);
207                pe = new PrefEntry(tkey.getText(), llSetting, llSetting, false);
208                ok = doAddListList(gui, pe, llSetting);
209            } else if (rbMapList.isSelected()) {
210                MapListSetting mlSetting = new MapListSetting(null);
211                pe = new PrefEntry(tkey.getText(), mlSetting, mlSetting, false);
212                ok = doAddMapList(gui, pe, mlSetting);
213            }
214        }
215        return ok ? pe : null;
216    }
217
218    private static boolean askAddSetting(JComponent gui, JPanel p) {
219        return new ExtendedDialog(gui, tr("Add setting"), tr("OK"), tr("Cancel"))
220                .setContent(p).setButtonIcons("ok", "cancel").showDialog().getValue() == 1;
221    }
222
223    private static boolean doAddSimple(final JComponent gui, PrefEntry pe, StringSetting sSetting) {
224        StringEditor sEditor = new StringEditor(gui, pe, sSetting);
225        sEditor.showDialog();
226        if (sEditor.getValue() == 1) {
227            String data = sEditor.getData();
228            if (!Objects.equals(sSetting.getValue(), data)) {
229                pe.setValue(new StringSetting(data));
230                return true;
231            }
232        }
233        return false;
234    }
235
236    private static boolean doAddList(final JComponent gui, PrefEntry pe, ListSetting lSetting) {
237        ListEditor lEditor = new ListEditor(gui, pe, lSetting);
238        lEditor.showDialog();
239        if (lEditor.getValue() == 1) {
240            List<String> data = lEditor.getData();
241            if (!lSetting.equalVal(data)) {
242                pe.setValue(new ListSetting(data));
243                return true;
244            }
245        }
246        return false;
247    }
248
249    private static boolean doAddListList(final JComponent gui, PrefEntry pe, ListListSetting llSetting) {
250        ListListEditor llEditor = new ListListEditor(gui, pe, llSetting);
251        llEditor.showDialog();
252        if (llEditor.getValue() == 1) {
253            List<List<String>> data = llEditor.getData();
254            if (!llSetting.equalVal(data)) {
255                pe.setValue(new ListListSetting(data));
256                return true;
257            }
258        }
259        return false;
260    }
261
262    private static boolean doAddMapList(final JComponent gui, PrefEntry pe, MapListSetting mlSetting) {
263        MapListEditor mlEditor = new MapListEditor(gui, pe, mlSetting);
264        mlEditor.showDialog();
265        if (mlEditor.getValue() == 1) {
266            List<Map<String, String>> data = mlEditor.getData();
267            if (!mlSetting.equalVal(data)) {
268                pe.setValue(new MapListSetting(data));
269                return true;
270            }
271        }
272        return false;
273    }
274
275    /**
276     * Reset selected preferences to their default values
277     * @param gui - parent component to display warning messages
278     */
279    public void resetPreferences(final JComponent gui) {
280        if (getSelectedRowCount() == 0) {
281            JOptionPane.showMessageDialog(
282                    gui,
283                    tr("Please select the row to delete."),
284                    tr("Warning"),
285                    JOptionPane.WARNING_MESSAGE
286                    );
287            return;
288        }
289        for (int row : getSelectedRows()) {
290            PrefEntry e = displayData.get(row);
291            e.reset();
292        }
293        fireDataChanged();
294    }
295
296    final class AllSettingsTableModel extends DefaultTableModel {
297
298        AllSettingsTableModel() {
299            setColumnIdentifiers(new String[]{tr("Key"), tr("Value")});
300        }
301
302        @Override
303        public boolean isCellEditable(int row, int column) {
304            return column == 1 && (displayData.get(row).getValue() instanceof StringSetting);
305        }
306
307        @Override
308        public int getRowCount() {
309            return displayData.size();
310        }
311
312        @Override
313        public Object getValueAt(int row, int column) {
314            if (column == 0)
315                return displayData.get(row).getKey();
316            else
317                return displayData.get(row);
318        }
319
320        @Override
321        public void setValueAt(Object o, int row, int column) {
322            PrefEntry pe = displayData.get(row);
323            String s = (String) o;
324            if (!s.equals(pe.getValue().getValue())) {
325                pe.setValue(new StringSetting(s));
326                fireTableCellUpdated(row, column);
327            }
328        }
329    }
330
331    static final class SettingCellRenderer extends DefaultTableCellRenderer {
332        private final Color backgroundColor = UIManager.getColor("Table.background");
333        private final Color changedColor = new NamedColorProperty(
334                         marktr("Advanced Background: Changed"),
335                         new Color(200, 255, 200)).get();
336        private final Color nonDefaultColor = new NamedColorProperty(
337                            marktr("Advanced Background: NonDefault"),
338                            new Color(255, 255, 200)).get();
339
340        @Override
341        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
342            if (value == null)
343                return this;
344            PrefEntry pe = (PrefEntry) value;
345            Setting<?> setting = pe.getValue();
346            Object val = setting.getValue();
347            String display = val != null ? val.toString() : "<html><i>&lt;"+tr("unset")+"&gt;</i></html>";
348
349            JLabel label = (JLabel) super.getTableCellRendererComponent(table,
350                    display, isSelected, hasFocus, row, column);
351
352            GuiHelper.setBackgroundReadable(label, backgroundColor);
353            if (pe.isChanged()) {
354                GuiHelper.setBackgroundReadable(label, changedColor);
355            } else if (!pe.isDefault()) {
356                GuiHelper.setBackgroundReadable(label, nonDefaultColor);
357            }
358
359            if (!pe.isDefault()) {
360                label.setFont(label.getFont().deriveFont(Font.BOLD));
361            }
362            val = pe.getDefaultValue().getValue();
363            if (val != null) {
364                if (pe.isDefault()) {
365                    label.setToolTipText(tr("Current value is default."));
366                } else {
367                    label.setToolTipText(tr("Default value is ''{0}''.", val));
368                }
369            } else {
370                label.setToolTipText(tr("Default value currently unknown (setting has not been used yet)."));
371            }
372            return label;
373        }
374    }
375
376    static final class SettingCellEditor extends DefaultCellEditor {
377        SettingCellEditor() {
378            super(new JosmTextField());
379        }
380
381        @Override
382        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
383            PrefEntry pe = (PrefEntry) value;
384            StringSetting stg = (StringSetting) pe.getValue();
385            String s = stg.getValue() == null ? "" : stg.getValue();
386            return super.getTableCellEditorComponent(table, s, isSelected, row, column);
387        }
388    }
389}