001: /*BEGIN_COPYRIGHT_BLOCK
002: *
003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are met:
008: * * Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * * Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
014: * names of its contributors may be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: *
029: * This software is Open Source Initiative approved Open Source Software.
030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
031: *
032: * This file is part of DrJava. Download the current version of this project
033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
034: *
035: * END_COPYRIGHT_BLOCK*/
036:
037: package edu.rice.cs.drjava.ui;
038:
039: import java.awt.EventQueue;
040: import java.awt.event.ActionEvent;
041: import java.awt.Color;
042: import java.awt.Font;
043:
044: import javax.swing.*;
045: import javax.swing.text.*;
046: import javax.swing.event.CaretListener;
047: import javax.swing.event.CaretEvent;
048: import javax.swing.event.DocumentListener;
049: import javax.swing.event.DocumentEvent;
050:
051: import java.io.Serializable;
052:
053: import edu.rice.cs.util.swing.Utilities;
054: import edu.rice.cs.util.text.ConsoleDocument;
055:
056: import edu.rice.cs.drjava.DrJava;
057: import edu.rice.cs.drjava.config.OptionConstants;
058: import edu.rice.cs.drjava.config.OptionListener;
059: import edu.rice.cs.drjava.config.OptionEvent;
060: import edu.rice.cs.drjava.model.repl.*;
061: import edu.rice.cs.drjava.platform.PlatformFactory;
062: import edu.rice.cs.drjava.model.ClipboardHistoryModel;
063:
064: /** Abstract class to handle hooking up a console document with its pane.
065: * TODO: move interactions specific functionality to InteractionsController
066: * @version $Id: AbstractConsoleController.java 4255 2007-08-28 19:17:37Z mgricken $
067: */
068: public abstract class AbstractConsoleController implements Serializable {
069:
070: /** Adapter for the Swing document used by the model.*/
071: protected final InteractionsDJDocument _adapter;
072:
073: /** Pane from the view. */
074: protected final InteractionsPane _pane;
075:
076: /** Style to use for default text. */
077: protected final SimpleAttributeSet _defaultStyle;
078:
079: /** Style to use for System.out. */
080: protected final SimpleAttributeSet _systemOutStyle;
081:
082: /** Style to use for System.err. */
083: protected final SimpleAttributeSet _systemErrStyle;
084:
085: /** Action to change focus to previous pane. Package private for testing purposes. */
086: volatile Action switchToPrevPaneAction;
087:
088: /** Action to change focus to next pane. */
089: volatile Action switchToNextPaneAction;
090:
091: /** Initializes the document adapter and interactions pane. Subclasses *must* call _init() at the end
092: * of their constructors.
093: */
094: protected AbstractConsoleController(InteractionsDJDocument adapter,
095: InteractionsPane pane) {
096: _adapter = adapter;
097: _pane = pane;
098: _defaultStyle = new SimpleAttributeSet();
099: _systemOutStyle = new SimpleAttributeSet();
100: _systemErrStyle = new SimpleAttributeSet();
101: }
102:
103: /** Gets the console document for this console.*/
104: public abstract ConsoleDocument getConsoleDoc();
105:
106: /** Initialization method. *Must* be called in constructor by all subclasses. */
107: protected void _init() {
108: _addDocumentStyles();
109: _setupModel();
110: _setupView();
111: }
112:
113: /** Adds AttributeSets as named styles to the document adapter. */
114: protected void _addDocumentStyles() {
115: // Default
116: _adapter.setDocStyle(ConsoleDocument.DEFAULT_STYLE,
117: _defaultStyle);
118: DrJava.getConfig().addOptionListener(
119: OptionConstants.DEFINITIONS_NORMAL_COLOR,
120: new OptionListener<Color>() {
121: public void optionChanged(OptionEvent<Color> oe) {
122: setDefaultFont(oe.value);
123: }
124: });
125:
126: // System.out
127: _systemOutStyle.addAttributes(_defaultStyle);
128: _systemOutStyle.addAttribute(StyleConstants.Foreground, DrJava
129: .getConfig().getSetting(
130: OptionConstants.SYSTEM_OUT_COLOR));
131: _adapter.setDocStyle(ConsoleDocument.SYSTEM_OUT_STYLE,
132: _systemOutStyle);
133: DrJava.getConfig().addOptionListener(
134: OptionConstants.SYSTEM_OUT_COLOR,
135: new OptionListener<Color>() {
136: public void optionChanged(OptionEvent<Color> oe) {
137: _systemOutStyle.addAttribute(
138: StyleConstants.Foreground, oe.value);
139: }
140: });
141:
142: // System.err
143: _systemErrStyle.addAttributes(_defaultStyle);
144: _systemErrStyle.addAttribute(StyleConstants.Foreground, DrJava
145: .getConfig().getSetting(
146: OptionConstants.SYSTEM_ERR_COLOR));
147: _adapter.setDocStyle(ConsoleDocument.SYSTEM_ERR_STYLE,
148: _systemErrStyle);
149: DrJava.getConfig().addOptionListener(
150: OptionConstants.SYSTEM_ERR_COLOR,
151: new OptionListener<Color>() {
152: public void optionChanged(OptionEvent<Color> oe) {
153: _systemErrStyle.addAttribute(
154: StyleConstants.Foreground, oe.value);
155: }
156: });
157: }
158:
159: /** Sets the font for the document, updating all existing text. This behavior is only necessary in Mac OS X, since
160: * setFont() works fine on JTextPane on all other tested platforms. This glitch in the Mac JVM still exists as of
161: * 11-28-06 in beta Java 6.0 build 88.
162: * @param f New font to use.
163: */
164: public void setDefaultFont(Font f) {
165: Color c = DrJava.getConfig().getSetting(
166: OptionConstants.DEFINITIONS_NORMAL_COLOR);
167: setDefaultFont(f, c);
168: }
169:
170: /** Sets the color for the document, updating all existing text. This behavior is only necessary in Mac OS X, since
171: * changing the main font works on all other tested platforms.
172: * @param c New color to use.
173: */
174: public void setDefaultFont(Color c) {
175: Font f = DrJava.getConfig().getSetting(
176: OptionConstants.FONT_MAIN);
177: setDefaultFont(f, c);
178: }
179:
180: /** Sets the font and color for the document, updating all existing text. This behavior is only necessary in Mac OS
181: * X, since setFont() and changing the main font works on all other tested platforms.
182: * @param f New font to use.
183: * @param c New color to use.
184: */
185: public void setDefaultFont(Font f, Color c) {
186: if (PlatformFactory.ONLY.isMacPlatform()) {
187: SimpleAttributeSet fontSet = new SimpleAttributeSet();
188: StyleConstants.setFontFamily(fontSet, f.getFamily());
189: StyleConstants.setFontSize(fontSet, f.getSize());
190: StyleConstants.setBold(fontSet, f.isBold());
191: StyleConstants.setItalic(fontSet, f.isItalic());
192: if (c != null) {
193: StyleConstants.setForeground(fontSet, c);
194: }
195: _adapter.setCharacterAttributes(0,
196: _adapter.getLength() + 1, fontSet, false);
197: _pane.setCharacterAttributes(fontSet, false);
198: _updateStyles(fontSet);
199: }
200: }
201:
202: /** Updates all document styles with the attributes contained in newSet.
203: * @param newSet Style containing new attributes to use.
204: */
205: protected void _updateStyles(AttributeSet newSet) {
206: _defaultStyle.addAttributes(newSet);
207: _systemOutStyle.addAttributes(newSet);
208: _systemErrStyle.addAttributes(newSet);
209: }
210:
211: /** Sets up the model.*/
212: protected abstract void _setupModel();
213:
214: /** Cached caret position from last invocation of CaretUpdateListener */
215: private volatile int _cachedCaretPos = 0;
216:
217: /** Cached prompt position from last invocation of CaretUpdateListener */
218: private volatile int _cachedPromptPos = 0;
219:
220: /** Must be called when new document is created if caret position != 0. */
221: public void setCachedCaretPos(int pos) {
222: _cachedCaretPos = pos;
223: }
224:
225: /** Must be called when new document is created if prompt position != 0. */
226: public void setCachedPromptPos(int pos) {
227: _cachedPromptPos = pos;
228: }
229:
230: /** Gets the current caret position. */
231: public int getCachedCaretPos() {
232: return _cachedCaretPos;
233: }
234:
235: /** Ensures that the caret always stays on or after the prompt, so that output is always scrolled to the bottom.
236: * (The prompt is always at the bottom.) This listener must not modify the console document itself, only the pane.
237: * It is given read access to the document by Swing when it is run as a listener immediately after a document update.
238: */
239: class CaretUpdateListener implements DocumentListener {
240: public void insertUpdate(final DocumentEvent e) {
241: /* Update caret position when text is inserted in the document. Fixes (?) bug #1571405. The promptPos is
242: * moved before the insertion is made so that this listener will see the udpated position. NOTE: The promptPos
243: * is NOT a Swing Position; it is an int offset maintianed by ConsoleDocument.
244: * The updating of the caretPosition is a confusing issue. This code has been written assuming that the
245: * processing of keyboard input advances the cursor automatically and it appears to work, but other program
246: * test suites (e.g. DefinitionsPaneTest) move the cursor manually.
247: */
248: int insertPos = e.getOffset();
249: int insertLen = e.getLength();
250: ConsoleDocument doc = getConsoleDoc();
251: int promptPos = doc.getPromptPos();
252: if (EventQueue.isDispatchThread()) { // insert was generated by keyboard input; do not move caret
253: _cachedCaretPos = insertPos + insertLen;
254: _cachedPromptPos = promptPos;
255: // EventQueue.invokeLater(new Runnable() { public void run() { _pane.setCaretPos(insertEnd); } });
256: return;
257: }
258:
259: final int newPos = getNewCaretPos(doc, promptPos,
260: insertPos, insertLen);
261: _cachedCaretPos = newPos;
262: _cachedPromptPos = promptPos;
263: // System.err.println("Setting cached caretpos to " + newPos);
264: /* Immediately update the caret position as part of the insertion, ignoring event thread only convention. As
265: * soon as the exclusive ReadLock is dropped on exiting this listener, a background thread running in the
266: * interpreter could write to the console, which is echoed in the interactions pane. If the caret position is
267: * not immediately updated, there could be a race because keyboard input events may already be queued in the
268: * event queue. */
269: _pane.setCaretPos(newPos);
270: }
271:
272: private int getNewCaretPos(ConsoleDocument doc, int promptPos,
273: int insertPos, int insertLen) {
274: final int docLen = doc.getLength();
275:
276: // if document has no prompt, place caret at end
277: if (!doc.hasPrompt() || promptPos == docLen)
278: return docLen;
279:
280: final int oldCaretPos = _cachedCaretPos;
281: final int oldPromptPos = _cachedPromptPos;
282: // If caret preceded the previous prompt, move it to the new prompPos.
283: if (oldCaretPos < oldPromptPos)
284: return promptPos;
285: // System.err.println("oldPromptPos = " + oldPromptPos + " oldCaretPos = " + oldCaretPos + " insertPos = " + insertPos
286: // + " insertLen = " + insertLen + " promptPos = " + promptPos + " caretPos = " +
287: // _pane.getCaretPosition());
288:
289: // Advance caret by insertion length because it is sitting at or beyond the prompt
290: int newCaretPos = oldCaretPos + insertLen;
291: return Math.min(newCaretPos, docLen);
292: }
293:
294: public void removeUpdate(DocumentEvent e) {
295: _ensureLegalCaretPos(e);
296: }
297:
298: public void changedUpdate(DocumentEvent e) {
299: _ensureLegalCaretPos(e);
300: }
301:
302: /** Moves the caret to the nearest legal position and caches the caret position. Assumes that the ReadLock is
303: * already held, which it is when these listener methods are run.
304: */
305: protected void _ensureLegalCaretPos(DocumentEvent e) {
306: // System.err.println("_ensureLegalCaretPosition(" + e + ") called");
307: ConsoleDocument doc = getConsoleDoc();
308: int newPos = _pane.getCaretPosition();
309: final int len = doc.getLength();
310: // System.err.println("caretPos = " + newPos + " len = " + len);
311: if (newPos > len) {
312: newPos = len;
313: Utilities.invokeLater(new Runnable() {
314: public void run() {
315: _pane.setCaretPosition(len);
316: }
317: });
318: }
319: setCachedCaretPos(newPos);
320: }
321: }
322:
323: /** Sets up the view. */
324: protected void _setupView() {
325: KeyStroke beginLineKey = DrJava.getConfig().getSetting(
326: OptionConstants.KEY_BEGIN_LINE);
327: _pane.addActionForKeyStroke(beginLineKey, gotoPromptPosAction);
328: _pane.addActionForKeyStroke(KeyBindingManager.Singleton
329: .addShiftModifier(beginLineKey),
330: selectToPromptPosAction);
331: KeyStroke endLineKey = DrJava.getConfig().getSetting(
332: OptionConstants.KEY_END_LINE);
333: _pane.addActionForKeyStroke(endLineKey, gotoEndAction);
334: _pane.addActionForKeyStroke(KeyBindingManager.Singleton
335: .addShiftModifier(endLineKey), selectToEndAction);
336:
337: DrJava.getConfig().addOptionListener(
338: OptionConstants.KEY_BEGIN_LINE,
339: new OptionListener<KeyStroke>() {
340: public void optionChanged(OptionEvent<KeyStroke> oe) {
341: _pane.addActionForKeyStroke(oe.value,
342: gotoPromptPosAction);
343: _pane.addActionForKeyStroke(
344: KeyBindingManager.Singleton
345: .addShiftModifier(oe.value),
346: selectToPromptPosAction);
347: }
348: });
349: DrJava.getConfig().addOptionListener(
350: OptionConstants.KEY_END_LINE,
351: new OptionListener<KeyStroke>() {
352: public void optionChanged(OptionEvent<KeyStroke> oe) {
353: _pane.addActionForKeyStroke(oe.value,
354: gotoEndAction);
355: _pane.addActionForKeyStroke(
356: KeyBindingManager.Singleton
357: .addShiftModifier(oe.value),
358: selectToEndAction);
359: }
360: });
361:
362: _pane.addActionForKeyStroke(DrJava.getConfig().getSetting(
363: OptionConstants.KEY_CUT), cutAction);
364: _pane.addActionForKeyStroke(DrJava.getConfig().getSetting(
365: OptionConstants.KEY_COPY), copyAction);
366: DrJava.getConfig().addOptionListener(OptionConstants.KEY_CUT,
367: new OptionListener<KeyStroke>() {
368: public void optionChanged(OptionEvent<KeyStroke> oe) {
369: _pane.addActionForKeyStroke(DrJava.getConfig()
370: .getSetting(OptionConstants.KEY_CUT),
371: cutAction);
372: }
373: });
374: DrJava.getConfig().addOptionListener(OptionConstants.KEY_COPY,
375: new OptionListener<KeyStroke>() {
376: public void optionChanged(OptionEvent<KeyStroke> oe) {
377: _pane.addActionForKeyStroke(DrJava.getConfig()
378: .getSetting(OptionConstants.KEY_COPY),
379: copyAction);
380: }
381: });
382: }
383:
384: /** Clears and resets the view (other than features derived from the model. */
385: public void resetView() {
386: // _pane.resetPrompts(); // NOT USED
387: // System.err.println("Prompts.reset" + "Prompts for pane " + _pane.hashCode() + " is " + _pane.getPromptList());
388: }
389:
390: /** Default cut action. */
391: Action cutAction = new DefaultEditorKit.CutAction() {
392: public void actionPerformed(ActionEvent e) {
393:
394: if (_pane.getSelectedText() != null) {
395: super .actionPerformed(e);
396: String s = edu.rice.cs.util.swing.Utilities
397: .getClipboardSelection(_pane);
398: if (s != null && s.length() != 0) {
399: ClipboardHistoryModel.singleton().put(s);
400: }
401: }
402: }
403: };
404:
405: /** Default copy action. */
406: Action copyAction = new DefaultEditorKit.CopyAction() {
407: public void actionPerformed(ActionEvent e) {
408: if (_pane.getSelectedText() != null) {
409: super .actionPerformed(e);
410: String s = edu.rice.cs.util.swing.Utilities
411: .getClipboardSelection(_pane);
412: if (s != null && s.length() != 0) {
413: ClipboardHistoryModel.singleton().put(s);
414: }
415: }
416: }
417: };
418:
419: /** Accessor method for the InteractionsDJDocument. */
420: public InteractionsDJDocument getDocumentAdapter() {
421: return _adapter;
422: }
423:
424: /** Accessor method for the InteractionsPane. */
425: public InteractionsPane getPane() {
426: return _pane;
427: }
428:
429: /** Determines if the associated console pane is currently computing.
430: * @return true iff the console is busy
431: */
432: protected boolean _busy() {
433: return !getConsoleDoc().hasPrompt();
434: }
435:
436: /** Inserts a new line at the caret position. */
437: AbstractAction newLineAction = new AbstractAction() {
438: public void actionPerformed(ActionEvent e) {
439: ConsoleDocument doc = getConsoleDoc();
440: doc.acquireWriteLock();
441: try {
442: doc.insertNewLine(_pane.getCaretPosition());
443: } finally {
444: doc.releaseWriteLock();
445: }
446: }
447: };
448:
449: /** Removes all text after the prompt. */
450: AbstractAction clearCurrentAction = new AbstractAction() {
451: public void actionPerformed(ActionEvent e) {
452: getConsoleDoc().clearCurrentInput();
453: }
454: };
455:
456: /** Goes to the end of the current input line. */
457: AbstractAction gotoEndAction = new AbstractAction() {
458: public void actionPerformed(ActionEvent e) {
459: moveToEnd();
460: }
461: };
462:
463: /** Selects to the end of the current input line. */
464: AbstractAction selectToEndAction = new AbstractAction() {
465: public void actionPerformed(ActionEvent e) {
466: ConsoleDocument doc = getConsoleDoc();
467: doc.acquireReadLock();
468: try {
469: _pane.moveCaretPosition(doc.getLength());
470: } finally {
471: doc.releaseReadLock();
472: }
473: }
474: };
475:
476: /** Moves the caret to the prompt. */
477: AbstractAction gotoPromptPosAction = new AbstractAction() {
478: public void actionPerformed(ActionEvent e) {
479: moveToPrompt();
480: }
481: };
482:
483: /** Selects to the current prompt. */
484: AbstractAction selectToPromptPosAction = new AbstractAction() {
485: public void actionPerformed(ActionEvent e) {
486: assert EventQueue.isDispatchThread();
487: ConsoleDocument doc = getConsoleDoc();
488: // Selects the text between the old pos and the prompt
489: _pane.moveCaretPosition(doc.getPromptPos());
490: }
491: };
492:
493: /** Moves the pane's caret to the end of the document. Only affects reduced_model not the document model. */
494: void moveToEnd() {
495: assert EventQueue.isDispatchThread();
496: int len = getConsoleDoc().getLength();
497: _pane.setCaretPosition(len);
498: setCachedCaretPos(len);
499: }
500:
501: /** Moves the pane's caret to the document's prompt. Only affects reduced_model not the document model. */
502: void moveToPrompt() {
503: assert EventQueue.isDispatchThread();
504: int pos = getConsoleDoc().getPromptPos();
505: _pane.setCaretPosition(pos);
506: setCachedCaretPos(pos);
507: }
508:
509: // /** Moves the pane's caret to the given position. Assumes that readLock on document is already held. */
510: // private void moveTo(int pos) {
511: // assert EventQueue.isDispatchThread();
512: //// // Sanity check
513: //// if (pos < 0) pos = 0;
514: //// else {
515: //// int maxLen = getConsoleDoc().getLength();
516: //// if (pos > maxLen) pos = maxLen;
517: //// }
518: // _pane.setCaretPosition(pos);
519: // }
520:
521: public void setPrevPaneAction(Action a) {
522: switchToPrevPaneAction = a;
523:
524: // We do this here since switchToPrevPaneAction is set after the constructor is called.
525: _pane.addActionForKeyStroke(DrJava.getConfig().getSetting(
526: OptionConstants.KEY_PREVIOUS_PANE),
527: switchToPrevPaneAction);
528: DrJava.getConfig().addOptionListener(
529: OptionConstants.KEY_PREVIOUS_PANE,
530: new OptionListener<KeyStroke>() {
531: public void optionChanged(OptionEvent<KeyStroke> oe) {
532: _pane
533: .addActionForKeyStroke(
534: DrJava
535: .getConfig()
536: .getSetting(
537: OptionConstants.KEY_PREVIOUS_PANE),
538: switchToPrevPaneAction);
539: }
540: });
541: }
542:
543: public void setNextPaneAction(Action a) {
544: switchToNextPaneAction = a;
545:
546: // We do this here since switchToNextPaneAction is set after the
547: // constructor is called.
548: _pane.addActionForKeyStroke(DrJava.getConfig().getSetting(
549: OptionConstants.KEY_NEXT_PANE), switchToNextPaneAction);
550: DrJava.getConfig().addOptionListener(
551: OptionConstants.KEY_NEXT_PANE,
552: new OptionListener<KeyStroke>() {
553: public void optionChanged(OptionEvent<KeyStroke> oe) {
554: _pane.addActionForKeyStroke(DrJava.getConfig()
555: .getSetting(
556: OptionConstants.KEY_NEXT_PANE),
557: switchToNextPaneAction);
558: }
559: });
560: }
561: }
|