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 }