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.editor.keymap;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.lang.ref.Reference;
047: import java.lang.ref.WeakReference;
048: import java.util.ArrayList;
049: import java.util.Collections;
050: import java.util.HashMap;
051: import java.util.HashSet;
052: import java.util.List;
053: import java.util.Map;
054: import java.util.MissingResourceException;
055: import java.util.Set;
056: import java.util.StringTokenizer;
057: import java.util.logging.Level;
058: import java.util.logging.Logger;
059: import javax.swing.Action;
060: import javax.swing.KeyStroke;
061: import javax.swing.text.EditorKit;
062: import javax.swing.text.TextAction;
063: import org.netbeans.api.editor.mimelookup.MimeLookup;
064: import org.netbeans.api.editor.mimelookup.MimePath;
065: import org.netbeans.api.editor.settings.MultiKeyBinding;
066: import org.netbeans.core.options.keymap.api.ShortcutAction;
067: import org.netbeans.core.options.keymap.spi.KeymapManager;
068: import org.netbeans.editor.BaseAction;
069: import org.netbeans.editor.BaseKit;
070: import org.netbeans.modules.editor.NbEditorKit;
071: import org.netbeans.modules.editor.settings.storage.api.EditorSettings;
072: import org.netbeans.modules.editor.settings.storage.api.KeyBindingSettingsFactory;
073: import org.openide.ErrorManager;
074: import org.openide.filesystems.FileObject;
075: import org.openide.filesystems.FileSystem;
076: import org.openide.filesystems.Repository;
077: import org.openide.util.Lookup;
078: import org.openide.util.NbBundle;
079: import org.openide.util.Utilities;
080:
081: /**
082: * @author Jan Jancura
083: */
084: public final class EditorBridge extends KeymapManager {
085:
086: private static final Logger LOG = Logger
087: .getLogger(EditorBridge.class.getName());
088:
089: private static final String EDITOR_BRIDGE = "EditorBridge"; //NOI18N
090:
091: public EditorBridge() {
092: super (EDITOR_BRIDGE);
093: }
094:
095: private Map<String, Set<ShortcutAction>> actions;
096:
097: public Map<String, Set<ShortcutAction>> getActions() {
098: if (actions == null) {
099: Map<String, String> categories = readCategories();
100: actions = new HashMap<String, Set<ShortcutAction>>();
101: for (EditorAction action : getEditorActionsMap().values()) {
102: String category = categories.get(action.getId());
103: if (category == null) {
104: category = NbBundle.getMessage(EditorBridge.class,
105: "CTL_Other"); // NOI18N
106: }
107: Set<ShortcutAction> a = actions.get(category);
108: if (a == null) {
109: a = new HashSet<ShortcutAction>();
110: actions.put(category, a);
111: }
112: a.add(action);
113: }
114: actions.remove("Hidden"); // NOI18N
115: }
116: return actions;
117: }
118:
119: public void refreshActions() {
120: editorActionsMap = null;
121: actions = null;
122: actionNameToMimeTypes = new HashMap<String, Set<String>>();
123: }
124:
125: public String getCurrentProfile() {
126: return getEditorSettings().getCurrentKeyMapProfile();
127: }
128:
129: public void setCurrentProfile(String profile) {
130: getEditorSettings().setCurrentKeyMapProfile(profile);
131: }
132:
133: public boolean isCustomProfile(String profile) {
134: return getEditorSettings().isCustomKeymapProfile(profile);
135: }
136:
137: public Map<ShortcutAction, Set<String>> getKeymap(String profile) {
138: Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>>();
139: readKeymap(profile, null, false, result);
140: for (String mimeType : getEditorSettings().getMimeTypes()) {
141: readKeymap(profile, mimeType, false, result);
142: }
143: return Collections.unmodifiableMap(result);
144: }
145:
146: public Map<ShortcutAction, Set<String>> getDefaultKeymap(
147: String profile) {
148: Map<ShortcutAction, Set<String>> result = new HashMap<ShortcutAction, Set<String>>();
149: readKeymap(profile, null, true, result);
150: for (String mimeType : getEditorSettings().getMimeTypes()) {
151: readKeymap(profile, mimeType, true, result);
152: }
153: return Collections.unmodifiableMap(result);
154: }
155:
156: public void deleteProfile(String profile) {
157: KeyBindingSettingsFactory kbs = getKeyBindingSettings(null);
158: kbs.setKeyBindings(profile, null);
159: }
160:
161: /**
162: * Saves actionToShortcuts Map (GlobalAction > Set (String (shortcut)).
163: * Ignores all non EditorAction actions.
164: */
165: public void saveKeymap(String profile,
166: Map<ShortcutAction, Set<String>> actionToShortcuts) {
167:
168: // 1)
169: // convert actionToShortcuts: Map (ShortcutAction > Set (String (shortcut AS-M)))
170: // to mimeTypeToKeyBinding: Map (String (mimetype) > List (MultiKeyBinding)).
171: Map<String, List<MultiKeyBinding>> mimeTypeToKeyBinding = new HashMap<String, List<MultiKeyBinding>>(); // editor shortcuts
172: for (ShortcutAction action : actionToShortcuts.keySet()) {
173: Set<String> shortcuts = actionToShortcuts.get(action);
174:
175: action = action.getKeymapManagerInstance(EDITOR_BRIDGE);
176: if (!(action instanceof EditorAction)) {
177: continue;
178: }
179:
180: EditorAction editorAction = (EditorAction) action;
181: Set<String> mimeTypes = getMimeTypes(editorAction);
182:
183: for (String shortcut : shortcuts) {
184: MultiKeyBinding mkb = new MultiKeyBinding(
185: stringToKeyStrokes2(shortcut), editorAction
186: .getId());
187: for (String mimeType : mimeTypes) {
188: List<MultiKeyBinding> l = mimeTypeToKeyBinding
189: .get(mimeType);
190: if (l == null) {
191: l = new ArrayList<MultiKeyBinding>();
192: mimeTypeToKeyBinding.put(mimeType, l);
193: }
194: l.add(mkb);
195: }
196: }
197: }
198:
199: // 2) save all shortcuts
200: for (String mimeType : keyBindingSettings.keySet()) {
201: KeyBindingSettingsFactory kbs = keyBindingSettings
202: .get(mimeType);
203: kbs.setKeyBindings(profile, mimeTypeToKeyBinding
204: .get(mimeType));
205: }
206: }
207:
208: // private methods .........................................................
209: /** Map (String (mimeType) > Set (String (action name))). */
210: private Map<String, EditorAction> editorActionsMap;
211: /** Map (ShortcutAction > Set (String (mimeType))). */
212: private Map<String, Set<String>> actionNameToMimeTypes = new HashMap<String, Set<String>>();
213:
214: /**
215: * Returns map of all editor actions.
216: * Map (String (mimeType) > Set (String (action name)))
217: */
218: private Map<String, EditorAction> getEditorActionsMap() {
219: if (editorActionsMap == null) {
220: editorActionsMap = new HashMap<String, EditorAction>();
221: for (String mimeType : getEditorSettings().getMimeTypes()) {
222: initActionMap(mimeType);
223: }
224: initActionMap(null);
225: }
226: return editorActionsMap;
227: }
228:
229: private Set<String> getMimeTypes(EditorAction a) {
230: getEditorActionsMap(); // initialization
231: return actionNameToMimeTypes.get(a.getId());
232: }
233:
234: /**
235: * Loads editor actions for given mimeType to editorActionsMap.
236: */
237: private void initActionMap(String mimeType) {
238:
239: // 1) get EditorKit
240: EditorKit editorKit = null;
241: if (mimeType == null) {
242: editorKit = BaseKit.getKit(NbEditorKit.class);
243: } else {
244: Lookup mimeLookup = MimeLookup.getLookup(MimePath
245: .parse(mimeType));
246: editorKit = mimeLookup.lookup(EditorKit.class);
247: }
248: if (editorKit == null) {
249: if (LOG.isLoggable(Level.WARNING)) {
250: LOG.warning("EditorKit not found for: " + mimeType); //NOI18N
251: }
252: return;
253: }
254:
255: // 2) copy actions from EditorKit to actionMap
256: Action[] as = editorKit.getActions();
257: int i;
258: int k = as.length;
259: for (i = 0; i < k; i++) {
260: Object isHidden = as[i].getValue(BaseAction.NO_KEYBINDING);
261: if (isHidden instanceof Boolean
262: && ((Boolean) isHidden).booleanValue()) {
263: continue; // ignore hidden actions
264: }
265: EditorAction action = new EditorAction((TextAction) as[i]);
266: String id = action.getId();
267: editorActionsMap.put(id, action);
268: Set<String> s = actionNameToMimeTypes.get(id);
269: if (s == null) {
270: s = new HashSet<String>();
271: actionNameToMimeTypes.put(id, s);
272: }
273: s.add(mimeType);
274: }
275: }
276:
277: private EditorSettings editorSettings;
278:
279: private EditorSettings getEditorSettings() {
280: if (editorSettings == null) {
281: editorSettings = EditorSettings.getDefault();
282: }
283: return editorSettings;
284: }
285:
286: private final Map<String, KeyBindingSettingsFactory> keyBindingSettings = new HashMap<String, KeyBindingSettingsFactory>();
287: private static final String[] EMPTY = new String[0];
288:
289: private KeyBindingSettingsFactory getKeyBindingSettings(
290: String mimeType) {
291: KeyBindingSettingsFactory kbs = keyBindingSettings
292: .get(mimeType);
293: if (kbs == null) {
294: kbs = EditorSettings.getDefault().getKeyBindingSettings(
295: mimeType == null ? EMPTY
296: : new String[] { mimeType });
297:
298: keyBindingSettings.put(mimeType, kbs);
299: getListener().add(kbs);
300: }
301: return kbs;
302: }
303:
304: private Listener listener;
305:
306: private Listener getListener() {
307: if (listener == null) {
308: listener = new Listener(this );
309: }
310: return listener;
311: }
312:
313: private static class Listener implements PropertyChangeListener {
314:
315: private Reference<EditorBridge> model;
316: private Set<KeyBindingSettingsFactory> factories = new HashSet<KeyBindingSettingsFactory>();
317:
318: Listener(EditorBridge model) {
319: this .model = new WeakReference<EditorBridge>(model);
320: }
321:
322: void add(KeyBindingSettingsFactory kbsf) {
323: this .factories.add(kbsf);
324: kbsf.addPropertyChangeListener(this );
325: }
326:
327: private EditorBridge getModel() {
328: EditorBridge m = model.get();
329: if (m != null) {
330: return m;
331: }
332: for (KeyBindingSettingsFactory kbsf : factories) {
333: kbsf.removePropertyChangeListener(this );
334: }
335: factories = new HashSet<KeyBindingSettingsFactory>();
336: return null;
337: }
338:
339: public void propertyChange(PropertyChangeEvent evt) {
340: EditorBridge m = getModel();
341: if (m == null) {
342: return;
343: //model.keyMaps = new HashMap ();
344: }
345: }
346: }
347:
348: /**
349: * Reads keymap for given mimetype and profile to given map
350: * Map (ShortcutAction > Set (String (shortcut)))
351: */
352: private void readKeymap(String profile, String mimeType,
353: boolean defaults, Map<ShortcutAction, Set<String>> map) {
354: // 1) get list of MultiKeyBindings
355: KeyBindingSettingsFactory kbs = getKeyBindingSettings(mimeType);
356: if (kbs == null) {
357: return;
358: }
359: List<MultiKeyBinding> keyBindings = defaults ? kbs
360: .getKeyBindingDefaults(profile) : kbs
361: .getKeyBindings(profile);
362: if (keyBindings == null) {
363: return;
364: }
365: // 2) create Map (String (action name) > Set (String (shortcut)))
366: Map<String, Set<String>> actionNameToShortcuts = convertKeymap(keyBindings);
367:
368: // 3) create Map (EditorAction > Set (String (shortcut)))
369: for (String actionName : actionNameToShortcuts.keySet()) {
370: Set<String> keyStrokes = actionNameToShortcuts
371: .get(actionName);
372: ShortcutAction action = (ShortcutAction) getEditorActionsMap()
373: .get(actionName);
374: if (action == null) {
375: if (LOG.isLoggable(Level.FINE)) {
376: LOG.fine("action not found " + actionName); //NOI18N
377: }
378: continue;
379: }
380: Set<String> s = map.get(action);
381: if (s == null) {
382: map.put(action, keyStrokes);
383: } else {
384: s.addAll(keyStrokes);
385: }
386: }
387: }
388:
389: /**
390: * create Map (String (action name) > Set (String (shortcut AS-M)))
391: *
392: * @param keyBindings list of MultiKeyBindings
393: */
394: private static Map<String, Set<String>> convertKeymap(
395: List<MultiKeyBinding> keyBindings) {
396: Map<String, Set<String>> actionNameToShortcuts = new HashMap<String, Set<String>>();
397:
398: for (int i = 0; i < keyBindings.size(); i++) {
399: MultiKeyBinding mkb = keyBindings.get(i);
400: StringBuilder sb = new StringBuilder();
401:
402: for (int j = 0; j < mkb.getKeyStrokeCount(); j++) {
403: if (j > 0) {
404: sb.append(' '); //NOI18N
405: }
406: sb.append(Utilities.keyToString(mkb.getKeyStrokeList()
407: .get(j)));
408: }
409:
410: Set<String> keyStrokes = actionNameToShortcuts.get(mkb
411: .getActionName());
412: if (keyStrokes == null) {
413: keyStrokes = new HashSet<String>();
414: actionNameToShortcuts.put(mkb.getActionName(),
415: keyStrokes);
416: }
417: keyStrokes.add(sb.toString());
418: }
419:
420: return actionNameToShortcuts;
421: }
422:
423: private static Map<String, String> readCategories() {
424: Map<String, String> result = new HashMap<String, String>();
425: FileSystem fs = Repository.getDefault().getDefaultFileSystem();
426: FileObject fo = fs.findResource("OptionsDialog/Actions"); //NOI18N
427: if (fo == null) {
428: return result;
429: }
430: FileObject[] categories = fo.getChildren();
431: for (int i = 0; i < categories.length; i++) {
432: String categoryName = categories[i].getName();
433: String bundleName = (String) categories[i]
434: .getAttribute("SystemFileSystem.localizingBundle"); //NOI18N
435: if (bundleName != null) {
436: try {
437: categoryName = NbBundle.getBundle(bundleName)
438: .getString(categories[i].getPath());
439: } catch (MissingResourceException ex) {
440: ErrorManager.getDefault().notify(ex);
441: }
442: }
443: FileObject[] actions = categories[i].getChildren();
444: for (int j = 0; j < actions.length; j++) {
445: if (actions[j].getExt().length() > 0) {
446: continue;
447: }
448: String actionName = actions[j].getName();
449: result.put(actionName, categoryName);
450: }
451: }
452: return result;
453: }
454:
455: public List<String> getProfiles() {
456: return null;
457: }
458:
459: private static KeyStroke[] stringToKeyStrokes2(String key) {
460: List<KeyStroke> result = new ArrayList<KeyStroke>();
461:
462: for (StringTokenizer st = new StringTokenizer(key, " "); st
463: .hasMoreTokens();) { //NOI18N
464: String ks = st.nextToken().trim();
465: KeyStroke keyStroke = Utilities.stringToKey(ks);
466:
467: if (keyStroke == null) {
468: LOG.warning("'" + ks + "' is not a valid keystroke"); //NOI18N
469: return null;
470: }
471:
472: result.add(keyStroke);
473: }
474:
475: return result.toArray(new KeyStroke[result.size()]);
476: }
477:
478: private static final class EditorAction implements ShortcutAction {
479:
480: private TextAction action;
481: private String name;
482: private String id;
483: private String delegaitngActionId;
484:
485: public EditorAction(TextAction a) {
486: action = a;
487: }
488:
489: public String getDisplayName() {
490: if (name == null) {
491: name = (String) action
492: .getValue(Action.SHORT_DESCRIPTION);
493: if (name == null) {
494: LOG
495: .warning("The action "
496: + action
497: + " doesn't provide short description, using its name."); //NOI18N
498: name = getId();
499: }
500: name = name.replaceAll("&", "").trim(); //NOI18N
501: }
502: return name;
503: }
504:
505: public String getId() {
506: if (id == null) {
507: id = (String) action.getValue(Action.NAME);
508: assert id != null : "Actions must have name, offending action: "
509: + action; //NOI18N
510: }
511: return id;
512: }
513:
514: public String getDelegatingActionId() {
515: if (delegaitngActionId == null) {
516: delegaitngActionId = (String) action
517: .getValue(NbEditorKit.SYSTEM_ACTION_CLASS_NAME_PROPERTY);
518: }
519: return delegaitngActionId;
520: }
521:
522: @Override
523: public boolean equals(Object o) {
524: if (!(o instanceof EditorAction)) {
525: return false;
526: }
527: return ((EditorAction) o).getId().equals(getId());
528: }
529:
530: @Override
531: public int hashCode() {
532: return getId().hashCode();
533: }
534:
535: @Override
536: public String toString() {
537: return "EditorAction[" + getDisplayName() + ":" + getId()
538: + "]"; //NOI18N
539: }
540:
541: public ShortcutAction getKeymapManagerInstance(
542: String keymapManagerName) {
543: if (EDITOR_BRIDGE.equals(keymapManagerName)) {
544: return this ;
545: } else {
546: return null;
547: }
548: }
549:
550: public TextAction getRealAction() {
551: return action;
552: }
553: } // End of EditorAction
554: }
|