001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.preferences.shortcut;
003    
004    import java.awt.Color;
005    import java.awt.Component;
006    import java.awt.Dimension;
007    import java.awt.GridBagConstraints;
008    import java.awt.GridBagLayout;
009    import java.awt.Insets;
010    import java.awt.Toolkit;
011    
012    import static org.openstreetmap.josm.tools.I18n.marktr;
013    import static org.openstreetmap.josm.tools.I18n.tr;
014    
015    import java.awt.event.KeyEvent;
016    import java.lang.reflect.Field;
017    import java.util.ArrayList;
018    import java.util.LinkedHashMap;
019    import java.util.Map;
020    
021    import java.util.regex.PatternSyntaxException;
022    import javax.swing.AbstractAction;
023    import javax.swing.BorderFactory;
024    import javax.swing.BoxLayout;
025    import javax.swing.DefaultComboBoxModel;
026    import javax.swing.JCheckBox;
027    import javax.swing.JLabel;
028    import javax.swing.JPanel;
029    import javax.swing.JScrollPane;
030    import javax.swing.JTable;
031    import javax.swing.JTextField;
032    import javax.swing.KeyStroke;
033    import javax.swing.ListSelectionModel;
034    import javax.swing.RowFilter;
035    import javax.swing.SwingConstants;
036    import javax.swing.event.DocumentEvent;
037    import javax.swing.event.DocumentListener;
038    import javax.swing.event.ListSelectionEvent;
039    import javax.swing.event.ListSelectionListener;
040    import javax.swing.table.AbstractTableModel;
041    import javax.swing.table.TableModel;
042    import javax.swing.table.DefaultTableCellRenderer;
043    import javax.swing.table.TableColumnModel;
044    
045    import javax.swing.table.TableRowSorter;
046    import org.openstreetmap.josm.Main;
047    import org.openstreetmap.josm.gui.widgets.JosmComboBox;
048    import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
049    import org.openstreetmap.josm.tools.Shortcut;
050    
051    /**
052     * This is the keyboard preferences content.
053     * If someone wants to merge it with ShortcutPreference.java, feel free.
054     */
055    public class PrefJPanel extends JPanel {
056    
057        // table of shortcuts
058        private AbstractTableModel model;
059        // comboboxes of modifier groups, mapping selectedIndex to real data
060        private static int[] modifInts = new int[]{
061            -1,
062            0,
063            KeyEvent.SHIFT_DOWN_MASK,
064            KeyEvent.CTRL_DOWN_MASK,
065            KeyEvent.ALT_DOWN_MASK,
066            KeyEvent.META_DOWN_MASK,
067            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK,
068            KeyEvent.ALT_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK,
069            KeyEvent.META_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK,
070            KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK,
071            KeyEvent.CTRL_DOWN_MASK | KeyEvent.META_DOWN_MASK,
072            KeyEvent.ALT_DOWN_MASK | KeyEvent.META_DOWN_MASK,
073            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK,
074            KeyEvent.META_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK
075        };
076        // and here are the texts fro the comboboxes
077        private static String[] modifList = new String[] {
078            tr("disabled"),
079            tr("no modifier"),
080            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[2]).getModifiers()),
081            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[3]).getModifiers()),
082            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[4]).getModifiers()),
083            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[5]).getModifiers()),
084            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[6]).getModifiers()),
085            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[7]).getModifiers()),
086            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[8]).getModifiers()),
087            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[9]).getModifiers()),
088            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[10]).getModifiers()),
089            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[11]).getModifiers()),
090            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[12]).getModifiers()),
091            KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, modifInts[13]).getModifiers())
092        };
093        // this are the display(!) texts for the checkboxes. Let the JVM do the i18n for us <g>.
094        // Ok, there's a real reason for this: The JVM should know best how the keys are labelled
095        // on the physical keyboard. What language pack is installed in JOSM is completely
096        // independent from the keyboard's labelling. But the operation system's locale
097        // usually matches the keyboard. This even works with my English Windows and my German
098        // keyboard.
099        private static String SHIFT = KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.SHIFT_DOWN_MASK).getModifiers());
100        private static String CTRL  = KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_DOWN_MASK).getModifiers());
101        private static String ALT   = KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.ALT_DOWN_MASK).getModifiers());
102        private static String META  = KeyEvent.getKeyModifiersText(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.META_DOWN_MASK).getModifiers());
103    
104        // A list of keys to present the user. Sadly this really is a list of keys Java knows about,
105        // not a list of real physical keys. If someone knows how to get that list?
106        private static Map<Integer, String> keyList = setKeyList();
107    
108        private static Map<Integer, String> setKeyList() {
109            Map<Integer, String> list = new LinkedHashMap<Integer, String>();
110            String unknown = Toolkit.getProperty("AWT.unknown", "Unknown");
111            // Assume all known keys are declared in KeyEvent as "public static int VK_*"
112            for (Field field : KeyEvent.class.getFields()) {
113                if (field.getName().startsWith("VK_")) {
114                    try {
115                        int i = field.getInt(null);
116                        String s = KeyEvent.getKeyText(i);
117                        if (s != null && s.length() > 0 && !s.contains(unknown)) {
118                            list.put(Integer.valueOf(i), s);
119                            //System.out.println(i+": "+s);
120                        }
121                    } catch (Exception e) {
122                        e.printStackTrace();
123                    }
124                }
125            }
126            list.put(Integer.valueOf(-1), "");
127            return list;
128        }
129    
130        private JCheckBox cbAlt = new JCheckBox();
131        private JCheckBox cbCtrl = new JCheckBox();
132        private JCheckBox cbMeta = new JCheckBox();
133        private JCheckBox cbShift = new JCheckBox();
134        private JCheckBox cbDefault = new JCheckBox();
135        private JCheckBox cbDisable = new JCheckBox();
136        private JosmComboBox tfKey = new JosmComboBox();
137    
138        JTable shortcutTable = new JTable();
139    
140        private JTextField filterField = new JTextField();
141    
142        /** Creates new form prefJPanel */
143        // Ain't those auto-generated comments helpful or what? <g>
144        public PrefJPanel(AbstractTableModel model) {
145            this.model = model;
146            initComponents();
147        }
148    
149        private class ShortcutTableCellRenderer extends DefaultTableCellRenderer {
150    
151            private boolean name;
152    
153            public ShortcutTableCellRenderer(boolean name) {
154                this.name = name;
155            }
156            
157            @Override
158            public Component getTableCellRendererComponent(JTable table, Object value, boolean
159                    isSelected, boolean hasFocus, int row, int column) {
160                int row1 = shortcutTable.convertRowIndexToModel(row);
161                Shortcut sc = (Shortcut)model.getValueAt(row1, -1);
162                if (sc==null) return null;
163                JLabel label = (JLabel) super.getTableCellRendererComponent(
164                    table, name ? sc.getLongText() : sc.getKeyText(), isSelected, hasFocus, row, column);
165                label.setBackground(Main.pref.getUIColor("Table.background"));
166                if (isSelected) {
167                    label.setForeground(Main.pref.getUIColor("Table.foreground"));
168                }
169                if(sc.getAssignedUser()) {
170                    label.setBackground(Main.pref.getColor(
171                            marktr("Shortcut Background: User"),
172                            new Color(200,255,200)));
173                } else if(!sc.getAssignedDefault()) {
174                    label.setBackground(Main.pref.getColor(
175                            marktr("Shortcut Background: Modified"),
176                            new Color(255,255,200)));
177                }
178                return label;
179            }
180        }
181        
182        private void initComponents() {
183            JPanel listPane = new JPanel();
184            JScrollPane listScrollPane = new JScrollPane();
185            JPanel shortcutEditPane = new JPanel();
186    
187            CbAction action = new CbAction(this);
188            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
189            add(buildFilterPanel());
190            listPane.setLayout(new java.awt.GridLayout());
191    
192            // This is the list of shortcuts:
193            shortcutTable.setModel(model);
194            shortcutTable.getSelectionModel().addListSelectionListener(new CbAction(this));
195            shortcutTable.setFillsViewportHeight(true);
196            shortcutTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
197            shortcutTable.setAutoCreateRowSorter(true);
198            TableColumnModel mod = shortcutTable.getColumnModel();
199            mod.getColumn(0).setCellRenderer(new ShortcutTableCellRenderer(true));
200            mod.getColumn(1).setCellRenderer(new ShortcutTableCellRenderer(false));
201            listScrollPane.setViewportView(shortcutTable);
202    
203            listPane.add(listScrollPane);
204    
205            add(listPane);
206    
207            // and here follows the edit area. I won't object to someone re-designing it, it looks, um, "minimalistic" ;)
208            shortcutEditPane.setLayout(new java.awt.GridLayout(5, 2));
209    
210            cbDefault.setAction(action);
211            cbDefault.setText(tr("Use default"));
212            cbShift.setAction(action);
213            cbShift.setText(SHIFT); // see above for why no tr()
214            cbDisable.setAction(action);
215            cbDisable.setText(tr("Disable"));
216            cbCtrl.setAction(action);
217            cbCtrl.setText(CTRL); // see above for why no tr()
218            cbAlt.setAction(action);
219            cbAlt.setText(ALT); // see above for why no tr()
220            tfKey.setAction(action);
221            tfKey.setModel(new DefaultComboBoxModel(keyList.values().toArray()));
222            cbMeta.setAction(action);
223            cbMeta.setText(META); // see above for why no tr()
224    
225            shortcutEditPane.add(cbDefault);
226            shortcutEditPane.add(new JLabel());
227            shortcutEditPane.add(cbShift);
228            shortcutEditPane.add(cbDisable);
229            shortcutEditPane.add(cbCtrl);
230            shortcutEditPane.add(new JLabel(tr("Key:"), SwingConstants.LEFT));
231            shortcutEditPane.add(cbAlt);
232            shortcutEditPane.add(tfKey);
233            shortcutEditPane.add(cbMeta);
234    
235            shortcutEditPane.add(new JLabel(tr("Attention: Use real keyboard keys only!")));
236    
237            action.actionPerformed(null); // init checkboxes
238    
239            add(shortcutEditPane);
240        }
241    
242        private JPanel buildFilterPanel() {
243            // copied from PluginPreference
244            JPanel pnl  = new JPanel(new GridBagLayout());
245            pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
246            GridBagConstraints gc = new GridBagConstraints();
247    
248            gc.anchor = GridBagConstraints.NORTHWEST;
249            gc.fill = GridBagConstraints.HORIZONTAL;
250            gc.weightx = 0.0;
251            gc.insets = new Insets(0,0,0,5);
252            pnl.add(new JLabel(tr("Search:")), gc);
253    
254            gc.gridx = 1;
255            gc.weightx = 1.0;
256            pnl.add(filterField, gc);
257            filterField.setToolTipText(tr("Enter a search expression"));
258            SelectAllOnFocusGainedDecorator.decorate(filterField);
259            filterField.getDocument().addDocumentListener(new FilterFieldAdapter());
260            pnl.setMaximumSize(new Dimension(300,10));
261            return pnl;
262        }
263    
264        private void disableAllModifierCheckboxes() {
265            cbDefault.setEnabled(false);
266            cbDisable.setEnabled(false);
267            cbShift.setEnabled(false);
268            cbCtrl.setEnabled(false);
269            cbAlt.setEnabled(false);
270            cbMeta.setEnabled(false);
271        }
272    
273        // this allows to edit shortcuts. it:
274        //  * sets the edit controls to the selected shortcut
275        //  * enabled/disables the controls as needed
276        //  * writes the user's changes to the shortcut
277        // And after I finally had it working, I realized that those two methods
278        // are playing ping-pong (politically correct: table tennis, I know) and
279        // even have some duplicated code. Feel free to refactor, If you have
280        // more expirience with GUI coding than I have.
281        private class CbAction extends AbstractAction implements ListSelectionListener {
282            private PrefJPanel panel;
283            public CbAction (PrefJPanel panel) {
284                this.panel = panel;
285            }
286            public void valueChanged(ListSelectionEvent e) {
287                ListSelectionModel lsm = panel.shortcutTable.getSelectionModel(); // can't use e here
288                if (!lsm.isSelectionEmpty()) {
289                    int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
290                    Shortcut sc = (Shortcut)panel.model.getValueAt(row, -1);
291                    panel.cbDefault.setSelected(!sc.getAssignedUser());
292                    panel.cbDisable.setSelected(sc.getKeyStroke() == null);
293                    panel.cbShift.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.SHIFT_DOWN_MASK) != 0);
294                    panel.cbCtrl.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.CTRL_DOWN_MASK) != 0);
295                    panel.cbAlt.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.ALT_DOWN_MASK) != 0);
296                    panel.cbMeta.setSelected(sc.getAssignedModifier() != -1 && (sc.getAssignedModifier() & KeyEvent.META_DOWN_MASK) != 0);
297                    if (sc.getKeyStroke() != null) {
298                        tfKey.setSelectedItem(keyList.get(sc.getKeyStroke().getKeyCode()));
299                    } else {
300                        tfKey.setSelectedItem(keyList.get(-1));
301                    }
302                    if (!sc.isChangeable()) {
303                        disableAllModifierCheckboxes();
304                        panel.tfKey.setEnabled(false);
305                    } else {
306                        panel.cbDefault.setEnabled(true);
307                        actionPerformed(null);
308                    }
309                    model.fireTableRowsUpdated(row, row);
310                } else {
311                    panel.disableAllModifierCheckboxes();
312                    panel.tfKey.setEnabled(false);
313                }
314            }
315            public void actionPerformed(java.awt.event.ActionEvent e) {
316                ListSelectionModel lsm = panel.shortcutTable.getSelectionModel();
317                if (lsm != null && !lsm.isSelectionEmpty()) {
318                    if (e != null) { // only if we've been called by a user action
319                        int row = panel.shortcutTable.convertRowIndexToModel(lsm.getMinSelectionIndex());
320                        Shortcut sc = (Shortcut)panel.model.getValueAt(row, -1);
321                        if (panel.cbDisable.isSelected()) {
322                            sc.setAssignedModifier(-1);
323                        } else if (panel.tfKey.getSelectedItem() == null || panel.tfKey.getSelectedItem().equals("")) {
324                            sc.setAssignedModifier(KeyEvent.VK_CANCEL);
325                        } else {
326                            sc.setAssignedModifier(
327                                    (panel.cbShift.isSelected() ? KeyEvent.SHIFT_DOWN_MASK : 0) |
328                                    (panel.cbCtrl.isSelected() ? KeyEvent.CTRL_DOWN_MASK : 0) |
329                                    (panel.cbAlt.isSelected() ? KeyEvent.ALT_DOWN_MASK : 0) |
330                                    (panel.cbMeta.isSelected() ? KeyEvent.META_DOWN_MASK : 0)
331                            );
332                            for (Map.Entry<Integer, String> entry : keyList.entrySet()) {
333                                if (entry.getValue().equals(panel.tfKey.getSelectedItem())) {
334                                    sc.setAssignedKey(entry.getKey());
335                                }
336                            }
337                        }
338                        sc.setAssignedUser(!panel.cbDefault.isSelected());
339                        valueChanged(null);
340                    }
341                    boolean state = !panel.cbDefault.isSelected();
342                    panel.cbDisable.setEnabled(state);
343                    state = state && !panel.cbDisable.isSelected();
344                    panel.cbShift.setEnabled(state);
345                    panel.cbCtrl.setEnabled(state);
346                    panel.cbAlt.setEnabled(state);
347                    panel.cbMeta.setEnabled(state);
348                    panel.tfKey.setEnabled(state);
349                } else {
350                    panel.disableAllModifierCheckboxes();
351                    panel.tfKey.setEnabled(false);
352                }
353            }
354        }
355    
356        class FilterFieldAdapter implements DocumentListener {
357            public void filter() {
358                String expr = filterField.getText().trim();
359                if (expr.length()==0) { expr=null; }
360                try {
361                    final TableRowSorter<? extends TableModel> sorter =
362                        ((TableRowSorter<? extends TableModel> )shortcutTable.getRowSorter());
363                    if (expr == null) {
364                        sorter.setRowFilter(null);
365                    } else {
366                        expr = expr.replace("+", "\\+");
367                        // split search string on whitespace, do case-insensitive AND search
368                        ArrayList<RowFilter<Object, Object>> andFilters = new ArrayList<RowFilter<Object, Object>>();
369                        for (String word : expr.split("\\s+")) {
370                            andFilters.add(RowFilter.regexFilter("(?i)" + word));
371                        }
372                        sorter.setRowFilter(RowFilter.andFilter(andFilters));
373                    }
374                    model.fireTableDataChanged();
375                }
376                catch (PatternSyntaxException ex) { }
377                catch (ClassCastException ex2) { /* eliminate warning */  }
378            }
379    
380            public void changedUpdate(DocumentEvent arg0) { filter(); }
381            public void insertUpdate(DocumentEvent arg0) {  filter(); }
382            public void removeUpdate(DocumentEvent arg0) { filter(); }
383        }
384    
385    }