PageRenderTime 56ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/projects/netbeans-7.3/o.n.core/src/org/netbeans/core/NbKeymap.java

https://gitlab.com/essere.lab.public/qualitas.class-corpus
Java | 503 lines | 385 code | 46 blank | 72 comment | 81 complexity | cfc881af9c2785662ca2b45511480fad MD5 | raw file
  1. /*
  2. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  3. *
  4. * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
  5. *
  6. * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
  7. * Other names may be trademarks of their respective owners.
  8. *
  9. * The contents of this file are subject to the terms of either the GNU
  10. * General Public License Version 2 only ("GPL") or the Common
  11. * Development and Distribution License("CDDL") (collectively, the
  12. * "License"). You may not use this file except in compliance with the
  13. * License. You can obtain a copy of the License at
  14. * http://www.netbeans.org/cddl-gplv2.html
  15. * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
  16. * specific language governing permissions and limitations under the
  17. * License. When distributing the software, include this License Header
  18. * Notice in each file and include the License file at
  19. * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
  20. * particular file as subject to the "Classpath" exception as provided
  21. * by Oracle in the GPL Version 2 section of the License file that
  22. * accompanied this code. If applicable, add the following below the
  23. * License Header, with the fields enclosed by brackets [] replaced by
  24. * your own identifying information:
  25. * "Portions Copyrighted [year] [name of copyright owner]"
  26. *
  27. * Contributor(s):
  28. *
  29. * The Original Software is NetBeans. The Initial Developer of the Original
  30. * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
  31. * Microsystems, Inc. All Rights Reserved.
  32. *
  33. * If you wish your version of this file to be governed by only the CDDL
  34. * or only the GPL Version 2, indicate your decision by adding
  35. * "[Contributor] elects to include this software in this distribution
  36. * under the [CDDL or GPL Version 2] license." If you do not indicate a
  37. * single choice of license, a recipient has the option to distribute
  38. * your version of this file under either the CDDL, the GPL Version 2 or
  39. * to extend the choice of license to its licensees as provided above.
  40. * However, if you add GPL Version 2 code and therefore, elected the GPL
  41. * Version 2 license, then the option applies only if the new code is
  42. * made subject to such option by the copyright holder.
  43. */
  44. package org.netbeans.core;
  45. import java.awt.EventQueue;
  46. import java.awt.Toolkit;
  47. import java.awt.event.ActionEvent;
  48. import java.awt.event.KeyEvent;
  49. import java.util.ArrayList;
  50. import java.util.Collections;
  51. import java.util.Comparator;
  52. import java.util.HashMap;
  53. import java.util.LinkedHashMap;
  54. import java.util.List;
  55. import java.util.Map;
  56. import java.util.TreeMap;
  57. import java.util.WeakHashMap;
  58. import java.util.logging.Level;
  59. import java.util.logging.Logger;
  60. import javax.swing.AbstractAction;
  61. import javax.swing.Action;
  62. import javax.swing.KeyStroke;
  63. import javax.swing.text.Keymap;
  64. import org.openide.awt.AcceleratorBinding;
  65. import org.openide.awt.StatusDisplayer;
  66. import org.openide.cookies.InstanceCookie;
  67. import org.openide.filesystems.FileAttributeEvent;
  68. import org.openide.filesystems.FileChangeAdapter;
  69. import org.openide.filesystems.FileChangeListener;
  70. import org.openide.filesystems.FileEvent;
  71. import org.openide.filesystems.FileObject;
  72. import org.openide.filesystems.FileRenameEvent;
  73. import org.openide.filesystems.FileUtil;
  74. import org.openide.loaders.DataObject;
  75. import org.openide.loaders.DataObjectNotFoundException;
  76. import org.openide.loaders.DataShadow;
  77. import org.openide.util.Lookup;
  78. import org.openide.util.RequestProcessor;
  79. import org.openide.util.Utilities;
  80. import org.openide.util.actions.SystemAction;
  81. import org.openide.util.lookup.ServiceProvider;
  82. @ServiceProvider(service=Keymap.class)
  83. public final class NbKeymap implements Keymap, Comparator<KeyStroke> {
  84. private static final RequestProcessor RP = new RequestProcessor(NbKeymap.class);
  85. /**
  86. * Extension, which indicates that the given binding should be removed by keymap
  87. * profile. The marker is ignored in the 'Shortcuts' base directory.
  88. */
  89. public static final String BINDING_REMOVED = "removed"; // NO18N
  90. /**
  91. * Extension of the DataShadow files; private in loaders API
  92. */
  93. public static final String SHADOW_EXT = "shadow"; // NOI18N
  94. //for unit testing only
  95. private RequestProcessor.Task refreshTask;
  96. private static final Action BROKEN = new AbstractAction("<broken>") { // NOI18N
  97. public void actionPerformed(ActionEvent e) {
  98. Toolkit.getDefaultToolkit().beep();
  99. }
  100. };
  101. /** Represents a binding of a keystroke. */
  102. private static class Binding {
  103. /** file defining an action; null if nested is not null */
  104. final FileObject actionDefinition;
  105. /** lazily instantiated actual action, in case actionDefinition is not null */
  106. private Action action;
  107. /** nested bindings; null if actionDefinition is not null */
  108. final Map<KeyStroke,Binding> nested;
  109. Binding(FileObject def) {
  110. actionDefinition = def;
  111. nested = null;
  112. }
  113. Binding() {
  114. actionDefinition = null;
  115. nested = new HashMap<KeyStroke,Binding>();
  116. }
  117. synchronized Action loadAction() {
  118. assert actionDefinition != null;
  119. if (action == null) {
  120. try {
  121. DataObject d = DataObject.find(actionDefinition);
  122. InstanceCookie ic = d.getLookup().lookup(InstanceCookie.class);
  123. if (ic == null) {
  124. return null;
  125. }
  126. action = (Action) ic.instanceCreate();
  127. } catch (/*ClassNotFoundException,IOException,ClassCastException*/Exception x) {
  128. LOG.log(Level.INFO, "could not load action for " + actionDefinition.getPath(), x);
  129. }
  130. }
  131. if (action == null) {
  132. action = BROKEN;
  133. }
  134. return action;
  135. }
  136. }
  137. private Map<KeyStroke,Binding> bindings;
  138. private Map<String,KeyStroke> id2Stroke;
  139. private final Map<Action,String> action2Id = new WeakHashMap<Action,String>();
  140. private FileChangeListener keymapListener;
  141. private FileChangeListener bindingsListener = new FileChangeAdapter() {
  142. public @Override void fileDataCreated(FileEvent fe) {
  143. refreshBindings();
  144. }
  145. public @Override void fileAttributeChanged(FileAttributeEvent fe) {
  146. refreshBindings();
  147. }
  148. public @Override void fileChanged(FileEvent fe) {
  149. refreshBindings();
  150. }
  151. public @Override void fileRenamed(FileRenameEvent fe) {
  152. refreshBindings();
  153. }
  154. public @Override void fileDeleted(FileEvent fe) {
  155. refreshBindings();
  156. }
  157. };
  158. private void refreshBindings() {
  159. refreshTask = RP.post(new Runnable() {
  160. @Override
  161. public void run() {
  162. doRefreshBindings();
  163. }
  164. });
  165. }
  166. private synchronized void doRefreshBindings() {
  167. bindings = null;
  168. bindings();
  169. }
  170. //for unit testing only
  171. boolean waitFinished() throws InterruptedException {
  172. return refreshTask != null ? refreshTask.waitFinished(9999) : false;
  173. }
  174. private synchronized Map<KeyStroke,Binding> bindings() {
  175. if (bindings == null) {
  176. bindings = new HashMap<KeyStroke,Binding>();
  177. boolean refresh = id2Stroke != null;
  178. id2Stroke = new TreeMap<String,KeyStroke>();
  179. List<FileObject> dirs = new ArrayList<FileObject>(2);
  180. dirs.add(FileUtil.getConfigFile("Shortcuts")); // NOI18N
  181. FileObject keymaps = FileUtil.getConfigFile("Keymaps"); // NOI18N
  182. if (keymaps != null) {
  183. String curr = (String) keymaps.getAttribute("currentKeymap"); // NOI18N
  184. if (curr == null) {
  185. curr = "NetBeans"; // NOI18N
  186. }
  187. dirs.add(keymaps.getFileObject(curr));
  188. if (keymapListener == null) {
  189. keymapListener = new FileChangeAdapter() {
  190. public @Override void fileAttributeChanged(FileAttributeEvent fe) {
  191. refreshBindings();
  192. }
  193. };
  194. keymaps.addFileChangeListener(keymapListener);
  195. }
  196. }
  197. Map<String,FileObject> id2Dir = new HashMap<String,FileObject>(); // #170677
  198. boolean processingProfile = false;
  199. // #217497: as translation String > KeyStroke[] is not unique, we must process .removed based on String
  200. // externalized keystroke. Note that LHM will retain iteration order the same as was originally processing
  201. // order of the folders, so replacing same KeyStroke[] with different externalization will still work.
  202. Map<String, FileObject> activeShortcuts = new LinkedHashMap<String, FileObject>();
  203. for (FileObject dir : dirs) {
  204. if (dir != null) {
  205. for (FileObject def : dir.getChildren()) {
  206. if (def.isData()) {
  207. boolean removed = processingProfile && BINDING_REMOVED.equals(def.getExt());
  208. String fn = def.getName();
  209. if (removed) {
  210. activeShortcuts.remove(fn);
  211. } else {
  212. activeShortcuts.put(fn, def);
  213. }
  214. }
  215. }
  216. dir.removeFileChangeListener(bindingsListener);
  217. dir.addFileChangeListener(bindingsListener);
  218. }
  219. // the 1st iteration is Shortcuts/ the next are profiles
  220. processingProfile = true;
  221. }
  222. outer: for (FileObject def : activeShortcuts.values()) {
  223. FileObject dir = def.getParent();
  224. if (def.isData()) {
  225. KeyStroke[] strokes = Utilities.stringToKeys(def.getName());
  226. if (strokes == null || strokes.length == 0) {
  227. LOG.log(Level.WARNING, "could not load parse name of " + def.getPath());
  228. continue;
  229. }
  230. Map<KeyStroke,Binding> binder = bindings;
  231. for (int i = 0; i < strokes.length - 1; i++) {
  232. Binding sub = binder.get(strokes[i]);
  233. if (sub != null && sub.nested == null) {
  234. LOG.log(Level.WARNING, "conflict between " + sub.actionDefinition.getPath() + " and " + def.getPath());
  235. sub = null;
  236. }
  237. if (sub == null) {
  238. binder.put(strokes[i], sub = new Binding());
  239. }
  240. binder = sub.nested;
  241. }
  242. // XXX warn about conflicts here too:
  243. binder.put(strokes[strokes.length - 1], new Binding(def));
  244. if (strokes.length == 1) {
  245. String id = idForFile(def);
  246. KeyStroke former = id2Dir.put(id, dir) == dir ? id2Stroke.get(id) : null;
  247. if (former == null || compare(former, strokes[0]) > 0) {
  248. id2Stroke.put(id, strokes[0]);
  249. }
  250. }
  251. }
  252. }
  253. if (refresh) {
  254. // Update accelerators of existing actions after switching keymap.
  255. EventQueue.invokeLater(new Runnable() {
  256. @Override
  257. public void run() {
  258. synchronized( action2Id ) {
  259. for (Map.Entry<Action, String> entry : action2Id.entrySet()) {
  260. entry.getKey().putValue(Action.ACCELERATOR_KEY, id2Stroke.get(entry.getValue()));
  261. }
  262. }
  263. }
  264. });
  265. }
  266. if (LOG.isLoggable(Level.FINE)) {
  267. for (Map.Entry<String,KeyStroke> entry : id2Stroke.entrySet()) {
  268. LOG.fine(entry.getValue() + " => " + entry.getKey());
  269. }
  270. }
  271. }
  272. return bindings;
  273. }
  274. private static final List<KeyStroke> context = new ArrayList<KeyStroke>(); // accessed reflectively from org.netbeans.editor.MultiKeymap
  275. private static void resetContext() {
  276. context.clear();
  277. StatusDisplayer.getDefault().setStatusText("");
  278. }
  279. public static KeyStroke[] getContext() { // called from ShortcutAndMenuKeyEventProcessor
  280. return context.toArray(new KeyStroke[context.size()]);
  281. }
  282. private static void shiftContext(KeyStroke stroke) {
  283. context.add(stroke);
  284. StringBuilder text = new StringBuilder();
  285. for (KeyStroke ks: context) {
  286. text.append(getKeyText(ks)).append(' ');
  287. }
  288. StatusDisplayer.getDefault().setStatusText(text.toString());
  289. }
  290. private static String getKeyText (KeyStroke keyStroke) {
  291. if (keyStroke == null) return "";
  292. String modifText = KeyEvent.getKeyModifiersText
  293. (keyStroke.getModifiers ());
  294. if ("".equals (modifText))
  295. return KeyEvent.getKeyText (keyStroke.getKeyCode ());
  296. return modifText + "+" + // NOI18N
  297. KeyEvent.getKeyText (keyStroke.getKeyCode ());
  298. }
  299. private static final Logger LOG = Logger.getLogger(NbKeymap.class.getName());
  300. public NbKeymap() {
  301. context.clear(); // may be useful in unit testing
  302. }
  303. public Action getDefaultAction() {
  304. return null;
  305. }
  306. public void setDefaultAction(Action a) {
  307. throw new UnsupportedOperationException();
  308. }
  309. public String getName() {
  310. return "Default"; // NOI18N
  311. }
  312. public Action getAction(final KeyStroke key) {
  313. switch (key.getKeyCode()) {
  314. case KeyEvent.VK_SHIFT:
  315. case KeyEvent.VK_CONTROL:
  316. case KeyEvent.VK_ALT:
  317. case KeyEvent.VK_ALT_GRAPH:
  318. case KeyEvent.VK_META:
  319. case KeyEvent.VK_UNDEFINED:
  320. case KeyEvent.CHAR_UNDEFINED:
  321. // Not actually a bindable key press.
  322. return null;
  323. }
  324. if (key.isOnKeyRelease()) {
  325. // Again, not really our business here.
  326. return null;
  327. }
  328. LOG.log(Level.FINE, "getAction {0}", key);
  329. Map<KeyStroke,Binding> binder = bindings();
  330. for (KeyStroke ctx : context) {
  331. Binding sub = binder.get(ctx);
  332. if (sub == null) {
  333. resetContext();
  334. return BROKEN; // no entry found after known prefix
  335. }
  336. binder = sub.nested;
  337. if (binder == null) {
  338. resetContext();
  339. return BROKEN; // anomalous, expected to find submap here
  340. }
  341. }
  342. Binding b = binder.get(key);
  343. if (b == null) {
  344. resetContext();
  345. return null; // normal, not found
  346. }
  347. if (b.nested == null) {
  348. resetContext();
  349. return b.loadAction(); // found real action
  350. } else {
  351. return new AbstractAction() {
  352. public void actionPerformed(ActionEvent e) {
  353. shiftContext(key); // entering submap
  354. }
  355. };
  356. }
  357. }
  358. public KeyStroke[] getBoundKeyStrokes() {
  359. assert false;
  360. return null;
  361. }
  362. public Action[] getBoundActions() {
  363. assert false;
  364. return null;
  365. }
  366. public KeyStroke[] getKeyStrokesForAction(Action a) {
  367. return new KeyStroke[0];
  368. }
  369. KeyStroke keyStrokeForAction(Action a, FileObject definingFile) {
  370. String id = idForFile(definingFile);
  371. bindings();
  372. synchronized( action2Id ) {
  373. action2Id.put(a, id);
  374. KeyStroke k = id2Stroke.get(id);
  375. LOG.log(Level.FINE, "found keystroke {0} for {1} with ID {2}", new Object[] {k, id(a), id});
  376. return k;
  377. }
  378. }
  379. @ServiceProvider(service=AcceleratorBinding.class)
  380. public static final class AcceleratorBindingImpl extends AcceleratorBinding {
  381. protected @Override KeyStroke keyStrokeForAction(Action action, FileObject definingFile) {
  382. Keymap km = Lookup.getDefault().lookup(Keymap.class);
  383. if (km instanceof NbKeymap) {
  384. return ((NbKeymap) km).keyStrokeForAction(action, definingFile);
  385. } else {
  386. LOG.log(Level.WARNING, "unexpected keymap: {0}", km);
  387. return null;
  388. }
  389. }
  390. }
  391. /**
  392. * Traverses shadow files to origin.
  393. * Returns impl class name if that is obvious (common for SystemAction's);
  394. * else just returns file path (usual for more modern registrations).
  395. */
  396. private static String idForFile(FileObject f) {
  397. if (f.hasExt(SHADOW_EXT)) {
  398. String path = (String) f.getAttribute("originalFile");
  399. if (path != null && f.getSize() == 0) {
  400. f = FileUtil.getConfigFile(path);
  401. if (f == null) {
  402. return path; // #169887: some race condition with layer init?
  403. }
  404. } else {
  405. try {
  406. DataObject d = DataObject.find(f);
  407. if (d instanceof DataShadow) {
  408. f = ((DataShadow) d).getOriginal().getPrimaryFile();
  409. }
  410. } catch (DataObjectNotFoundException x) {
  411. LOG.log(Level.FINE, f.getPath(), x);
  412. }
  413. }
  414. }
  415. // Cannot actually load instanceCreate methodvalue=... attribute; just want to see if it is there.
  416. if (f.hasExt("instance") && !Collections.list(f.getAttributes()).contains("instanceCreate")) {
  417. String clazz = (String) f.getAttribute("instanceClass");
  418. if (clazz != null) {
  419. return clazz;
  420. } else {
  421. return f.getName().replace('-', '.');
  422. }
  423. }
  424. return f.getPath();
  425. }
  426. public synchronized boolean isLocallyDefined(KeyStroke key) {
  427. assert false;
  428. return false;
  429. }
  430. public int compare(KeyStroke k1, KeyStroke k2) {
  431. //#47024 and 32733 - "Find" should not be shown as an accelerator,
  432. //nor should "Backspace" for Delete. Solution: The shorter text wins.
  433. return KeyEvent.getKeyText(k1.getKeyCode()).length() -
  434. KeyEvent.getKeyText(k2.getKeyCode()).length();
  435. }
  436. public void addActionForKeyStroke(KeyStroke key, Action a) {
  437. assert false;
  438. }
  439. public void removeKeyStrokeBinding(KeyStroke key) {
  440. assert false;
  441. }
  442. public void removeBindings() {
  443. assert false;
  444. }
  445. public Keymap getResolveParent() {
  446. return null;
  447. }
  448. public void setResolveParent(Keymap parent) {
  449. throw new UnsupportedOperationException();
  450. }
  451. private static Object id(Action a) {
  452. if (a instanceof SystemAction) {
  453. return a.getClass();
  454. }
  455. return a;
  456. }
  457. }