PageRenderTime 51ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/projects/netbeans-7.3/options.keymap/src/org/netbeans/modules/options/keymap/MutableShortcutsModel.java

https://gitlab.com/essere.lab.public/qualitas.class-corpus
Java | 610 lines | 417 code | 55 blank | 138 comment | 100 complexity | 50141a6b0dd615f811cca127d0824baa MD5 | raw file
  1. /*
  2. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  3. *
  4. * Copyright 2012 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. * If you wish your version of this file to be governed by only the CDDL
  28. * or only the GPL Version 2, indicate your decision by adding
  29. * "[Contributor] elects to include this software in this distribution
  30. * under the [CDDL or GPL Version 2] license." If you do not indicate a
  31. * single choice of license, a recipient has the option to distribute
  32. * your version of this file under either the CDDL, the GPL Version 2 or
  33. * to extend the choice of license to its licensees as provided above.
  34. * However, if you add GPL Version 2 code and therefore, elected the GPL
  35. * Version 2 license, then the option applies only if the new code is
  36. * made subject to such option by the copyright holder.
  37. *
  38. * Contributor(s):
  39. *
  40. * Portions Copyrighted 2012 Sun Microsystems, Inc.
  41. */
  42. package org.netbeans.modules.options.keymap;
  43. import java.io.IOException;
  44. import java.util.ArrayList;
  45. import java.util.Arrays;
  46. import java.util.Collection;
  47. import java.util.Collections;
  48. import java.util.HashMap;
  49. import java.util.HashSet;
  50. import java.util.Iterator;
  51. import java.util.LinkedHashSet;
  52. import java.util.List;
  53. import java.util.Map;
  54. import java.util.Set;
  55. import java.util.StringTokenizer;
  56. import javax.swing.KeyStroke;
  57. import org.netbeans.api.annotations.common.NonNull;
  58. import org.netbeans.api.annotations.common.NullAllowed;
  59. import org.netbeans.core.options.keymap.api.KeyStrokeUtils;
  60. import org.netbeans.core.options.keymap.api.ShortcutAction;
  61. import org.netbeans.core.options.keymap.api.ShortcutsFinder;
  62. import org.openide.util.Exceptions;
  63. import org.openide.util.Lookup;
  64. import org.openide.util.RequestProcessor;
  65. import org.openide.util.Task;
  66. import org.openide.util.Utilities;
  67. /**
  68. * Wrapper around the {@link KeymapModel}. This wrapper uses human-readable keystroke names,
  69. * and support local modifications. Once the modifications are {#link #apply applied}, they are
  70. * written to the shared storage. The underlying ShortcutsFinder (if any) is also
  71. * refreshed. The model is NOT thread-safe.
  72. * <p/>
  73. * The model should be cloned from the global ShortcutsFinder, then the caller may choose
  74. * to apply() the changes, or simply discard the entire data structure.
  75. *
  76. * @author Svata Dedic
  77. */
  78. class MutableShortcutsModel extends ShortcutsFinderImpl implements ShortcutsFinder.Writer {
  79. /**
  80. * Current profile
  81. */
  82. private String currentProfile;
  83. /**
  84. * Key: category name. Value = pair of List&lt;ShortcutAction>. The 1st List
  85. * holds all actions for the category AND subcategories, the 2nd List holds
  86. * list of actions in the category only. Initialized lazily by {@link #getItems}
  87. */
  88. private Map<String, List<Object>[]> categoryToActionsCache =
  89. new HashMap<String, List<Object>[]> ();
  90. /**
  91. * Profiles, which has been modified. All keybindings are searched in this Map
  92. * first.
  93. */
  94. private Map<String, Map<ShortcutAction, Set<String>>> modifiedProfiles =
  95. new HashMap<String, Map<ShortcutAction, Set<String>>> ();
  96. private Set<String> revertedProfiles = new HashSet<String>();
  97. private Set<ShortcutAction> revertedActions = new HashSet<ShortcutAction>();
  98. /**
  99. * Set of profiles to be deleted
  100. */
  101. private Set<String> deletedProfiles = new HashSet<String> ();
  102. /**
  103. * Global ShortcutsFinder to reset when the keymap is changed.
  104. */
  105. @NullAllowed
  106. private ShortcutsFinder master;
  107. public MutableShortcutsModel(@NonNull KeymapModel model, ShortcutsFinder master) {
  108. super(model);
  109. this.master = master == null ? Lookup.getDefault().lookup(ShortcutsFinder.class) : master;
  110. }
  111. List<String> getProfiles () {
  112. Set<String> result = new HashSet<String> (model.getProfiles ());
  113. result.addAll (modifiedProfiles.keySet ());
  114. List<String> r = new ArrayList<String> (result);
  115. Collections.sort (r);
  116. return r;
  117. }
  118. boolean isChangedProfile(String profile) {
  119. return modifiedProfiles.containsKey(profile);
  120. }
  121. boolean isCustomProfile (String profile) {
  122. return model.isCustomProfile (profile);
  123. }
  124. boolean deleteOrRestoreProfile (String profile) {
  125. if (model.isCustomProfile (profile)) {
  126. deletedProfiles.add (profile);
  127. modifiedProfiles.remove (profile);
  128. clearShortcuts(profile);
  129. return true;
  130. } else {
  131. modifiedProfiles.remove(profile);
  132. revertedProfiles.add(profile);
  133. clearShortcuts(profile);
  134. return false;
  135. }
  136. }
  137. protected String getCurrentProfile () {
  138. if (currentProfile == null) {
  139. return model.getCurrentProfile();
  140. } else {
  141. return currentProfile;
  142. }
  143. }
  144. void setCurrentProfile (String currentKeymap) {
  145. this.currentProfile = currentKeymap;
  146. }
  147. void cloneProfile (String newProfileName) {
  148. Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>> ();
  149. cloneProfile ("", result);
  150. modifiedProfiles.put (newProfileName, result);
  151. // just in case, if the profile was deleted, then created anew
  152. deletedProfiles.remove(newProfileName);
  153. }
  154. private void cloneProfile (
  155. String category, // name of currently resolved category
  156. Map<ShortcutAction, Set<String>> result
  157. ) {
  158. Iterator it = getItems (category).iterator ();
  159. while (it.hasNext ()) {
  160. Object o = it.next ();
  161. String[] shortcuts = getShortcuts ((ShortcutAction) o);
  162. result.put ((ShortcutAction)o, new HashSet<String> (Arrays.asList (shortcuts)));
  163. }
  164. }
  165. public ShortcutAction findActionForShortcut (String shortcut) {
  166. return findActionForShortcut (shortcut, "", false, null, "");
  167. }
  168. /**
  169. * Filters the actions and retains only those which come from the same KeymapManager
  170. * as the 'anchor' action. Actions from the same keymap manager are typically not allowed
  171. * to have the same key binding
  172. *
  173. * @param actions actions to filter
  174. * @param anchor action that identifies the KeymapManager
  175. * @return filtered action list, as a new collection
  176. */
  177. Collection<ShortcutAction> filterSameScope(Set<ShortcutAction> actions, ShortcutAction anchor) {
  178. return KeymapModel.filterSameScope(actions, anchor);
  179. }
  180. /**
  181. * Finds action with conflicting shortcut (or a prefix, for a multi-keybinding)
  182. * for a shortcut
  183. * @param shortcut the shortcut to look for
  184. * @return action with same shortcut, or shortcutprefix. If the prefix is same
  185. * but the rest of multi-keybinding is different, returns <code>null</code> (no conflict).
  186. */
  187. Set<ShortcutAction> findActionForShortcutPrefix(String shortcut) {
  188. Set<ShortcutAction> set = new HashSet<ShortcutAction>();
  189. if (shortcut.length() == 0) {
  190. return set;
  191. }
  192. //has to work with multi-keybinding properly,
  193. //ie. not allow 'Ctrl+J' and 'Ctrl+J X' at the same time
  194. if (shortcut.contains(" ")) {
  195. findActionForShortcut(shortcut.substring(0, shortcut.lastIndexOf(' ')), "", true, set, shortcut);
  196. } else {
  197. findActionForShortcut(shortcut, "", true, set, shortcut);
  198. }
  199. return set;
  200. }
  201. private ShortcutAction findActionForShortcut (String shortcut, String category, boolean prefixSearch, Set<ShortcutAction> set, String completeMultikeySC) {
  202. //search in modified profiles first
  203. Map<ShortcutAction, Set<String>> map = modifiedProfiles.get(getCurrentProfile());
  204. if (map != null) {
  205. for (Map.Entry<ShortcutAction, Set<String>> entry : map.entrySet()) {
  206. for (String sc : entry.getValue()) {
  207. if (prefixSearch) {
  208. if (sc.equals(shortcut) || (sc.startsWith(completeMultikeySC) && shortcut.equals(completeMultikeySC) && sc.contains(" "))) {
  209. set.add(entry.getKey());
  210. }
  211. } else if (sc.equals(shortcut)) {
  212. return entry.getKey();
  213. }
  214. }
  215. }
  216. }
  217. Iterator it = getItems (category).iterator ();
  218. while (it.hasNext ()) {
  219. Object o = it.next ();
  220. ShortcutAction action = (ShortcutAction) o;
  221. String[] shortcuts = getShortcuts (action);
  222. int i, k = shortcuts.length;
  223. for (i = 0; i < k; i++) {
  224. if (prefixSearch) {
  225. if (shortcuts[i].equals(shortcut) || (shortcuts[i].startsWith(completeMultikeySC) && shortcut.equals(completeMultikeySC) && shortcuts[i].contains(" "))) {
  226. set.add(action);
  227. }
  228. } else if (shortcuts[i].equals(shortcut)) {
  229. return action;
  230. }
  231. }
  232. }
  233. return null;
  234. }
  235. protected ShortcutAction findActionForId (String actionId, String category, boolean delegate) {
  236. // check whether the ID is not a duplicate one -> no action found:
  237. Iterator it = getItems (category).iterator ();
  238. while (it.hasNext ()) {
  239. Object o = it.next ();
  240. String id;
  241. if (delegate) {
  242. // fallback for issue #197068 - try to find actions also by their classname:
  243. id = LayersBridge.getOrigActionClass((ShortcutAction)o);
  244. } else {
  245. id = ((ShortcutAction) o).getId ();
  246. }
  247. if (id != null && actionId.equals (id)) {
  248. return (ShortcutAction) o;
  249. }
  250. }
  251. return null;
  252. }
  253. protected Map<ShortcutAction,Set<String>> getKeymap (String profile) {
  254. Map<ShortcutAction,Set<String>> base;
  255. if (revertedProfiles.contains(profile)) {
  256. base = model.getKeymapDefaults(profile);
  257. } else {
  258. base = super.getKeymap(profile);
  259. }
  260. if (modifiedProfiles.containsKey(profile)) {
  261. base = new HashMap<ShortcutAction,Set<String>>(base);
  262. base.putAll(modifiedProfiles.get(profile));
  263. }
  264. return base;
  265. }
  266. public String[] getShortcuts (ShortcutAction action) {
  267. String profile = getCurrentProfile();
  268. if (modifiedProfiles.containsKey (profile)) {
  269. // find it in modified shortcuts
  270. Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles.
  271. get (profile);
  272. if (actionToShortcuts.containsKey (action)) {
  273. Set<String> s = actionToShortcuts.get (action);
  274. return s.toArray (new String [s.size ()]);
  275. }
  276. }
  277. return super.getShortcuts(action);
  278. }
  279. /**
  280. * Set of all shortcuts used by current profile (including modifications)
  281. * In case there is a multikey keybinding used, its prefix is included
  282. * @return set of shortcuts
  283. */
  284. public Set<String> getAllCurrentlyUsedShortcuts() {
  285. Set<String> set = new LinkedHashSet<String>();
  286. //add modified shortcuts, if any
  287. String profile = getCurrentProfile();
  288. Set<ShortcutAction> processed = new HashSet<ShortcutAction>();
  289. Map<ShortcutAction, Set<String>> modMap = modifiedProfiles.get(profile);
  290. if (modMap != null) {
  291. processed.addAll(modMap.keySet());
  292. for (Map.Entry<ShortcutAction, Set<String>> entry : modMap.entrySet()) {
  293. for (String sc : entry.getValue()) {
  294. set.add(sc);
  295. if (sc.contains(" ")) { // NOI18N
  296. set.add(sc.substring(0, sc.indexOf(' ')));
  297. }
  298. }
  299. }
  300. }
  301. //add default shortcuts
  302. for (Map.Entry<ShortcutAction, Set<String>> entry : getProfileMap(profile).entrySet()) {
  303. // ignore entries, which are going to be overriden by modifiedProfiles.
  304. if (processed.contains(entry.getKey())) {
  305. continue;
  306. }
  307. for (String sc : entry.getValue()) {
  308. set.add(sc);
  309. if (sc.contains(" ")) {
  310. set.add(sc.substring(0, sc.indexOf(' ')));
  311. }
  312. }
  313. }
  314. return set;
  315. }
  316. void addShortcut (ShortcutAction action, String shortcut) {
  317. // delete old shortcut
  318. ShortcutAction act = findActionForShortcut (shortcut);
  319. Set<String> s = new LinkedHashSet<String> ();
  320. s.addAll (Arrays.asList (getShortcuts (action)));
  321. s.add (shortcut);
  322. setShortcuts (action, s);
  323. }
  324. /**
  325. * Reverts shortcuts. If there is a conflict between the restored shortucts and other
  326. * actions, the method will do nothing unless 'force' is true, and returns collection of conflicting actions.
  327. * Return value of null indicates successful change.
  328. *
  329. * @param action action to revert
  330. * @param force if true, does not check conflicts; used after user confirmation
  331. * @return {@code null} for success, or collection of conflicting actions
  332. */
  333. Collection<ShortcutAction> revertShortcutsToDefault(ShortcutAction action, boolean force) {
  334. if (model.isCustomProfile(getCurrentProfile())) {
  335. return null;
  336. }
  337. Map<ShortcutAction, Set<String>> m = model.getKeymapDefaults (getCurrentProfile());
  338. m = convertFromEmacs(m);
  339. Set<String> shortcuts = m.get(action);
  340. if (shortcuts == null) {
  341. shortcuts = Collections.<String>emptySet(); //this action has no default shortcut
  342. }
  343. //lets search for conflicting SCs
  344. Set<ShortcutAction> conflictingActions = new HashSet<ShortcutAction>();
  345. for(String sc : shortcuts) {
  346. ShortcutAction ac = findActionForShortcut(sc);
  347. if (ac != null && !ac.equals(action)) {
  348. conflictingActions.add(ac);
  349. }
  350. }
  351. // retain only conflicting actions from the same keymap manager
  352. Collection<ShortcutAction> filtered = KeymapModel.filterSameScope(conflictingActions, action);
  353. if (!filtered.isEmpty() && !force) {
  354. return conflictingActions;
  355. }
  356. revertedActions.add(action);
  357. setShortcuts(action, shortcuts);
  358. for (ShortcutAction a : filtered) {
  359. String[] ss = getShortcuts(a);
  360. Set<String> newSs = new HashSet<String>(Arrays.asList(ss));
  361. newSs.removeAll(shortcuts);
  362. setShortcuts(a, newSs);
  363. }
  364. return null;
  365. }
  366. public void setShortcuts (ShortcutAction action, Set<String> shortcuts) {
  367. Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles.get (getCurrentProfile());
  368. if (actionToShortcuts == null) {
  369. actionToShortcuts = new HashMap<ShortcutAction, Set<String>> ();
  370. modifiedProfiles.put (getCurrentProfile(), actionToShortcuts);
  371. }
  372. actionToShortcuts.put (action, shortcuts);
  373. }
  374. public void removeShortcut (ShortcutAction action, String shortcut) {
  375. Set<String> s = new LinkedHashSet<String> (Arrays.asList (getShortcuts (action)));
  376. s.remove (shortcut);
  377. setShortcuts(action, s);
  378. }
  379. /**
  380. * Simple guard against scheduling multiple tasks in advance. Also guards
  381. * against reentrancy.
  382. */
  383. private volatile boolean applyInProgress = false;
  384. public void apply () {
  385. postApply();
  386. }
  387. /* test only */ Task postApply() {
  388. if (applyInProgress) {
  389. return null;
  390. }
  391. applyInProgress = true;
  392. return RequestProcessor.getDefault ().post (new Runnable () {
  393. public void run () {
  394. for (String profile : revertedProfiles) {
  395. try {
  396. model.revertProfile(profile);
  397. } catch (IOException ex) {
  398. Exceptions.printStackTrace(ex);
  399. }
  400. }
  401. if (!revertedActions.isEmpty()) {
  402. try {
  403. model.revertActions(revertedActions);
  404. } catch (IOException ex) {
  405. Exceptions.printStackTrace(ex);
  406. }
  407. }
  408. for (String profile: modifiedProfiles.keySet()) {
  409. Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles.get (profile);
  410. actionToShortcuts = convertToEmacs (actionToShortcuts);
  411. model.changeKeymap (
  412. profile,
  413. actionToShortcuts
  414. );
  415. }
  416. for (String profile: deletedProfiles) {
  417. model.deleteProfile (profile);
  418. }
  419. model.setCurrentProfile (currentProfile);
  420. clearState();
  421. model = new KeymapModel ();
  422. applyInProgress = false;
  423. clearCache();
  424. if (master != null) {
  425. master.refreshActions();
  426. }
  427. }
  428. });
  429. }
  430. public boolean isChanged () {
  431. return (!modifiedProfiles.isEmpty ()) || !deletedProfiles.isEmpty () || !revertedProfiles.isEmpty() || !revertedActions.isEmpty();
  432. }
  433. private void clearState() {
  434. modifiedProfiles = new HashMap<String, Map<ShortcutAction, Set<String>>> ();
  435. deletedProfiles = new HashSet<String> ();
  436. revertedActions = new HashSet<ShortcutAction>();
  437. revertedProfiles = new HashSet<String>();
  438. currentProfile = null;
  439. }
  440. public void cancel () {
  441. clearState();
  442. }
  443. Map<String, Map<ShortcutAction, Set<String>>> getModifiedProfiles() {
  444. return modifiedProfiles;
  445. }
  446. Set<String> getDeletedProfiles() {
  447. return deletedProfiles;
  448. }
  449. void setModifiedProfiles(Map<String, Map<ShortcutAction, Set<String>>> mp) {
  450. this.modifiedProfiles = mp;
  451. }
  452. void setDeletedProfiles(Set<String> dp) {
  453. this.deletedProfiles = dp;
  454. }
  455. /**
  456. * Converts Map (ShortcutAction > Set (String (shortcut Alt+Shift+P))) to
  457. * Map (ShortcutAction > Set (String (shortcut AS-P))).
  458. */
  459. private static Map<ShortcutAction, Set<String>> convertToEmacs (Map<ShortcutAction, Set<String>> shortcuts) {
  460. Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>> ();
  461. for (Map.Entry<ShortcutAction, Set<String>> entry: shortcuts.entrySet()) {
  462. ShortcutAction action = entry.getKey();
  463. Set<String> newSet = new HashSet<String> ();
  464. for (String s: entry.getValue()) {
  465. if (s.length () == 0) continue;
  466. KeyStroke[] ks = getKeyStrokes (s, " ");
  467. if (ks == null)
  468. continue; // unparsable shortcuts ignorred
  469. StringBuffer sb = new StringBuffer (
  470. Utilities.keyToString (ks [0], true)
  471. );
  472. int i, k = ks.length;
  473. for (i = 1; i < k; i++)
  474. sb.append (' ').append (Utilities.keyToString (ks [i], true));
  475. newSet.add (sb.toString ());
  476. }
  477. result.put (action, newSet);
  478. }
  479. return result;
  480. }
  481. /**
  482. * Returns multi keystroke for given text representation of shortcuts
  483. * (like Alt+A B). Returns null if text is not parsable, and empty array
  484. * for empty string.
  485. */
  486. private static KeyStroke[] getKeyStrokes (String keyStrokes, String delim) {
  487. if (keyStrokes.length () == 0) return new KeyStroke [0];
  488. StringTokenizer st = new StringTokenizer (keyStrokes, delim);
  489. List<KeyStroke> result = new ArrayList<KeyStroke> ();
  490. while (st.hasMoreTokens ()) {
  491. String ks = st.nextToken ().trim ();
  492. KeyStroke keyStroke = KeyStrokeUtils.getKeyStroke (ks);
  493. if (keyStroke == null) return null; // text is not parsable
  494. result.add (keyStroke);
  495. }
  496. return result.toArray (new KeyStroke [result.size ()]);
  497. }
  498. public Set<String> getCategories() {
  499. return model.getActionCategories();
  500. }
  501. /**
  502. * Returns actions in the category and subcategories
  503. * @param category
  504. * @return
  505. */
  506. public List<Object/*Union2<String,ShortcutAction>*/> getItems (String category) {
  507. return getItems(category, true);
  508. }
  509. /**
  510. * Returns list of actions in the given category, and optionally in the sub-categories.
  511. *
  512. * @param category
  513. * @param prefix
  514. * @return
  515. */
  516. public List<Object/*Union2<String,ShortcutAction>*/> getItems (String category, boolean prefix) {
  517. List<ShortcutAction>[] result = (List<ShortcutAction>[])(List[])categoryToActionsCache.get (category);
  518. if (result == null) {
  519. List<ShortcutAction> allActions = new ArrayList<ShortcutAction>();
  520. List<ShortcutAction> thisActions = Collections.emptyList();
  521. Set<String> filtered = new HashSet<String>(model.getActionCategories());
  522. for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
  523. String cat = it.next();
  524. if (!cat.startsWith(category)) {
  525. it.remove();
  526. } else if (category.length() > 0 && cat.length() > category.length() && cat.charAt(category.length()) != '/') {
  527. it.remove();
  528. }
  529. }
  530. for (String c : filtered) {
  531. Collection<ShortcutAction> act = model.getActions(c);
  532. allActions.addAll(act);
  533. if (c.length() == category.length()) {
  534. thisActions = new ArrayList<ShortcutAction>(act);
  535. }
  536. }
  537. Collections.<ShortcutAction>sort (allActions, new KeymapViewModel.ActionsComparator ());
  538. if (!thisActions.isEmpty()) {
  539. Collections.<ShortcutAction>sort (thisActions, new KeymapViewModel.ActionsComparator ());
  540. }
  541. result = new List[] { allActions , thisActions };
  542. ((Map)categoryToActionsCache).put (category, result);
  543. }
  544. return (List)(prefix ? result[0] : result[1]);
  545. }
  546. boolean differsFromDefault(String profile) {
  547. if (modifiedProfiles.containsKey(profile)) {
  548. return true;
  549. }
  550. if (revertedProfiles.contains(profile)) {
  551. return false;
  552. }
  553. return !model.getKeymapDefaults(profile).equals(model.getKeymap(profile));
  554. }
  555. }