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