001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.shortcut;
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.Dimension;
010import java.awt.GridBagLayout;
011import java.awt.GridLayout;
012import java.awt.Toolkit;
013import java.awt.event.KeyEvent;
014import java.awt.im.InputContext;
015import java.lang.reflect.Field;
016import java.util.LinkedHashMap;
017import java.util.List;
018import java.util.Map;
019
020import javax.swing.AbstractAction;
021import javax.swing.JCheckBox;
022import javax.swing.JLabel;
023import javax.swing.JPanel;
024import javax.swing.JScrollPane;
025import javax.swing.JTable;
026import javax.swing.KeyStroke;
027import javax.swing.ListSelectionModel;
028import javax.swing.SwingConstants;
029import javax.swing.UIManager;
030import javax.swing.event.ListSelectionEvent;
031import javax.swing.event.ListSelectionListener;
032import javax.swing.table.AbstractTableModel;
033import javax.swing.table.DefaultTableCellRenderer;
034import javax.swing.table.TableColumnModel;
035
036import org.openstreetmap.josm.data.preferences.NamedColorProperty;
037import org.openstreetmap.josm.gui.util.GuiHelper;
038import org.openstreetmap.josm.gui.util.TableHelper;
039import org.openstreetmap.josm.gui.widgets.FilterField;
040import org.openstreetmap.josm.gui.widgets.JosmComboBox;
041import org.openstreetmap.josm.tools.GBC;
042import org.openstreetmap.josm.tools.KeyboardUtils;
043import org.openstreetmap.josm.tools.Logging;
044import org.openstreetmap.josm.tools.Shortcut;
045
046/**
047 * This is the keyboard preferences content.
048 */
049public class PrefJPanel extends JPanel {
050
051    // table of shortcuts
052    private final AbstractTableModel model;
053    // this are the display(!) texts for the checkboxes. Let the JVM do the i18n for us <g>.
054    // Ok, there's a real reason for this: The JVM should know best how the keys are labelled
055    // on the physical keyboard. What language pack is installed in JOSM is completely
056    // independent from the keyboard's labelling. But the operation system's locale
057    // usually matches the keyboard. This even works with my English Windows and my German keyboard.
058    private static final String SHIFT = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
059            KeyEvent.SHIFT_DOWN_MASK).getModifiers());
060    private static final String CTRL = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
061            KeyEvent.CTRL_DOWN_MASK).getModifiers());
062    private static final String ALT = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
063            KeyEvent.ALT_DOWN_MASK).getModifiers());
064    private static final String META = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
065            KeyEvent.META_DOWN_MASK).getModifiers());
066
067    // A list of keys to present the user. Sadly this really is a list of keys Java knows about,
068    // not a list of real physical keys. If someone knows how to get that list?
069    private static final Map<Integer, String> keyList = setKeyList();
070
071    private final JCheckBox cbAlt = new JCheckBox();
072    private final JCheckBox cbCtrl = new JCheckBox();
073    private final JCheckBox cbMeta = new JCheckBox();
074    private final JCheckBox cbShift = new JCheckBox();
075    private final JCheckBox cbDefault = new JCheckBox();
076    private final JCheckBox cbDisable = new JCheckBox();
077    private final JosmComboBox<String> tfKey = new JosmComboBox<>();
078
079    private final JTable shortcutTable = new JTable();
080    private final FilterField filterField;
081
082    /** Creates new form prefJPanel */
083    public PrefJPanel() {
084        this.model = new ScListModel();
085        this.filterField = new FilterField();
086        initComponents();
087    }
088
089    private static Map<Integer, String> setKeyList() {
090        Map<Integer, String> list = new LinkedHashMap<>();
091        String unknown = Toolkit.getProperty("AWT.unknown", "Unknown");
092        // Assume all known keys are declared in KeyEvent as "public static int VK_*"
093        for (Field field : KeyEvent.class.getFields()) {
094            // Ignore VK_KP_DOWN, UP, etc. because they have the same name as VK_DOWN, UP, etc. See #8340
095            if (field.getName().startsWith("VK_") && !field.getName().startsWith("VK_KP_")) {
096                try {
097                    int i = field.getInt(null);
098                    String s = KeyEvent.getKeyText(i);
099                    if (s != null && s.length() > 0 && !s.contains(unknown)) {
100                        list.put(Integer.valueOf(i), s);
101                    }
102                } catch (IllegalArgumentException | IllegalAccessException e) {
103                    Logging.error(e);
104                }
105            }
106        }
107        KeyboardUtils.getExtendedKeyCodes(InputContext.getInstance().getLocale())
108                .forEach((key, value) -> list.put(key, value.toString()));
109        list.put(Integer.valueOf(-1), "");
110        return list;
111    }
112
113    /**
114     * Show only shortcuts with descriptions containing given substring
115     * @param substring The substring used to filter
116     */
117    public void filter(String substring) {
118        filterField.setText(substring);
119    }
120
121    private static class ScListModel extends AbstractTableModel {
122        private final String[] columnNames = {tr("Action"), tr("Shortcut")};
123        private final transient List<Shortcut> data;
124
125        /**
126         * Constructs a new {@code ScListModel}.
127         */
128        ScListModel() {
129            data = Shortcut.listAll();
130        }
131
132        @Override
133        public int getColumnCount() {
134            return columnNames.length;
135        }
136
137        @Override
138        public int getRowCount() {
139            return data.size();
140        }
141
142        @Override
143        public String getColumnName(int col) {
144            return columnNames[col];
145        }
146
147        @Override
148        public Object getValueAt(int row, int col) {
149            return (col == 0) ? data.get(row).getLongText() : data.get(row);
150        }
151    }
152
153    private class ShortcutTableCellRenderer extends DefaultTableCellRenderer {
154
155        private final transient NamedColorProperty SHORTCUT_BACKGROUND_USER_COLOR = new NamedColorProperty(
156                marktr("Shortcut Background: User"),
157                new Color(200, 255, 200));
158        private final transient NamedColorProperty SHORTCUT_BACKGROUND_MODIFIED_COLOR = new NamedColorProperty(
159                marktr("Shortcut Background: Modified"),
160                new Color(255, 255, 200));
161
162        private final boolean name;
163
164        ShortcutTableCellRenderer(boolean name) {
165            this.name = name;
166        }
167
168        @Override
169        public Component getTableCellRendererComponent(JTable table, Object value, boolean
170                isSelected, boolean hasFocus, int row, int column) {
171            int row1 = shortcutTable.convertRowIndexToModel(row);
172            Shortcut sc = (Shortcut) model.getValueAt(row1, -1);
173            if (sc == null)
174                return null;
175            JLabel label = (JLabel) super.getTableCellRendererComponent(
176                table, name ? sc.getLongText() : sc.getKeyText(), isSelected, hasFocus, row, column);
177            GuiHelper.setBackgroundReadable(label, UIManager.getColor("Table.background"));
178            if (sc.isAssignedUser()) {
179                GuiHelper.setBackgroundReadable(label, SHORTCUT_BACKGROUND_USER_COLOR.get());
180            } else if (!sc.isAssignedDefault()) {
181                GuiHelper.setBackgroundReadable(label, SHORTCUT_BACKGROUND_MODIFIED_COLOR.get());
182            }
183            return label;
184        }
185    }
186
187    private void initComponents() {
188        CbAction action = new CbAction(this);
189        GBC gbc = GBC.eol().insets(3).fill(GBC.HORIZONTAL);
190
191        setLayout(new GridBagLayout());
192        add(buildFilterPanel(), gbc);
193
194        // This is the list of shortcuts:
195        TableHelper.setFont(shortcutTable, getClass());
196        shortcutTable.setModel(model);
197        shortcutTable.getSelectionModel().addListSelectionListener(action);
198        shortcutTable.setFillsViewportHeight(true);
199        shortcutTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
200        shortcutTable.setAutoCreateRowSorter(true);
201        filterField.filter(shortcutTable, model);
202        TableColumnModel mod = shortcutTable.getColumnModel();
203        mod.getColumn(0).setCellRenderer(new ShortcutTableCellRenderer(true));
204        mod.getColumn(1).setCellRenderer(new ShortcutTableCellRenderer(false));
205        JScrollPane listScrollPane = new JScrollPane();
206        listScrollPane.setViewportView(shortcutTable);
207
208        gbc.weighty = 1;
209        add(listScrollPane, gbc.fill(GBC.BOTH));
210
211        // and here follows the edit area. I won't object to someone re-designing it, it looks, um, "minimalistic" ;)
212
213        cbDefault.setAction(action);
214        cbDefault.setText(tr("Use default"));
215        cbShift.setAction(action);
216        cbShift.setText(SHIFT); // see above for why no tr()
217        cbDisable.setAction(action);
218        cbDisable.setText(tr("Disable"));
219        cbCtrl.setAction(action);
220        cbCtrl.setText(CTRL); // see above for why no tr()
221        cbAlt.setAction(action);
222        cbAlt.setText(ALT); // see above for why no tr()
223        tfKey.setAction(action);
224        tfKey.getModel().addAllElements(keyList.values());
225        cbMeta.setAction(action);
226        cbMeta.setText(META); // see above for why no tr()
227
228        JPanel shortcutEditPane = new JPanel(new GridLayout(5, 2));
229
230        shortcutEditPane.add(cbDefault);
231        shortcutEditPane.add(new JLabel());
232        shortcutEditPane.add(cbShift);
233        shortcutEditPane.add(cbDisable);
234        shortcutEditPane.add(cbCtrl);
235        shortcutEditPane.add(new JLabel(tr("Key:"), SwingConstants.LEADING));
236        shortcutEditPane.add(cbAlt);
237        shortcutEditPane.add(tfKey);
238        shortcutEditPane.add(cbMeta);
239
240        shortcutEditPane.add(new JLabel(tr("Attention: Use real keyboard keys only!")));
241
242        action.actionPerformed(null); // init checkboxes
243
244        gbc.weighty = 0;
245        add(shortcutEditPane, gbc);
246    }
247
248    private JPanel buildFilterPanel() {
249        // copied from PluginPreference
250        JPanel pnl = new JPanel(new GridBagLayout());
251        pnl.add(filterField, GBC.eol().insets(0, 0, 0, 5).fill(GBC.HORIZONTAL));
252        pnl.setMaximumSize(new Dimension(Integer.MAX_VALUE, 10));
253        return pnl;
254    }
255
256    // this allows to edit shortcuts. it:
257    //  * sets the edit controls to the selected shortcut
258    //  * enabled/disables the controls as needed
259    //  * writes the user's changes to the shortcut
260    // And after I finally had it working, I realized that those two methods
261    // are playing ping-pong (politically correct: table tennis, I know) and
262    // even have some duplicated code. Feel free to refactor, If you have
263    // more experience with GUI coding than I have.
264    private static class CbAction extends AbstractAction implements ListSelectionListener {
265        private final PrefJPanel panel;
266
267        CbAction(PrefJPanel panel) {
268            this.panel = panel;
269        }
270
271        private void disableAllModifierCheckboxes() {
272            panel.cbDefault.setEnabled(false);
273            panel.cbDisable.setEnabled(false);
274            panel.cbShift.setEnabled(false);
275            panel.cbCtrl.setEnabled(false);
276            panel.cbAlt.setEnabled(false);
277            panel.cbMeta.setEnabled(false);
278        }
279
280        @Override
281        public void valueChanged(ListSelectionEvent e) {
282            ListSelectionModel lsm = panel.shortcutTable.getSelectionModel(); // can't use e here
283            if (!lsm.isSelectionEmpty()) {
284                int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
285                Shortcut sc = (Shortcut) panel.model.getValueAt(row, -1);
286                panel.cbDefault.setSelected(!sc.isAssignedUser());
287                panel.cbDisable.setSelected(sc.getKeyStroke() == null);
288                panel.cbShift.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.SHIFT_DOWN_MASK) != 0);
289                panel.cbCtrl.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.CTRL_DOWN_MASK) != 0);
290                panel.cbAlt.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.ALT_DOWN_MASK) != 0);
291                panel.cbMeta.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.META_DOWN_MASK) != 0);
292                if (sc.getKeyStroke() != null) {
293                    panel.tfKey.setSelectedItem(keyList.get(sc.getKeyStroke().getKeyCode()));
294                } else {
295                    panel.tfKey.setSelectedItem(keyList.get(-1));
296                }
297                if (!sc.isChangeable()) {
298                    disableAllModifierCheckboxes();
299                    panel.tfKey.setEnabled(false);
300                } else {
301                    panel.cbDefault.setEnabled(true);
302                    actionPerformed(null);
303                }
304                panel.model.fireTableRowsUpdated(row, row);
305            } else {
306                disableAllModifierCheckboxes();
307                panel.tfKey.setEnabled(false);
308            }
309        }
310
311        @Override
312        public void actionPerformed(java.awt.event.ActionEvent e) {
313            ListSelectionModel lsm = panel.shortcutTable.getSelectionModel();
314            if (lsm != null && !lsm.isSelectionEmpty()) {
315                if (e != null) { // only if we've been called by a user action
316                    int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
317                    Shortcut sc = (Shortcut) panel.model.getValueAt(row, -1);
318                    Object selectedKey = panel.tfKey.getSelectedItem();
319                    if (panel.cbDisable.isSelected()) {
320                        sc.setAssignedModifier(-1);
321                    } else if (selectedKey == null || "".equals(selectedKey)) {
322                        sc.setAssignedModifier(KeyEvent.VK_CANCEL);
323                    } else {
324                        sc.setAssignedModifier(
325                                (panel.cbShift.isSelected() ? KeyEvent.SHIFT_DOWN_MASK : 0) |
326                                (panel.cbCtrl.isSelected() ? KeyEvent.CTRL_DOWN_MASK : 0) |
327                                (panel.cbAlt.isSelected() ? KeyEvent.ALT_DOWN_MASK : 0) |
328                                (panel.cbMeta.isSelected() ? KeyEvent.META_DOWN_MASK : 0)
329                        );
330                        for (Map.Entry<Integer, String> entry : keyList.entrySet()) {
331                            if (entry.getValue().equals(selectedKey)) {
332                                sc.setAssignedKey(entry.getKey());
333                            }
334                        }
335                    }
336                    sc.setAssignedUser(!panel.cbDefault.isSelected());
337                    valueChanged(null);
338                }
339                boolean state = !panel.cbDefault.isSelected();
340                panel.cbDisable.setEnabled(state);
341                state = state && !panel.cbDisable.isSelected();
342                panel.cbShift.setEnabled(state);
343                panel.cbCtrl.setEnabled(state);
344                panel.cbAlt.setEnabled(state);
345                panel.cbMeta.setEnabled(state);
346                panel.tfKey.setEnabled(state);
347            } else {
348                disableAllModifierCheckboxes();
349                panel.tfKey.setEnabled(false);
350            }
351        }
352    }
353}