001: /*=============================================================================
002: * Copyright Texas Instruments 2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2 of the License, or (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: *
018: * $ProjectHeader: OSCRIPT 0.155 Fri, 20 Dec 2002 18:34:22 -0800 rclark $
019: */
020:
021: package oscript.swing.text;
022:
023: import oscript.data.*;
024: import oscript.util.SymbolMap;
025: import oscript.syntaxtree.NodeToken;
026:
027: import java.util.*;
028: import java.awt.Graphics;
029: import java.awt.Color;
030: import java.awt.Font;
031: import javax.swing.text.*;
032: import javax.swing.event.*;
033: import javax.swing.undo.CompoundEdit;
034:
035: /**
036: * An editor-kit for an ODE src code editor. The document used is just
037: * a plain-old {@link PlainDocument}. Rather than trying to integrate the
038: * parser with the {@link Document}'s {@link Element} structure, the parsed
039: * representation of the document is stored seperately as an array of tokens.
040: * We create a {@link View} for rendering the document that takes into account
041: * the token that overlays a section of text, and the {@link AttributeSet}
042: * which that token maps to, as it renders the text.
043: * <p>
044: * For performance reasons, parsing happens asynchronously, which means that
045: * whenever the document is mutated. So, when there is an insert, we update
046: * the (now out of date) tokens by increasing the end-offset of the current
047: * token, and the begin-offset and end-offset of all subsequent tokens by the
048: * number of characters inserted. When there is a remove, the process is
049: * essentially the reverse.
050: * <p>
051: * The script side of things handles creating the parsing thread, and it uses
052: * {@link DocumentListener}s to determine when reparsing is needed.
053: *
054: * @author Rob Clark (rob@ti.com)
055: * <!--$Format: " * @version $Revision$"$-->
056: * @version 1.10
057: * @see #createDefaultDocument
058: */
059: public class ODEEditorKit extends DefaultEditorKit {
060: private SymbolMap attrSetTable;
061: private NodeToken[] nodeTokens = new NodeToken[0];
062:
063: /**
064: * Class Constructor.
065: */
066: public ODEEditorKit() {
067: attrSetTable = new SymbolMap();
068: }
069:
070: /**
071: * Set the attribute set table.
072: *
073: * @param attrSetTable the table that maps NodeToken.kind to an attribute
074: * set. This table is created and maintained from script code, but
075: * used here while rendering the text.
076: */
077: public void setAttributeSetTable(SymbolMap attrSetTable) {
078: this .attrSetTable = attrSetTable;
079: }
080:
081: /**
082: * Called from script code after parsing is completed. For performance
083: * reasons the tokens are stored here, after they are generated by script.
084: */
085: public synchronized void setNodeTokens(Vector v) {
086: NodeToken[] newNodeTokens = new NodeToken[v.size()];
087: v.copyInto(newNodeTokens);
088: nodeTokens = newNodeTokens;
089: }
090:
091: /**
092: * Get a factory for producing {@link View}s for rendering {@link Document}s
093: * created by this editor-kit.
094: *
095: * @return the view-factory
096: */
097: public ViewFactory getViewFactory() {
098: return new ViewFactory() {
099: public View create(Element elem) {
100: return new NodeTokenView(elem);
101: }
102: };
103: }
104:
105: /**
106: * Create a uninitialized document. The {@link Document} used by this editor
107: * kit is basically just a {@link PlainDocument} that has been extended to
108: * synchronously update the offsets of the tokens in response to document
109: * mutations (insert/remove). This is important to ensure that the token's
110: * offset maps to sensible positions in the document during the period between
111: * when the document is edited, and when the parser has finished re-parsing it
112: *
113: * @return an unitialized document
114: */
115: public Document createDefaultDocument() {
116: return new ODEDocument();
117: }
118:
119: public class ODEDocument extends PlainDocument {
120: private long lastMutateTime;
121:
122: public long getLastMutateTime() {
123: return lastMutateTime;
124: }
125:
126: private int undoableSequenceLevel = 0;
127: private CompoundEdit undoableSequence = null;
128:
129: /**
130: * All document updates performed by the runnable result in a single
131: * undo event. This call gate provides a way to coalesce multiple
132: * insert/remove mutates into a single undoable event.
133: */
134: public void performUndoableSequence(Runnable r) {
135: synchronized (ODEEditorKit.this ) {
136: undoableSequenceLevel++;
137: r.run();
138: undoableSequenceLevel--;
139:
140: if ((undoableSequenceLevel == 0)
141: && (undoableSequence != null)) {
142: undoableSequence.end();
143: super .fireUndoableEditUpdate(new UndoableEditEvent(
144: this , undoableSequence));
145: undoableSequence = null;
146: }
147: }
148: }
149:
150: private CompoundEdit getUndoableSequence() {
151: if (undoableSequence == null) {
152: undoableSequence = new CompoundEdit();
153: // super.fireUndoableEditUpdate( new UndoableEditEvent( this, undoableSequence ) );
154: }
155: return undoableSequence;
156: }
157:
158: protected void fireUndoableEditUpdate(UndoableEditEvent evt) {
159: synchronized (ODEEditorKit.this ) {
160: if (undoableSequenceLevel > 0)
161: getUndoableSequence().addEdit(evt.getEdit());
162: else
163: super .fireUndoableEditUpdate(evt);
164: }
165: }
166:
167: /**
168: * Get a reader that returns the contents of this document
169: */
170: public java.io.Reader getDocumentReader() {
171: return new java.io.Reader() {
172:
173: private int idx = 0;
174:
175: public int read(char[] cbuf, int off, int len)
176: throws java.io.IOException {
177: try {
178: len = Math.min(len, getLength() - idx);
179: len = Math.min(len, 128); // minimize size of actual read to lessen impact of crossing gap
180: System.arraycopy(getText(idx, len)
181: .toCharArray(), 0, cbuf, off, len);
182: idx += len;
183:
184: if (len == 0)
185: return -1;
186:
187: return len;
188: } catch (BadLocationException e) {
189: throw new java.io.IOException("bad location: "
190: + e.getMessage());
191: }
192: }
193:
194: public void close() {
195: }
196:
197: };
198: }
199:
200: /* Possibly could do something clever here, and queue up "deltas",
201: * and not process them until the next time getToken() is called
202: * (possibly only if off > queued_offs[0])
203: */
204:
205: public void remove(int off, int len)
206: throws BadLocationException {
207: synchronized (ODEEditorKit.this ) {
208: lastMutateTime = System.currentTimeMillis();
209:
210: // deal with bogus removes here... it is kind of a hack, but
211: // easier than dealing with it at all the places that call
212: // remove!
213: off = Math.max(off, 0);
214: len = Math.min(off + len, getLength()) - off;
215:
216: updateRemove(off, len);
217: super .remove(off, len);
218: }
219: }
220:
221: public void insertString(int off, String str, AttributeSet a)
222: throws BadLocationException {
223: synchronized (ODEEditorKit.this ) {
224: lastMutateTime = System.currentTimeMillis();
225:
226: updateInsert(off, str.length());
227: super .insertString(off, str, a);
228: }
229: }
230: }
231:
232: private synchronized void updateInsert(int off, int len) {
233: NodeToken nt = getToken(off);
234: if (nt != null) {
235: // kinda weird how we do this, but this way we don't get gaps between
236: // adjacent node-tokens as we iterate which might screw up getToken()
237: LinkedList tokenList = new LinkedList();
238: tokenList.add(nt);
239: while ((nt = getToken(nt.endOffset + 1)) != null)
240: tokenList.add(nt);
241:
242: Iterator itr = tokenList.iterator();
243: ((NodeToken) (itr.next())).endOffset += len;
244:
245: while (itr.hasNext()) {
246: nt = (NodeToken) (itr.next());
247: nt.beginOffset += len;
248: nt.endOffset += len;
249: }
250: }
251: }
252:
253: private final synchronized void updateRemove(int off, int len) {
254: NodeToken nt = getToken(off - len);
255: if (nt != null) {
256: int totalSub = 0;
257:
258: // kinda weird how we do this, but this way we don't get gaps between
259: // adjacent node-tokens as we iterate which might screw up getToken()
260: LinkedList tokenList = new LinkedList();
261: tokenList.add(nt);
262: while ((nt = getToken(nt.endOffset + 1)) != null)
263: tokenList.add(nt);
264:
265: for (Iterator itr = tokenList.iterator(); itr.hasNext();) {
266: nt = (NodeToken) (itr.next());
267:
268: // don't want to move endOffset past current offset, so
269: // figure out how much to subtract, and roll-over the
270: // rest:
271: int sub = len - Math.max(0, off + len - nt.endOffset);
272: len -= sub;
273: off += sub;
274:
275: nt.beginOffset -= totalSub;
276: totalSub += sub;
277: nt.endOffset -= totalSub;
278: }
279: }
280: }
281:
282: /**
283: * The {@link View} implementation. Basically just a {@link PlainView} that
284: * changes the attribute-set for different node tokens within the view as it
285: * renders.
286: */
287: private class NodeTokenView extends PlainView {
288: private Segment text = new Segment();
289:
290: /**
291: * Class Constructor.
292: */
293: NodeTokenView(Element elem) {
294: super (elem);
295: }
296:
297: /**
298: * Renders the given range in the model as normal unselected
299: * text. Uses the foreground or disabled color to render the text.
300: *
301: * @param g the graphics context
302: * @param x the starting X coordinate >= 0
303: * @param y the starting Y coordinate >= 0
304: * @param p0 the beginning position in the model >= 0
305: * @param p1 the ending position in the model >= 0
306: * @returns the X location of the end of the range >= 0
307: * @exception BadLocationException if the range is invalid
308: */
309: protected synchronized int drawUnselectedText(Graphics g,
310: int x, int y, int p0, int p1)
311: throws BadLocationException {
312: if ((nodeTokens == null) || (nodeTokens.length == 0))
313: return super .drawUnselectedText(g, x, y, p0, p1);
314:
315: Document doc = getDocument();
316: int p = p0;
317: int kind = -1;
318:
319: while (p < p1) {
320: NodeToken nt = getToken(p);
321: int mark;
322:
323: if (nt == null) {
324: setKind(g, -1);
325: mark = p1;
326: } else {
327: if (kind != nt.kind)
328: setKind(g, kind = nt.kind);
329: mark = Math.min(nt.endOffset + 1, p1);
330: }
331:
332: if (mark > p) {
333: doc.getText(p, mark - p, text);
334: x = Utilities.drawTabbedText(text, x, y, g, this ,
335: mark);
336: }
337:
338: p = mark;
339: }
340:
341: return x;
342: }
343:
344: private void setKind(Graphics g, int kind) {
345: if (kind >= oscript.util.SymbolTable.MIN_SYMBOL_ID) {
346: AttributeSet attrSet = (AttributeSet) (attrSetTable
347: .get(kind));
348:
349: // other attributes? how to handle background color?
350: g.setColor(getFgColor(attrSet));
351: g.setFont(getFont(g.getFont(), attrSet));
352: } else {
353: g.setColor(Color.black);
354: }
355: }
356: }
357:
358: private static Color defaultFgColor = Color.black;
359:
360: public static void setDefaultFgColor(Color c) {
361: defaultFgColor = c;
362: }
363:
364: /**
365: * Given an attribute set, get the foreground color
366: */
367: private static Color getFgColor(AttributeSet attrSet) {
368: Color c = null;
369:
370: if (attrSet != null)
371: c = (Color) (attrSet
372: .getAttribute(StyleConstants.Foreground));
373:
374: if (c == null)
375: c = defaultFgColor;
376:
377: return c;
378: }
379:
380: /**
381: * All editor-kits share a common font cache, keyed by an instance of
382: * {@link FontCacheKey}.
383: */
384: private static Hashtable fontCache = new Hashtable();
385:
386: private static class FontCacheKey {
387: private String name;
388: private int style;
389: private int size;
390: private int hash;
391:
392: FontCacheKey(String name, int style, int size) {
393: this .name = name;
394: this .style = style;
395: this .size = size;
396:
397: hash = name.hashCode() ^ ((style << 16) | size);
398: }
399:
400: public int hashCode() {
401: return hash;
402: }
403:
404: public boolean equals(Object obj) {
405: return (obj instanceof FontCacheKey)
406: && (((FontCacheKey) obj).hash == hash)
407: && ((FontCacheKey) obj).name.equals(name)
408: && (((FontCacheKey) obj).style == style)
409: && (((FontCacheKey) obj).size == size);
410: }
411: }
412:
413: /**
414: * Given an attribute set, get the font. For performance reasons,
415: * this avoids re-instantiating fonts, and caches fonts keys by
416: * < name, style size >. (Use of Font#deriveFont() would be
417: * easier, but is buggy on early versions of java v1.4 on macosx)
418: */
419: private static Font getFont(Font origFont, AttributeSet attrSet) {
420: int style = Font.PLAIN;
421:
422: if (attrSet != null) {
423: Integer val = (Integer) (attrSet.getAttribute("style"));
424: if (val != null)
425: style = val.intValue();
426: }
427:
428: Object key = new FontCacheKey(origFont.getName(), style,
429: origFont.getSize());
430: Font font = (Font) (fontCache.get(key));
431:
432: if (font == null) {
433: font = new Font(origFont.getName(), style, origFont
434: .getSize());
435: fontCache.put(key, font);
436: }
437:
438: return font;
439: }
440:
441: // XXX others... getBgColor, getFont, etc
442:
443: /*
444: * This can be cleaned up significantly with something that produces an
445: * Iterator interface.
446: */
447:
448: /**
449: * Given an offset into the document, find the corresponding token.
450: * This is the API used by everything else for accessing the tokens.
451: * The remaining methods are just used to implement this method.
452: *
453: * @param off the offset into the document
454: */
455: public synchronized NodeToken getToken(int off) {
456: // check the current and next token first, otherwise revert to search:
457: if ((0 <= idx) && (idx < nodeTokens.length)) {
458: if (off >= nodeTokens[idx].getActualBeginOffset()) {
459: if (off <= nodeTokens[idx].endOffset)
460: return getActualToken(nodeTokens[idx], off);
461: else if ((++idx < nodeTokens.length)
462: && (off <= nodeTokens[idx].endOffset))
463: return getActualToken(nodeTokens[idx], off);
464: }
465: }
466:
467: idx = findTokenIndex(off);
468:
469: if ((0 <= idx) && (idx < nodeTokens.length))
470: return getActualToken(nodeTokens[idx], off);
471: return null;
472: }
473:
474: private int idx = 0; // cache last access, to speed next one
475:
476: // given a regular token, and off, determine the actual token (ie.
477: // regular or special)
478: private static NodeToken getActualToken(NodeToken node, int off) {
479: for (int i = 0; i < node.numSpecials(); i++)
480: if (node.getSpecialAt(i).endOffset >= off)
481: return node.getSpecialAt(i);
482: if (node.endOffset >= off)
483: return node;
484: return null;
485: }
486:
487: // find index of regular token containing off
488: private int findTokenIndex(int off) {
489: int a = 0;
490: int b = nodeTokens.length;
491: int idx = 0;
492:
493: while (a < b) {
494: idx = (a + b) / 2;
495:
496: // there can be gaps between tokens, so we need to use the
497: // end-offset of the prev token:
498: int bo = (idx > 0) ? nodeTokens[idx - 1].endOffset : 0;
499: int eo = nodeTokens[idx].endOffset;
500:
501: if (off < bo)
502: b = idx;
503: else if (off > eo)
504: a = idx + 1;
505: else
506: break;
507: }
508:
509: return idx;
510: }
511:
512: // for debug
513: public String offsetToNodeToken(int off) {
514:
515: NodeToken nt = getToken(off);
516: return "{image=\"" + nt + "\", kind="
517: + oscript.parser.OscriptParser.getTokenString(nt.kind)
518: + "}";
519: }
520: }
521:
522: /*
523: * Local Variables:
524: * tab-width: 2
525: * indent-tabs-mode: nil
526: * mode: java
527: * c-indentation-style: java
528: * c-basic-offset: 2
529: * eval: (c-set-offset 'substatement-open '0)
530: * eval: (c-set-offset 'case-label '+)
531: * eval: (c-set-offset 'inclass '+)
532: * eval: (c-set-offset 'inline-open '0)
533: * End:
534: */
|