source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/shortcut/PrefJPanel.java @ 14012

Last change on this file since 14012 was 14012, checked in by Don-vip, 6 weeks ago

see #16453 - proper support of different keyboard layouts

  • Property svn:eol-style set to native
File size: 17.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.preferences.shortcut;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.GridBagConstraints;
11import java.awt.GridBagLayout;
12import java.awt.GridLayout;
13import java.awt.Insets;
14import java.awt.Toolkit;
15import java.awt.event.KeyEvent;
16import java.awt.im.InputContext;
17import java.lang.reflect.Field;
18import java.util.ArrayList;
19import java.util.LinkedHashMap;
20import java.util.List;
21import java.util.Map;
22import java.util.regex.PatternSyntaxException;
23
24import javax.swing.AbstractAction;
25import javax.swing.BorderFactory;
26import javax.swing.BoxLayout;
27import javax.swing.DefaultComboBoxModel;
28import javax.swing.JCheckBox;
29import javax.swing.JLabel;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32import javax.swing.JTable;
33import javax.swing.KeyStroke;
34import javax.swing.ListSelectionModel;
35import javax.swing.RowFilter;
36import javax.swing.SwingConstants;
37import javax.swing.UIManager;
38import javax.swing.event.DocumentEvent;
39import javax.swing.event.DocumentListener;
40import javax.swing.event.ListSelectionEvent;
41import javax.swing.event.ListSelectionListener;
42import javax.swing.table.AbstractTableModel;
43import javax.swing.table.DefaultTableCellRenderer;
44import javax.swing.table.TableColumnModel;
45import javax.swing.table.TableModel;
46import javax.swing.table.TableRowSorter;
47
48import org.openstreetmap.josm.data.preferences.NamedColorProperty;
49import org.openstreetmap.josm.gui.util.GuiHelper;
50import org.openstreetmap.josm.gui.widgets.JosmComboBox;
51import org.openstreetmap.josm.gui.widgets.JosmTextField;
52import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
53import org.openstreetmap.josm.tools.KeyboardUtils;
54import org.openstreetmap.josm.tools.Logging;
55import org.openstreetmap.josm.tools.Shortcut;
56
57/**
58 * This is the keyboard preferences content.
59 */
60public class PrefJPanel extends JPanel {
61
62    // table of shortcuts
63    private final AbstractTableModel model;
64    // this are the display(!) texts for the checkboxes. Let the JVM do the i18n for us <g>.
65    // Ok, there's a real reason for this: The JVM should know best how the keys are labelled
66    // on the physical keyboard. What language pack is installed in JOSM is completely
67    // independent from the keyboard's labelling. But the operation system's locale
68    // usually matches the keyboard. This even works with my English Windows and my German keyboard.
69    private static final String SHIFT = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
70            KeyEvent.SHIFT_DOWN_MASK).getModifiers());
71    private static final String CTRL = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
72            KeyEvent.CTRL_DOWN_MASK).getModifiers());
73    private static final String ALT = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
74            KeyEvent.ALT_DOWN_MASK).getModifiers());
75    private static final String META = KeyEvent.getModifiersExText(KeyStroke.getKeyStroke(KeyEvent.VK_A,
76            KeyEvent.META_DOWN_MASK).getModifiers());
77
78    // A list of keys to present the user. Sadly this really is a list of keys Java knows about,
79    // not a list of real physical keys. If someone knows how to get that list?
80    private static Map<Integer, String> keyList = setKeyList();
81
82    private final JCheckBox cbAlt = new JCheckBox();
83    private final JCheckBox cbCtrl = new JCheckBox();
84    private final JCheckBox cbMeta = new JCheckBox();
85    private final JCheckBox cbShift = new JCheckBox();
86    private final JCheckBox cbDefault = new JCheckBox();
87    private final JCheckBox cbDisable = new JCheckBox();
88    private final JosmComboBox<String> tfKey = new JosmComboBox<>();
89
90    private final JTable shortcutTable = new JTable();
91
92    private final JosmTextField filterField = new JosmTextField();
93
94    /** Creates new form prefJPanel */
95    public PrefJPanel() {
96        this.model = new ScListModel();
97        initComponents();
98    }
99
100    private static Map<Integer, String> setKeyList() {
101        Map<Integer, String> list = new LinkedHashMap<>();
102        String unknown = Toolkit.getProperty("AWT.unknown", "Unknown");
103        // Assume all known keys are declared in KeyEvent as "public static int VK_*"
104        for (Field field : KeyEvent.class.getFields()) {
105            // Ignore VK_KP_DOWN, UP, etc. because they have the same name as VK_DOWN, UP, etc. See #8340
106            if (field.getName().startsWith("VK_") && !field.getName().startsWith("VK_KP_")) {
107                try {
108                    int i = field.getInt(null);
109                    String s = KeyEvent.getKeyText(i);
110                    if (s != null && s.length() > 0 && !s.contains(unknown)) {
111                        list.put(Integer.valueOf(i), s);
112                    }
113                } catch (IllegalArgumentException | IllegalAccessException e) {
114                    Logging.error(e);
115                }
116            }
117        }
118        KeyboardUtils.getExtendedKeyCodes(InputContext.getInstance().getLocale()).entrySet()
119            .forEach(e -> list.put(e.getKey(), e.getValue().toString()));
120        list.put(Integer.valueOf(-1), "");
121        return list;
122    }
123
124    /**
125     * Show only shortcuts with descriptions containing given substring
126     * @param substring The substring used to filter
127     */
128    public void filter(String substring) {
129        filterField.setText(substring);
130    }
131
132    private static class ScListModel extends AbstractTableModel {
133        private final String[] columnNames = new String[]{tr("Action"), tr("Shortcut")};
134        private final transient List<Shortcut> data;
135
136        /**
137         * Constructs a new {@code ScListModel}.
138         */
139        ScListModel() {
140            data = Shortcut.listAll();
141        }
142
143        @Override
144        public int getColumnCount() {
145            return columnNames.length;
146        }
147
148        @Override
149        public int getRowCount() {
150            return data.size();
151        }
152
153        @Override
154        public String getColumnName(int col) {
155            return columnNames[col];
156        }
157
158        @Override
159        public Object getValueAt(int row, int col) {
160            return (col == 0) ? data.get(row).getLongText() : data.get(row);
161        }
162    }
163
164    private class ShortcutTableCellRenderer extends DefaultTableCellRenderer {
165
166        private final transient NamedColorProperty SHORTCUT_BACKGROUND_USER_COLOR = new NamedColorProperty(
167                marktr("Shortcut Background: User"),
168                new Color(200, 255, 200));
169        private final transient NamedColorProperty SHORTCUT_BACKGROUND_MODIFIED_COLOR = new NamedColorProperty(
170                marktr("Shortcut Background: Modified"),
171                new Color(255, 255, 200));
172
173        private final boolean name;
174
175        ShortcutTableCellRenderer(boolean name) {
176            this.name = name;
177        }
178
179        @Override
180        public Component getTableCellRendererComponent(JTable table, Object value, boolean
181                isSelected, boolean hasFocus, int row, int column) {
182            int row1 = shortcutTable.convertRowIndexToModel(row);
183            Shortcut sc = (Shortcut) model.getValueAt(row1, -1);
184            if (sc == null)
185                return null;
186            JLabel label = (JLabel) super.getTableCellRendererComponent(
187                table, name ? sc.getLongText() : sc.getKeyText(), isSelected, hasFocus, row, column);
188            GuiHelper.setBackgroundReadable(label, UIManager.getColor("Table.background"));
189            if (sc.isAssignedUser()) {
190                GuiHelper.setBackgroundReadable(label, SHORTCUT_BACKGROUND_USER_COLOR.get());
191            } else if (!sc.isAssignedDefault()) {
192                GuiHelper.setBackgroundReadable(label, SHORTCUT_BACKGROUND_MODIFIED_COLOR.get());
193            }
194            return label;
195        }
196    }
197
198    private void initComponents() {
199        CbAction action = new CbAction(this);
200        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
201        add(buildFilterPanel());
202
203        // This is the list of shortcuts:
204        shortcutTable.setModel(model);
205        shortcutTable.getSelectionModel().addListSelectionListener(action);
206        shortcutTable.setFillsViewportHeight(true);
207        shortcutTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
208        shortcutTable.setAutoCreateRowSorter(true);
209        TableColumnModel mod = shortcutTable.getColumnModel();
210        mod.getColumn(0).setCellRenderer(new ShortcutTableCellRenderer(true));
211        mod.getColumn(1).setCellRenderer(new ShortcutTableCellRenderer(false));
212        JScrollPane listScrollPane = new JScrollPane();
213        listScrollPane.setViewportView(shortcutTable);
214
215        JPanel listPane = new JPanel(new GridLayout());
216        listPane.add(listScrollPane);
217        add(listPane);
218
219        // and here follows the edit area. I won't object to someone re-designing it, it looks, um, "minimalistic" ;)
220
221        cbDefault.setAction(action);
222        cbDefault.setText(tr("Use default"));
223        cbShift.setAction(action);
224        cbShift.setText(SHIFT); // see above for why no tr()
225        cbDisable.setAction(action);
226        cbDisable.setText(tr("Disable"));
227        cbCtrl.setAction(action);
228        cbCtrl.setText(CTRL); // see above for why no tr()
229        cbAlt.setAction(action);
230        cbAlt.setText(ALT); // see above for why no tr()
231        tfKey.setAction(action);
232        tfKey.setModel(new DefaultComboBoxModel<>(keyList.values().toArray(new String[keyList.size()])));
233        cbMeta.setAction(action);
234        cbMeta.setText(META); // see above for why no tr()
235
236        JPanel shortcutEditPane = new JPanel(new GridLayout(5, 2));
237
238        shortcutEditPane.add(cbDefault);
239        shortcutEditPane.add(new JLabel());
240        shortcutEditPane.add(cbShift);
241        shortcutEditPane.add(cbDisable);
242        shortcutEditPane.add(cbCtrl);
243        shortcutEditPane.add(new JLabel(tr("Key:"), SwingConstants.LEFT));
244        shortcutEditPane.add(cbAlt);
245        shortcutEditPane.add(tfKey);
246        shortcutEditPane.add(cbMeta);
247
248        shortcutEditPane.add(new JLabel(tr("Attention: Use real keyboard keys only!")));
249
250        action.actionPerformed(null); // init checkboxes
251
252        add(shortcutEditPane);
253    }
254
255    private JPanel buildFilterPanel() {
256        // copied from PluginPreference
257        JPanel pnl = new JPanel(new GridBagLayout());
258        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
259        GridBagConstraints gc = new GridBagConstraints();
260
261        gc.anchor = GridBagConstraints.NORTHWEST;
262        gc.fill = GridBagConstraints.HORIZONTAL;
263        gc.weightx = 0.0;
264        gc.insets = new Insets(0, 0, 0, 5);
265        pnl.add(new JLabel(tr("Search:")), gc);
266
267        gc.gridx = 1;
268        gc.weightx = 1.0;
269        pnl.add(filterField, gc);
270        filterField.setToolTipText(tr("Enter a search expression"));
271        SelectAllOnFocusGainedDecorator.decorate(filterField);
272        filterField.getDocument().addDocumentListener(new FilterFieldAdapter());
273        pnl.setMaximumSize(new Dimension(300, 10));
274        return pnl;
275    }
276
277    // this allows to edit shortcuts. it:
278    //  * sets the edit controls to the selected shortcut
279    //  * enabled/disables the controls as needed
280    //  * writes the user's changes to the shortcut
281    // And after I finally had it working, I realized that those two methods
282    // are playing ping-pong (politically correct: table tennis, I know) and
283    // even have some duplicated code. Feel free to refactor, If you have
284    // more experience with GUI coding than I have.
285    private static class CbAction extends AbstractAction implements ListSelectionListener {
286        private final PrefJPanel panel;
287
288        CbAction(PrefJPanel panel) {
289            this.panel = panel;
290        }
291
292        private void disableAllModifierCheckboxes() {
293            panel.cbDefault.setEnabled(false);
294            panel.cbDisable.setEnabled(false);
295            panel.cbShift.setEnabled(false);
296            panel.cbCtrl.setEnabled(false);
297            panel.cbAlt.setEnabled(false);
298            panel.cbMeta.setEnabled(false);
299        }
300
301        @Override
302        public void valueChanged(ListSelectionEvent e) {
303            ListSelectionModel lsm = panel.shortcutTable.getSelectionModel(); // can't use e here
304            if (!lsm.isSelectionEmpty()) {
305                int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
306                Shortcut sc = (Shortcut) panel.model.getValueAt(row, -1);
307                panel.cbDefault.setSelected(!sc.isAssignedUser());
308                panel.cbDisable.setSelected(sc.getKeyStroke() == null);
309                panel.cbShift.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.SHIFT_DOWN_MASK) != 0);
310                panel.cbCtrl.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.CTRL_DOWN_MASK) != 0);
311                panel.cbAlt.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.ALT_DOWN_MASK) != 0);
312                panel.cbMeta.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.META_DOWN_MASK) != 0);
313                if (sc.getKeyStroke() != null) {
314                    panel.tfKey.setSelectedItem(keyList.get(sc.getKeyStroke().getKeyCode()));
315                } else {
316                    panel.tfKey.setSelectedItem(keyList.get(-1));
317                }
318                if (!sc.isChangeable()) {
319                    disableAllModifierCheckboxes();
320                    panel.tfKey.setEnabled(false);
321                } else {
322                    panel.cbDefault.setEnabled(true);
323                    actionPerformed(null);
324                }
325                panel.model.fireTableRowsUpdated(row, row);
326            } else {
327                disableAllModifierCheckboxes();
328                panel.tfKey.setEnabled(false);
329            }
330        }
331
332        @Override
333        public void actionPerformed(java.awt.event.ActionEvent e) {
334            ListSelectionModel lsm = panel.shortcutTable.getSelectionModel();
335            if (lsm != null && !lsm.isSelectionEmpty()) {
336                if (e != null) { // only if we've been called by a user action
337                    int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
338                    Shortcut sc = (Shortcut) panel.model.getValueAt(row, -1);
339                    Object selectedKey = panel.tfKey.getSelectedItem();
340                    if (panel.cbDisable.isSelected()) {
341                        sc.setAssignedModifier(-1);
342                    } else if (selectedKey == null || "".equals(selectedKey)) {
343                        sc.setAssignedModifier(KeyEvent.VK_CANCEL);
344                    } else {
345                        sc.setAssignedModifier(
346                                (panel.cbShift.isSelected() ? KeyEvent.SHIFT_DOWN_MASK : 0) |
347                                (panel.cbCtrl.isSelected() ? KeyEvent.CTRL_DOWN_MASK : 0) |
348                                (panel.cbAlt.isSelected() ? KeyEvent.ALT_DOWN_MASK : 0) |
349                                (panel.cbMeta.isSelected() ? KeyEvent.META_DOWN_MASK : 0)
350                        );
351                        for (Map.Entry<Integer, String> entry : keyList.entrySet()) {
352                            if (entry.getValue().equals(selectedKey)) {
353                                sc.setAssignedKey(entry.getKey());
354                            }
355                        }
356                    }
357                    sc.setAssignedUser(!panel.cbDefault.isSelected());
358                    valueChanged(null);
359                }
360                boolean state = !panel.cbDefault.isSelected();
361                panel.cbDisable.setEnabled(state);
362                state = state && !panel.cbDisable.isSelected();
363                panel.cbShift.setEnabled(state);
364                panel.cbCtrl.setEnabled(state);
365                panel.cbAlt.setEnabled(state);
366                panel.cbMeta.setEnabled(state);
367                panel.tfKey.setEnabled(state);
368            } else {
369                disableAllModifierCheckboxes();
370                panel.tfKey.setEnabled(false);
371            }
372        }
373    }
374
375    class FilterFieldAdapter implements DocumentListener {
376        private void filter() {
377            String expr = filterField.getText().trim();
378            if (expr.isEmpty()) {
379                expr = null;
380            }
381            try {
382                final TableRowSorter<? extends TableModel> sorter =
383                    (TableRowSorter<? extends TableModel>) shortcutTable.getRowSorter();
384                if (expr == null) {
385                    sorter.setRowFilter(null);
386                } else {
387                    expr = expr.replace("+", "\\+");
388                    // split search string on whitespace, do case-insensitive AND search
389                    List<RowFilter<Object, Object>> andFilters = new ArrayList<>();
390                    for (String word : expr.split("\\s+")) {
391                        andFilters.add(RowFilter.regexFilter("(?i)" + word));
392                    }
393                    sorter.setRowFilter(RowFilter.andFilter(andFilters));
394                }
395                model.fireTableDataChanged();
396            } catch (PatternSyntaxException | ClassCastException ex) {
397                Logging.warn(ex);
398            }
399        }
400
401        @Override
402        public void changedUpdate(DocumentEvent e) {
403            filter();
404        }
405
406        @Override
407        public void insertUpdate(DocumentEvent e) {
408            filter();
409        }
410
411        @Override
412        public void removeUpdate(DocumentEvent e) {
413            filter();
414        }
415    }
416}
Note: See TracBrowser for help on using the repository browser.