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