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