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