001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.options.keymap;
043:
044: import java.awt.Component;
045: import java.beans.PropertyChangeEvent;
046: import java.beans.PropertyChangeListener;
047: import java.util.ArrayList;
048: import java.util.Arrays;
049: import java.util.Collections;
050: import java.util.Comparator;
051: import java.util.HashMap;
052: import java.util.HashSet;
053: import java.util.Iterator;
054: import java.util.List;
055: import java.util.Map;
056: import java.util.Set;
057: import java.util.StringTokenizer;
058: import java.util.TreeMap;
059: import java.util.Vector;
060: import javax.swing.AbstractButton;
061: import javax.swing.JLabel;
062: import javax.swing.KeyStroke;
063: import javax.swing.SwingUtilities;
064: import javax.swing.event.TreeModelEvent;
065: import javax.swing.event.TreeModelListener;
066: import javax.swing.tree.TreeModel;
067: import javax.swing.tree.TreePath;
068: import org.netbeans.core.options.keymap.api.ShortcutAction;
069: import org.netbeans.core.options.keymap.api.ShortcutsFinder;
070: import org.openide.DialogDescriptor;
071: import org.openide.DialogDisplayer;
072: import org.openide.ErrorManager;
073: import org.openide.awt.Mnemonics;
074: import org.openide.util.NbBundle;
075: import org.openide.util.RequestProcessor;
076: import org.openide.util.Utilities;
077:
078: /**
079: *
080: * @author Jan Jancura
081: */
082: public class KeymapViewModel implements TreeModel, ShortcutsFinder {
083:
084: private Vector<TreeModelListener> listeners = new Vector<TreeModelListener>();
085: private String currentProfile;
086: private KeymapModel model = new KeymapModel();
087: // Map (String ("xx/yy") > List (Object (action)))
088: // tree of actions in folders
089: private Map<String, List<Object>> categoryToActionsCache = new HashMap<String, List<Object>>();
090: // Profile name to map of action to set of shortcuts
091: private Map<String, Map<ShortcutAction, Set<String>>> modifiedProfiles = new HashMap<String, Map<ShortcutAction, Set<String>>>();
092: // Set (String (profileName)).
093: private Set<String> deletedProfiles = new HashSet<String>();
094: // Map (String (keymapName) > Map (ShortcutAction > Set (String (shortcut Ctrl+F)))).
095: private Map<String, Map<ShortcutAction, Set<String>>> shortcutsCache = new HashMap<String, Map<ShortcutAction, Set<String>>>();
096:
097: static final ActionsComparator actionsComparator = new ActionsComparator();
098:
099: /**
100: * Creates a new instance of KeymapModel
101: */
102: public KeymapViewModel() {
103: currentProfile = model.getCurrentProfile();
104: }
105:
106: // TreeModel ...............................................................
107:
108: public Object getRoot() {
109: return "";
110: }
111:
112: public Object getChild(Object parent, int index) {
113: return getItems((String) parent).get(index);
114: }
115:
116: public int getChildCount(Object parent) {
117: if (parent instanceof String)
118: return getItems((String) parent).size();
119: return 0;
120: }
121:
122: public boolean isLeaf(Object node) {
123: return !(node instanceof String);
124: }
125:
126: public void valueForPathChanged(TreePath path, Object newValue) {
127: }
128:
129: public int getIndexOfChild(Object parent, Object child) {
130: return getItems((String) parent).indexOf(child);
131: }
132:
133: public void addTreeModelListener(TreeModelListener l) {
134: listeners.add(l);
135: }
136:
137: public void removeTreeModelListener(TreeModelListener l) {
138: listeners.remove(l);
139: }
140:
141: private void treeChanged() {
142: final Vector v = (Vector) listeners.clone();
143: SwingUtilities.invokeLater(new Runnable() {
144: public void run() {
145: TreeModelEvent tme = new TreeModelEvent(this ,
146: new TreePath(getRoot()));
147: int i, k = v.size();
148: for (i = 0; i < k; i++) {
149: ((TreeModelListener) v.get(i))
150: .treeStructureChanged(tme);
151: }
152: }
153: });
154: }
155:
156: private void nodeChanged(final TreePath path) {
157: final Vector v = (Vector) listeners.clone();
158: SwingUtilities.invokeLater(new Runnable() {
159: public void run() {
160: TreeModelEvent tme = new TreeModelEvent(this , path);
161: int i, k = v.size();
162: for (i = 0; i < k; i++)
163: ((TreeModelListener) v.get(i))
164: .treeNodesChanged(tme);
165: }
166: });
167: }
168:
169: // ListModel ...............................................................
170:
171: // Map (String ("xx/yy") > Map ...)
172: private Map<String, List<String>> categories;
173:
174: /**
175: * Returns map of categories and subcategories.
176: * Root: getCategories ().get ("")
177: * Subcategories: getCategories ().get (category)
178: *
179: * Map (String (category name) > List (String (category name))).
180: */
181: public Map<String, List<String>> getCategories() {
182: if (categories == null) {
183: categories = new TreeMap<String, List<String>>();
184: List<String> c = new ArrayList<String>(model
185: .getActionCategories());
186: Collections.sort(c);
187: for (String cn : c) {
188: String folderName = "";
189: StringTokenizer st = new StringTokenizer(cn, "/");
190: while (st.hasMoreTokens()) {
191: String name = st.nextToken();
192: List<String> asd = categories.get(folderName);
193: if (asd == null) {
194: asd = new ArrayList<String>();
195: categories.put(folderName, asd);
196: }
197: folderName = folderName.length() == 0 ? name
198: : folderName + '/' + name;
199: if (asd.isEmpty()
200: || !asd.get(asd.size() - 1).equals(
201: folderName))
202: asd.add(folderName);
203: }
204: }
205: }
206: return categories;
207: }
208:
209: /**
210: * Returns list of subcategories (String) for given category merged
211: * together with actions for give category.
212: */
213: public List<Object/*Union2<String,ShortcutAction>*/> getItems(
214: String category) {
215: List<Object> result = categoryToActionsCache.get(category);
216: if (result == null) {
217: result = new ArrayList<Object>();
218: List<String> ll = getCategories().get(category);
219: if (ll != null)
220: result.addAll(ll);
221: List<ShortcutAction> l = new ArrayList<ShortcutAction>(
222: model.getActions(category));
223: Collections.<ShortcutAction> sort(l,
224: new ActionsComparator());
225: result.addAll(l);
226: categoryToActionsCache.put(category, result);
227: //S ystem.out.println("getItems " + category + " : " + result);
228: }
229: return result;
230: }
231:
232: // public ListCellRenderer getListCellRenderer () {
233: // return new KeymapListRenderer (this);
234: // }
235:
236: // other methods ...........................................................
237:
238: List getProfiles() {
239: Set<String> result = new HashSet<String>(model.getProfiles());
240: result.addAll(modifiedProfiles.keySet());
241: List<String> r = new ArrayList<String>(result);
242: Collections.sort(r);
243: return r;
244: }
245:
246: boolean isCustomProfile(String profile) {
247: return model.isCustomProfile(profile);
248: }
249:
250: void deleteProfile(String profile) {
251: if (model.isCustomProfile(profile)) {
252: deletedProfiles.add(profile);
253: modifiedProfiles.remove(profile);
254: } else {
255: Map<ShortcutAction, Set<String>> m = model
256: .getKeymapDefaults(profile);
257: m = convertFromEmacs(m);
258: modifiedProfiles.put(profile, m);
259: treeChanged();
260: }
261: }
262:
263: String getCurrentProfile() {
264: return currentProfile;
265: }
266:
267: void setCurrentProfile(String currentKeymap) {
268: this .currentProfile = currentKeymap;
269: treeChanged();
270: }
271:
272: void cloneProfile(String newProfileName) {
273: Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>>();
274: cloneProfile("", result);
275: modifiedProfiles.put(newProfileName, result);
276: }
277:
278: private void cloneProfile(String category, // name of currently resolved category
279: Map<ShortcutAction, Set<String>> result) {
280: Iterator it = getItems(category).iterator();
281: while (it.hasNext()) {
282: Object o = it.next();
283: if (o instanceof String)
284: cloneProfile((String) o, result);
285: else {
286: String[] shortcuts = getShortcuts((ShortcutAction) o);
287: result.put((ShortcutAction) o, new HashSet<String>(
288: Arrays.asList(shortcuts)));
289: }
290: }
291: }
292:
293: public ShortcutAction findActionForShortcut(String shortcut) {
294: return findActionForShortcut(shortcut, "");
295: }
296:
297: private ShortcutAction findActionForShortcut(String shortcut,
298: String category) {
299: Iterator it = getItems(category).iterator();
300: while (it.hasNext()) {
301: Object o = it.next();
302: if (o instanceof String) {
303: ShortcutAction result = findActionForShortcut(shortcut,
304: (String) o);
305: if (result != null)
306: return result;
307: continue;
308: }
309: ShortcutAction action = (ShortcutAction) o;
310: String[] shortcuts = getShortcuts(action);
311: int i, k = shortcuts.length;
312: for (i = 0; i < k; i++) {
313: if (shortcuts[i].equals(shortcut))
314: return action;
315: if (shortcuts[i].equals(shortcut + " "))
316: return action;
317: }
318: }
319: return null;
320: }
321:
322: public ShortcutAction findActionForId(final String actionId) {
323: if (SwingUtilities.isEventDispatchThread())
324: return findActionForId(actionId, "");
325:
326: final ShortcutAction[] result = new ShortcutAction[1];
327: try {
328: SwingUtilities.invokeAndWait(new Runnable() {
329: public void run() {
330: result[0] = findActionForId(actionId, "");
331: }
332: });
333: } catch (Exception ex) {
334: ErrorManager.getDefault().notify(ex);
335: }
336: return result[0];
337: }
338:
339: private ShortcutAction findActionForId(String actionId,
340: String category) {
341: Iterator it = getItems(category).iterator();
342: while (it.hasNext()) {
343: Object o = it.next();
344: if (o instanceof String) {
345: ShortcutAction result = findActionForId(actionId,
346: (String) o);
347: if (result != null)
348: return result;
349: continue;
350: }
351: String id = ((ShortcutAction) o).getId();
352: if (actionId.equals(id))
353: return (ShortcutAction) o;
354: }
355: return null;
356: }
357:
358: public String[] getShortcuts(ShortcutAction action) {
359: if (modifiedProfiles.containsKey(currentProfile)) {
360: // find it in modified shortcuts
361: Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles
362: .get(currentProfile);
363: if (actionToShortcuts.containsKey(action)) {
364: Set<String> s = actionToShortcuts.get(action);
365: return s.toArray(new String[s.size()]);
366: }
367: }
368:
369: if (!shortcutsCache.containsKey(currentProfile)) {
370: // read profile and put it to cache
371: Map<ShortcutAction, Set<String>> profileMap = convertFromEmacs(model
372: .getKeymap(currentProfile));
373: shortcutsCache.put(currentProfile, profileMap);
374: }
375: Map<ShortcutAction, Set<String>> profileMap = shortcutsCache
376: .get(currentProfile);
377: Set<String> shortcuts = profileMap.get(action);
378: if (shortcuts == null)
379: return new String[0];
380: return shortcuts.toArray(new String[shortcuts.size()]);
381: }
382:
383: void addShortcut(TreePath path, String shortcut) {
384: // delete old shortcut
385: ShortcutAction action = findActionForShortcut(shortcut);
386: if (action != null) {
387: removeShortcut(action, shortcut);
388: }
389: action = (ShortcutAction) path.getLastPathComponent();
390: Set<String> s = new HashSet<String>();
391: s.add(shortcut);
392: s.addAll(Arrays.asList(getShortcuts(action)));
393: setShortcuts(action, s);
394: nodeChanged(path);
395: }
396:
397: public void setShortcuts(ShortcutAction action,
398: Set<String> shortcuts) {
399: Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles
400: .get(currentProfile);
401: if (actionToShortcuts == null) {
402: actionToShortcuts = new HashMap<ShortcutAction, Set<String>>();
403: modifiedProfiles.put(currentProfile, actionToShortcuts);
404: }
405: actionToShortcuts.put(action, shortcuts);
406: }
407:
408: void removeShortcut(TreePath path, String shortcut) {
409: ShortcutAction action = (ShortcutAction) path
410: .getLastPathComponent();
411: removeShortcut(action, shortcut);
412: nodeChanged(path);
413: }
414:
415: private void removeShortcut(ShortcutAction action, String shortcut) {
416: Set<String> s = new HashSet<String>(Arrays
417: .asList(getShortcuts(action)));
418: s.remove(shortcut);
419: setShortcuts(action, s);
420: }
421:
422: public void refreshActions() {
423: categoryToActionsCache = new HashMap<String, List<Object>>();
424: model.refreshActions();
425: }
426:
427: public void apply() {
428: RequestProcessor.getDefault().post(new Runnable() {
429: public void run() {
430: for (String profile : modifiedProfiles.keySet()) {
431: Map<ShortcutAction, Set<String>> actionToShortcuts = modifiedProfiles
432: .get(profile);
433: actionToShortcuts = convertToEmacs(actionToShortcuts);
434: model.changeKeymap(profile, actionToShortcuts);
435: }
436: for (String profile : deletedProfiles) {
437: model.deleteProfile(profile);
438: }
439: model.setCurrentProfile(currentProfile);
440: modifiedProfiles = new HashMap<String, Map<ShortcutAction, Set<String>>>();
441: deletedProfiles = new HashSet<String>();
442: shortcutsCache = new HashMap<String, Map<ShortcutAction, Set<String>>>();
443: model = new KeymapModel();
444: }
445: });
446: }
447:
448: public boolean isChanged() {
449: return (!modifiedProfiles.isEmpty())
450: || !deletedProfiles.isEmpty();
451: }
452:
453: public void cancel() {
454: modifiedProfiles = new HashMap<String, Map<ShortcutAction, Set<String>>>();
455: deletedProfiles = new HashSet<String>();
456: shortcutsCache = new HashMap<String, Map<ShortcutAction, Set<String>>>();
457: setCurrentProfile(model.getCurrentProfile());
458: model = new KeymapModel();
459: }
460:
461: /**
462: *
463: */
464: public String showShortcutsDialog() {
465: final ShortcutsDialog d = new ShortcutsDialog();
466: d.init(this );
467: final DialogDescriptor descriptor = new DialogDescriptor(d,
468: loc("Add_Shortcut_Dialog"), true, new Object[] {
469: DialogDescriptor.OK_OPTION,
470: DialogDescriptor.CANCEL_OPTION },
471: DialogDescriptor.OK_OPTION,
472: DialogDescriptor.DEFAULT_ALIGN, null, d.getListener());
473: descriptor.setClosingOptions(new Object[] {
474: DialogDescriptor.OK_OPTION,
475: DialogDescriptor.CANCEL_OPTION });
476: descriptor.setAdditionalOptions(new Object[] { d.getBClear(),
477: d.getBTab() });
478: descriptor.setValid(d.isShortcutValid());
479: d.addPropertyChangeListener(new PropertyChangeListener() {
480: public void propertyChange(PropertyChangeEvent evt) {
481: if (evt.getPropertyName() == null
482: || ShortcutsDialog.PROP_SHORTCUT_VALID
483: .equals(evt.getPropertyName())) {
484: descriptor.setValid(d.isShortcutValid());
485: }
486: }
487: });
488:
489: DialogDisplayer.getDefault().notify(descriptor);
490: if (descriptor.getValue() == DialogDescriptor.OK_OPTION)
491: return d.getTfShortcut().getText();
492: return null;
493: }
494:
495: /**
496: * Converts Map (ShortcutAction > Set (String (shortcut Alt+Shift+P))) to
497: * Map (ShortcutAction > Set (String (shortcut AS-P))).
498: */
499: private static Map<ShortcutAction, Set<String>> convertToEmacs(
500: Map<ShortcutAction, Set<String>> shortcuts) {
501: Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>>();
502: for (Map.Entry<ShortcutAction, Set<String>> entry : shortcuts
503: .entrySet()) {
504: ShortcutAction action = entry.getKey();
505: Set<String> newSet = new HashSet<String>();
506: for (String s : entry.getValue()) {
507: if (s.length() == 0)
508: continue;
509: KeyStroke[] ks = getKeyStrokes(s, " ");
510: if (ks == null)
511: continue; // unparsable shortcuts ignorred
512: StringBuffer sb = new StringBuffer(Utilities
513: .keyToString(ks[0]));
514: int i, k = ks.length;
515: for (i = 1; i < k; i++)
516: sb.append(' ').append(Utilities.keyToString(ks[i]));
517: newSet.add(sb.toString());
518: }
519: result.put(action, newSet);
520: }
521: return result;
522: }
523:
524: /**
525: * Converts Map (ShortcutAction > Set (String (shortcut AS-P))) to
526: * Map (ShortcutAction > Set (String (shortcut Alt+Shift+P))).
527: */
528: private static Map<ShortcutAction, Set<String>> convertFromEmacs(
529: Map<ShortcutAction, Set<String>> emacs) {
530: Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>>();
531: for (Map.Entry<ShortcutAction, Set<String>> entry : emacs
532: .entrySet()) {
533: ShortcutAction action = entry.getKey();
534: Set<String> shortcuts = new HashSet<String>();
535: for (String emacsShortcut : entry.getValue()) {
536: KeyStroke[] keyStroke = Utilities
537: .stringToKeys(emacsShortcut);
538: shortcuts
539: .add(Utils.getKeyStrokesAsText(keyStroke, " "));
540: }
541: result.put(action, shortcuts);
542: }
543: return result;
544: }
545:
546: /**
547: * Returns multi keystroke for given text representation of shortcuts
548: * (like Alt+A B). Returns null if text is not parsable, and empty array
549: * for empty string.
550: */
551: private static KeyStroke[] getKeyStrokes(String keyStrokes,
552: String delim) {
553: if (keyStrokes.length() == 0)
554: return new KeyStroke[0];
555: StringTokenizer st = new StringTokenizer(keyStrokes, delim);
556: List<KeyStroke> result = new ArrayList<KeyStroke>();
557: while (st.hasMoreTokens()) {
558: String ks = st.nextToken().trim();
559: KeyStroke keyStroke = Utils.getKeyStroke(ks);
560: if (keyStroke == null)
561: return null; // text is not parsable
562: result.add(keyStroke);
563: }
564: return result.toArray(new KeyStroke[result.size()]);
565: }
566:
567: private static String loc(String key) {
568: return NbBundle.getMessage(KeymapPanel.class, key);
569: }
570:
571: private static void loc(Component c, String key) {
572: if (c instanceof AbstractButton)
573: Mnemonics.setLocalizedText((AbstractButton) c, loc(key));
574: else
575: Mnemonics.setLocalizedText((JLabel) c, loc(key));
576: }
577:
578: // innerclasses ............................................................
579:
580: static class ActionsComparator implements Comparator {
581:
582: public int compare(Object o1, Object o2) {
583: if (o1 instanceof String)
584: if (o2 instanceof String)
585: return ((String) o1).compareTo((String) o2);
586: else
587: return 1;
588: else if (o2 instanceof String)
589: return -1;
590: else
591: return ((ShortcutAction) o1).getDisplayName()
592: .compareTo(
593: ((ShortcutAction) o2).getDisplayName());
594: }
595: }
596: }
|