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.editor;
043:
044: import org.openide.util.NbBundle;
045: import org.openide.util.RequestProcessor;
046:
047: import java.awt.event.ActionEvent;
048: import java.util.logging.Level;
049: import java.util.logging.LogRecord;
050: import java.util.logging.Logger;
051: import javax.swing.Action;
052: import javax.swing.JMenuItem;
053: import javax.swing.text.BadLocationException;
054: import javax.swing.text.TextAction;
055: import javax.swing.text.JTextComponent;
056: import javax.swing.text.Caret;
057:
058: /**
059: * This is the parent of majority of the actions. It implements
060: * the necessary resetting depending of what is required
061: * by constructor of target action.
062: * The other thing implemented here is macro recording.
063: *
064: * @author Miloslav Metelka
065: * @version 1.00
066: */
067:
068: public abstract class BaseAction extends TextAction {
069:
070: /** Text of the menu item in popup menu for this action */
071: public static final String POPUP_MENU_TEXT = "PopupMenuText"; // NOI18N
072:
073: /** Prefix for the name of the key for description in locale support */
074: public static final String LOCALE_DESC_PREFIX = "desc-"; // NOI18N
075:
076: /** Prefix for the name of the key for popup description in locale support */
077: public static final String LOCALE_POPUP_PREFIX = "popup-"; // NOI18N
078:
079: /** Resource for the icon */
080: public static final String ICON_RESOURCE_PROPERTY = "IconResource"; // NOI18N
081:
082: /** Remove the selected text at the action begining */
083: public static final int SELECTION_REMOVE = 1;
084:
085: /** Reset magic caret position */
086: public static final int MAGIC_POSITION_RESET = 2;
087:
088: /**
089: * Reset abbreviation accounting to empty string.
090: * @deprecated Not used anymore.
091: */
092: public static final int ABBREV_RESET = 4;
093:
094: /** Prevents adding the new undoable edit to the old one when the next
095: * document change occurs.
096: */
097: public static final int UNDO_MERGE_RESET = 8;
098:
099: /** Reset word-match table */
100: public static final int WORD_MATCH_RESET = 16;
101:
102: /** Clear status bar text */
103: public static final int CLEAR_STATUS_TEXT = 32;
104:
105: /** The action will not be recorded if in macro recording */
106: public static final int NO_RECORDING = 64;
107:
108: /** Save current position in the jump list */
109: public static final int SAVE_POSITION = 128;
110:
111: /** The name of Action property. If the action has property NO_KEYBINDING set to true, it won't
112: * be listed in editor keybindings customizer list.
113: */
114: public static final String NO_KEYBINDING = "no-keybinding"; //NOI18N
115:
116: /** logger for reporting invoked actions */
117: private static Logger UILOG = Logger
118: .getLogger("org.netbeans.ui.actions.editor"); // NOI18N
119:
120: /** Bit mask of what should be updated when the action is performed before
121: * the action's real task is invoked.
122: */
123: protected int updateMask;
124:
125: private static boolean recording;
126: private static StringBuffer macroBuffer = new StringBuffer();
127: private static StringBuffer textBuffer = new StringBuffer();
128:
129: static final long serialVersionUID = -4255521122272110786L;
130:
131: public BaseAction(String name) {
132: this (name, 0);
133: }
134:
135: public BaseAction(String name, int updateMask) {
136: super (name);
137: this .updateMask = updateMask;
138: }
139:
140: /** Find a value in resource bundles.
141: * @deprecated this method is deprecated like the LocaleSupport which it uses by default.
142: * It should be replaced by implementing {@link #getShortDescriptionBundleClass()}
143: */
144: protected Object findValue(String key) {
145: return LocaleSupport.getString(key);
146: }
147:
148: public @Override
149: Object getValue(String key) {
150: Object obj = super .getValue(key);
151:
152: if (obj == null) {
153: obj = createDefaultValue(key);
154: if (obj != null) {
155: putValue(key, obj);
156: }
157: }
158:
159: return obj;
160: }
161:
162: /**
163: * This method is called when there is no value for the particular key.
164: * <br/>
165: * If the returned value is non-null it is remembered
166: * by {@link #putValue(String, Object)} so in that case this method
167: * is only called once.
168: *
169: * <p>
170: * <b>Note:</b> When overriding this method <code>super</code> implementation
171: * should always be called.
172: *
173: * @param key key for which the default value should be found.
174: * @return default value or null if the default value does not exist
175: * for the given key.
176: */
177: protected Object createDefaultValue(String key) {
178: Object ret = null;
179: if (SHORT_DESCRIPTION.equals(key)) {
180: Class bundleClass = getShortDescriptionBundleClass();
181: if (bundleClass != null) {
182: // The bundle key is just the action's name
183: String bundleKey = (String) getValue(Action.NAME);
184: ret = NbBundle.getBundle(bundleClass).getString(
185: bundleKey);
186: } else { // default to slow deprecated findValue()
187: // getDefaultShortDescription() is only called once for non-null ret value
188: ret = getDefaultShortDescription();
189: }
190:
191: } else if (POPUP_MENU_TEXT.equals(key)) {
192: String bundleKey = LOCALE_POPUP_PREFIX
193: + getValue(Action.NAME);
194: ret = findValue(bundleKey);
195: if (ret == null) {
196: ret = getValue(SHORT_DESCRIPTION);
197: }
198: }
199: return ret;
200: }
201:
202: /**
203: * Get the class in a package where resource bundle for localization
204: * of the short description of this action resides.
205: * <br/>
206: * By default this method returns null.
207: */
208: protected Class getShortDescriptionBundleClass() {
209: return null;
210: }
211:
212: /**
213: * Get the default value for {@link Action#SHORT_DESCRIPTION} property.
214: * <br>
215: * If this method returns non-empty value it will only be called once
216: * (its result will be remembered).
217: *
218: * @return value that will be use as result for
219: * <code>Action.getValue(Action.SHORT_DESCRIPTION)</code>.
220: */
221: protected Object getDefaultShortDescription() {
222: String actionName = (String) getValue(Action.NAME);
223: String localizerKey = LOCALE_DESC_PREFIX + actionName;
224: Object obj = findValue(localizerKey);
225: if (obj == null) {
226: obj = findValue(actionName);
227: if (obj == null)
228: obj = actionName;
229: }
230: return obj;
231: }
232:
233: /** This method is called once after the action is constructed
234: * and then each time the settings are changed.
235: * @param evt event describing the changed setting name. It's null
236: * if it's called after the action construction.
237: * @param kitClass class of the kit that created the actions
238: */
239: protected void settingsChange(SettingsChangeEvent evt,
240: Class kitClass) {
241: }
242:
243: /** This method is made final here as there's an important
244: * processing that must be done before the real action
245: * functionality is performed. It can include the following:
246: * 1. Updating of the target component depending on the update
247: * mask given in action constructor.
248: * 2. Possible macro recoding when the macro recording
249: * is turned on.
250: * The real action functionality should be done in
251: * the method actionPerformed(ActionEvent evt, JTextComponent target)
252: * which must be redefined by the target action.
253: */
254: public final void actionPerformed(final ActionEvent evt) {
255: final JTextComponent target = getTextComponent(evt);
256:
257: if (recording && 0 == (updateMask & NO_RECORDING)) {
258: recordAction(target, evt);
259: }
260:
261: updateComponent(target);
262:
263: if (UILOG.isLoggable(Level.FINE)) {
264: String actionName = getValue(NAME) != null ? getValue(NAME)
265: .toString().toLowerCase() : null;
266: if (actionName != null
267: && !"default-typed".equals(actionName) && //NOI18N
268: -1 == actionName.indexOf("caret") && //NOI18N
269: -1 == actionName.indexOf("delete") && //NOI18N
270: -1 == actionName.indexOf("selection") && //NOI18N
271: -1 == actionName.indexOf("build-tool-tip") && //NOI18N
272: -1 == actionName.indexOf("build-popup-menu") && //NOI18N
273: -1 == actionName.indexOf("page-up") && //NOI18N
274: -1 == actionName.indexOf("page-down") && //NOI18N
275: -1 == actionName.indexOf("-kit-install") //NOI18N
276: ) {
277: LogRecord r = new LogRecord(Level.FINE,
278: "UI_ACTION_EDITOR"); // NOI18N
279: r.setResourceBundle(NbBundle
280: .getBundle(BaseAction.class));
281: if (evt != null) {
282: r.setParameters(new Object[] { evt, evt.toString(),
283: this , toString(), getValue(NAME) });
284: } else {
285: r.setParameters(new Object[] { "no-ActionEvent",
286: "no-ActionEvent", this , toString(),
287: getValue(NAME) }); //NOI18N
288: }
289: r.setLoggerName(UILOG.getName());
290: UILOG.log(r);
291: }
292: }
293:
294: if (asynchonous()) {
295: RequestProcessor.getDefault().post(new Runnable() {
296: public void run() {
297: actionPerformed(evt, target);
298: }
299: });
300: } else {
301: actionPerformed(evt, target);
302: }
303: }
304:
305: private void recordAction(JTextComponent target, ActionEvent evt) {
306: if (this == target.getKeymap().getDefaultAction()) { // defaultKeyTyped
307: textBuffer.append(getFilteredActionCommand(evt
308: .getActionCommand()));
309: } else { // regular action
310: if (textBuffer.length() > 0) {
311: if (macroBuffer.length() > 0)
312: macroBuffer.append(' ');
313: macroBuffer.append(encodeText(textBuffer.toString()));
314: textBuffer.setLength(0);
315: }
316: if (macroBuffer.length() > 0)
317: macroBuffer.append(' ');
318: String name = (String) getValue(Action.NAME);
319: macroBuffer.append(encodeActionName(name));
320: }
321: }
322:
323: private String getFilteredActionCommand(String cmd) {
324: if (cmd == null || cmd.length() == 0)
325: return "";
326: char ch = cmd.charAt(0);
327: if ((ch >= 0x20) && (ch != 0x7F))
328: return cmd;
329: else
330: return "";
331: }
332:
333: boolean startRecording(JTextComponent target) {
334: if (recording)
335: return false;
336: recording = true;
337: macroBuffer.setLength(0);
338: textBuffer.setLength(0);
339: Utilities.setStatusText(target, NbBundle.getBundle(
340: BaseAction.class).getString("macro-recording"));
341: return true;
342: }
343:
344: String stopRecording(JTextComponent target) {
345: if (!recording)
346: return null;
347:
348: if (textBuffer.length() > 0) {
349: if (macroBuffer.length() > 0)
350: macroBuffer.append(' ');
351: macroBuffer.append(encodeText(textBuffer.toString()));
352: }
353: String retVal = macroBuffer.toString();
354: recording = false;
355: Utilities.setStatusText(target, ""); // NOI18N
356: return retVal;
357: }
358:
359: private String encodeText(String s) {
360: char[] text = s.toCharArray();
361: StringBuffer encoded = new StringBuffer("\""); // NOI18N
362: for (int i = 0; i < text.length; i++) {
363: char c = text[i];
364: if (c == '"' || c == '\\')
365: encoded.append('\\');
366: encoded.append(c);
367: }
368: return encoded.append('"').toString();
369: }
370:
371: private String encodeActionName(String s) {
372: char[] actionName = s.toCharArray();
373: StringBuffer encoded = new StringBuffer();
374: for (int i = 0; i < actionName.length; i++) {
375: char c = actionName[i];
376: if (Character.isWhitespace(c) || c == '\\')
377: encoded.append('\\');
378: encoded.append(c);
379: }
380: return encoded.toString();
381: }
382:
383: /** The target method that performs the real action functionality.
384: * @param evt action event describing the action that occured
385: * @param target target component where the action occured. It's retrieved
386: * by the TextAction.getTextComponent(evt).
387: */
388: public abstract void actionPerformed(ActionEvent evt,
389: JTextComponent target);
390:
391: protected boolean asynchonous() {
392: return false;
393: }
394:
395: public JMenuItem getPopupMenuItem(JTextComponent target) {
396: return null;
397: }
398:
399: public String getPopupMenuText(JTextComponent target) {
400: String txt = (String) getValue(POPUP_MENU_TEXT);
401: if (txt == null) {
402: txt = (String) getValue(NAME);
403: }
404: return txt;
405: }
406:
407: /** Update the component according to the update mask specified
408: * in the constructor of the action.
409: * @param target target component to be updated.
410: */
411: public void updateComponent(JTextComponent target) {
412: updateComponent(target, this .updateMask);
413: }
414:
415: /** Update the component according to the given update mask
416: * @param target target component to be updated.
417: * @param updateMask mask that specifies what will be updated
418: */
419: public void updateComponent(JTextComponent target, int updateMask) {
420: if (target != null
421: && target.getDocument() instanceof BaseDocument) {
422: BaseDocument doc = (BaseDocument) target.getDocument();
423: boolean writeLocked = false;
424:
425: try {
426: // remove selected text
427: if ((updateMask & SELECTION_REMOVE) != 0) {
428: writeLocked = true;
429: doc.extWriteLock();
430: Caret caret = target.getCaret();
431: if (caret != null
432: && Utilities.isSelectionShowing(caret)) {
433: int dot = caret.getDot();
434: int markPos = caret.getMark();
435: if (dot < markPos) { // swap positions
436: int tmpPos = dot;
437: dot = markPos;
438: markPos = tmpPos;
439: }
440: try {
441: target.getDocument().remove(markPos,
442: dot - markPos);
443: } catch (BadLocationException e) {
444: Utilities.annotateLoggable(e);
445: }
446: }
447: }
448:
449: // reset magic caret position
450: if ((updateMask & MAGIC_POSITION_RESET) != 0) {
451: if (target.getCaret() != null)
452: target.getCaret().setMagicCaretPosition(null);
453: }
454:
455: // reset merging of undoable edits
456: if ((updateMask & UNDO_MERGE_RESET) != 0) {
457: doc.resetUndoMerge();
458: }
459:
460: // reset word matching
461: if ((updateMask & WORD_MATCH_RESET) != 0) {
462: ((BaseTextUI) target.getUI()).getEditorUI()
463: .getWordMatch().clear();
464: }
465:
466: // Clear status bar text
467: if (!recording && (updateMask & CLEAR_STATUS_TEXT) != 0) {
468: Utilities.clearStatusText(target);
469: }
470:
471: // Save current caret position in the jump-list
472: if ((updateMask & SAVE_POSITION) != 0) {
473: JumpList.checkAddEntry(target);
474: }
475:
476: } finally {
477: if (writeLocked) {
478: doc.extWriteUnlock();
479: }
480: }
481: }
482: }
483:
484: }
|