001: /*=============================================================================
002: * Copyright Texas Instruments, Inc., 2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (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
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.swing.console;
020:
021: import ti.exceptions.ProgrammingErrorException;
022:
023: import java.awt.Graphics;
024: import java.awt.Image;
025: import java.awt.Point;
026: import java.awt.Rectangle;
027: import java.util.Iterator;
028: import java.util.LinkedList;
029: import java.util.TreeSet;
030: import java.util.Comparator;
031: import java.util.Vector;
032:
033: /**
034: * The console-buffer is the actual "core" of the console, leaving all the
035: * details of being a swing component (resizing, scrollbar, etc.) to the
036: * public class {@link Console}. All synchronization happens here, for
037: * modifying the state of the buffer, and rendering the buffer.
038: * <p>
039: * This really could be an inner-class, as it is "owned" by the console that
040: * creates it, but that one file would be too big.
041: *
042: * @author Rob Clark
043: * @version 0.0
044: */
045: class ConsoleBuffer implements InputHandler,
046: java.awt.image.ImageObserver, java.io.Serializable {
047: /**
048: * Number of rows.
049: */
050: private int nrows;
051:
052: /**
053: * Number of colums.
054: */
055: private int ncols;
056:
057: /**
058: * The set of rows is a circular buffer, and <code>lastRow</code> is the
059: * index of the last row in the "unwrapped" buffer.
060: */
061: private int lastRow = 0;
062: private Row[] rows;
063:
064: /**
065: * Because the buffer might be locked by the time a repaint event is
066: * handled, but the paintBuffer() method can't just do nothing (or the
067: * console will flicker), when the console is locked we just repaint
068: * the last thing we painted to screen. While the console is locked,
069: * the offscreen isn't touched, so this is safe. But, since the
070: * <code>lastRow</code> might have changed, we need to snapshot it
071: * every time the offscreen is refreshed.
072: * <p>
073: * A repaint() is triggered whenever the buffer is unlock()'d, so
074: * just redrawing the last thing is a safe thing to do.
075: */
076: private int lastRowAtRefresh = 0;
077:
078: private Console console;
079:
080: private static final String EOL = System.getProperty(
081: "line.separator", "\n");
082:
083: /**
084: * Class Constructor.
085: */
086: ConsoleBuffer(Console console, int nrows, int ncols) {
087: this .console = console;
088:
089: this .nrows = nrows;
090: this .ncols = ncols;
091:
092: rows = new Row[nrows];
093: for (int i = 0; i < nrows; i++)
094: rows[i] = new Row(0);
095: }
096:
097: /**
098: * Get the number of rows in the buffer.
099: *
100: * @return number of rows
101: */
102: int getRowCount() {
103: return nrows;
104: }
105:
106: /**
107: * Get the number of columns in the buffer.
108: *
109: * @return number of columns
110: */
111: int getColumnCount() {
112: return ncols;
113: }
114:
115: /**
116: * Set the number of rows in the buffer.
117: *
118: * @param nrows number of rows
119: */
120: void setRowCount(int nrows) {
121: throw new ProgrammingErrorException("unimplemented");
122: }
123:
124: /**
125: * Set the number of columns in the buffer.
126: *
127: * @param ncols number of columns
128: */
129: synchronized void setColumnCount(int ncols) {
130: ncols = Math.max(1, ncols);
131:
132: if (this .ncols != ncols) {
133: this .ncols = ncols;
134:
135: if (offScreen != null)
136: offScreen.flush();
137:
138: offScreen = null;
139:
140: for (int i = 0; i < nrows; i++)
141: rows[i].markDirty();
142: }
143: }
144:
145: /**
146: * Convert the specified row/column coordinate to an offset into the
147: * character stream.
148: *
149: * @param row row coordinate
150: * @param col column coordinate
151: * @return offset into document stream
152: */
153: int toOffset(int row, int col) {
154: int actualRow = (lastRow + 1 + row) % nrows;
155:
156: if ((actualRow < 0) || (actualRow >= rows.length))
157: return -1;
158:
159: int actualCol = Math.min(col, rows[actualRow].getLength());
160:
161: return rows[actualRow].getOffset() + actualCol;
162: }
163:
164: /**
165: * Convert the specified offset into a row/column coordinate. The
166: * x value of the returned point is the column, and the y value is
167: * the row.
168: *
169: * @param offset the offset into document stream
170: * @return the row/col coordinate
171: */
172: Point toPoint(int offset) {
173: // deal with offset >= end of character stream:
174: if (offset >= (rows[lastRow].getOffset() + rows[lastRow]
175: .getLength()))
176: return new Point(rows[lastRow].getLength(), nrows - 1);
177:
178: // start search from bottom of document, because this is used to
179: // determine cursor position, and the cursor is usually (always?)
180: // near the end of the document:
181:
182: for (int i = 0; i < nrows; i++) {
183: int idx = (lastRow - i + nrows) % nrows;
184:
185: int rowOffset = rows[idx].getOffset();
186: int rowLength = rows[idx].getLength();
187:
188: if ((offset >= rowOffset)
189: && (offset < (rowOffset + rowLength))) {
190: int row = nrows - i - 1;
191: int col = offset - rowOffset;
192:
193: return new Point(col, row);
194: }
195: }
196:
197: // offset must have scrolled off the top:
198: return new Point(0, 0);
199: }
200:
201: /**
202: * Append characters to the end of the character stream.
203: *
204: * @param cbuf the character buffer
205: * @param off the offset into cbuf to first character to append
206: * @param len the number of characters to append
207: */
208: public synchronized void append(char[] cbuf, int off, int len) {
209: if ((off + len) > cbuf.length)
210: throw new ProgrammingErrorException("bad args");
211:
212: int consumed = 0;
213:
214: while (consumed < len) {
215: boolean lf = false;
216: boolean wrap = false;
217:
218: int n = ncols - rows[lastRow].getLength(); // max chars to fit in row
219:
220: // n could be negative if ncols is changed
221: n = Math.max(0, Math.min(n, len - consumed));
222:
223: for (int i = 0; i < n; i++) {
224: if (cbuf[off + consumed + i] == Console.LF) {
225: n = i;
226: lf = true;
227: break;
228: }
229: }
230:
231: rows[lastRow].append(cbuf, off + consumed, n);
232: consumed += n;
233:
234: if (rows[lastRow].getLength() >= ncols)
235: wrap = true;
236:
237: if (lf || wrap) {
238: // if LF, consume '\n' char
239: if (lf)
240: consumed++;
241:
242: int offset = (rows[lastRow].getOffset() + rows[lastRow]
243: .getLength());
244:
245: lastRow = (lastRow + 1) % nrows;
246: rows[lastRow] = new Row(offset);
247:
248: // remove regions where (region.end < rows[firstRow].getOffset())
249: int firstRow = (lastRow + 1) % nrows;
250: int firstOffset = rows[firstRow].getOffset();
251:
252: Vector regionsToRemove = null; // we can't remove while iterating
253: Iterator itr = pointSet.iterator();
254: while (itr.hasNext()) {
255: RegionPoint rp = (RegionPoint) (itr.next());
256:
257: if (rp.getOffset() >= firstOffset)
258: break;
259:
260: if (rp instanceof EndRegionPoint) {
261: if (regionsToRemove == null)
262: regionsToRemove = new Vector();
263: regionsToRemove.addElement(rp.getRegion());
264: }
265: }
266:
267: if (regionsToRemove != null)
268: for (int i = 0; i < regionsToRemove.size(); i++)
269: removeRegionImpl((Region) (regionsToRemove
270: .elementAt(i)));
271: }
272: }
273:
274: refresh();
275: }
276:
277: /**
278: * Delete characters from end of character stream.
279: *
280: * @param num the number of characters to delete
281: */
282: public synchronized void zap(int num) {
283: // used to avoid infinite loop in case of zap'ing too many chars:
284: int watchdog = nrows;
285:
286: while ((num > 0) && (watchdog > 0)) {
287: int n = Math.min(num, rows[lastRow].getLength());
288:
289: rows[lastRow].zap(n);
290:
291: if (rows[lastRow].getLength() == 0)
292: lastRow = (lastRow + nrows - 1) % nrows;
293:
294: num -= n;
295:
296: watchdog--;
297: }
298:
299: refresh();
300: }
301:
302: /**
303: * Get the current offset of the last character in the character stream.
304: *
305: * @return an offset
306: */
307: public synchronized int getOffset() {
308: return rows[lastRow].getOffset() + rows[lastRow].getLength();
309: }
310:
311: /**
312: * Get the data within the specified region. If the requested region has
313: * scrolled past the top of the buffer, the returned data may be truncated.
314: *
315: * @param offset the begining of the range
316: * @param len the length of the range in characters
317: * @return the data
318: */
319: public synchronized char[] getData(int offset, int len) {
320: // offset and len may be invalid, so fix that first: ///////
321: {
322: int beg = rows[((2 * lastRow) + 1) % nrows].getOffset();
323: int end = getOffset();
324: offset = Math.max(offset, beg);
325: len = Math.min(len, end - beg);
326: }
327: //////////////////////////////////////////////////////////////
328:
329: int end = offset + len;
330:
331: StringBuffer sb = new StringBuffer(len);
332:
333: for (int i = 0; i < nrows; i++) {
334: int idx = (lastRow + i + 1) % nrows;
335:
336: int rowOffset = rows[idx].getOffset();
337: int rowLength = rows[idx].getLength();
338: int rowEnd = rowOffset + rowLength;
339:
340: if ((offset < rowEnd) && (end > rowOffset)) {
341: String str = rows[idx].getData();
342:
343: int a = Math.max(offset, rowOffset);
344: int b = Math.min(end, rowEnd);
345:
346: sb.append(str.toCharArray(), a - rowOffset, b - a);
347:
348: if (end > rowEnd)
349: sb.append(EOL);
350: }
351: }
352:
353: return sb.toString().toCharArray();
354: }
355:
356: /**
357: * Close method
358: */
359: public void close() {
360: /* no-op */
361: }
362:
363: /*=======================================================================*/
364: // sorted list of points is walked thru as we refresh()
365: private TreeSet pointSet = new TreeSet(new RegionPointComparator());
366:
367: private static class RegionPointComparator implements Comparator,
368: java.io.Serializable {
369: public int compare(Object o1, Object o2) {
370: return ((RegionPoint) o1).compareTo((RegionPoint) o2);
371: }
372: }
373:
374: /**
375: * Add a region mapped over a section of character stream. If the section
376: * of the character stream over which the region is mapped has scrolled off
377: * the top of the fixed size row buffer, the region will be automatically
378: * removed.
379: *
380: * @param r region to add
381: * @see #removeRegion
382: */
383: public synchronized void addRegion(Region r) {
384: if (r.getStart() < r.getEnd()) {
385: pointSet.add(new StartRegionPoint(r));
386: pointSet.add(new EndRegionPoint(r));
387: markDirty(r.getStart(), r.getEnd());
388: refresh();
389: }
390: }
391:
392: /**
393: * Remove a region.
394: *
395: * @param r region to remove
396: * @see #addRegion
397: */
398: public synchronized void removeRegion(Region r) {
399: if (r.getStart() < r.getEnd()) {
400: removeRegionImpl(r);
401: markDirty(r.getStart(), r.getEnd());
402: refresh();
403: }
404: }
405:
406: private final void removeRegionImpl(Region r) // remove region, but does not trigger
407: { // a refresh(), or markDirty()
408: pointSet.remove(new StartRegionPoint(r));
409: pointSet.remove(new EndRegionPoint(r));
410: }
411:
412: /**
413: * Given a <code>start</code> and <code>end</code> offsets, mark the rows
414: * that contain parts of the specified region as needing to be rerendered.
415: */
416: private final void markDirty(int start, int end) {
417: // the common case is that the region being added/removed is at or near
418: // the end of the buffer, so work backwards from the end of the buffer,
419: // and bail out once we are past the start of the specified region
420: for (int i = 0; i < nrows; i++) {
421: int idx = (lastRow + nrows - i) % nrows;
422: int rowStart = rows[idx].getOffset();
423: int rowEnd = rowStart + rows[idx].getLength();
424:
425: if ((start <= rowEnd) && (end >= rowStart))
426: rows[idx].markDirty();
427:
428: if (rowStart <= start)
429: break;
430: }
431: }
432:
433: /**
434: * Get an object on which to synchronize access to the buffer
435: *
436: * @return an object suitable for synchronizing buffer access
437: * @see #getRegions
438: */
439: public Object getBufferLock() {
440: return this ;
441: }
442:
443: /**
444: * Get an iterator of the regions containing the specified range. Access
445: * to the iterator must be synchronized on the buffer-lock, to prevent
446: * concurrent modification problems. For example:
447: * <pre>
448: * synchronized( ih.getBufferLock() )
449: * {
450: * for( Iterator itr=ih.getRegions( off, len ); itr.hasNext(); )
451: * {
452: * ...
453: * }
454: * }
455: * </pre>
456: *
457: * @param offset the begining of the range
458: * @param len the length of the range in characters
459: * @return an iterator of {@link Region}
460: * @see #getBufferLock
461: */
462: public Iterator getRegions(int offset, int len) {
463: return new RegionIterator(pointSet.iterator(), offset, len);
464: }
465:
466: private static class RegionIterator implements Iterator {
467: private Iterator itr;
468: private int start;
469: private int end;
470: private Object next;
471:
472: RegionIterator(Iterator itr, int offset, int len) {
473: this .itr = itr;
474: this .start = offset;
475: this .end = offset + len;
476: update();
477: }
478:
479: public boolean hasNext() {
480: return next != null;
481: }
482:
483: public Object next() {
484: if (next == null)
485: throw new java.util.NoSuchElementException();
486: Object obj = next;
487: update();
488: return obj;
489: }
490:
491: public void remove() {
492: throw new UnsupportedOperationException("remove");
493: }
494:
495: private void update() {
496: next = null;
497: while (itr.hasNext()) {
498: RegionPoint rp = (RegionPoint) (itr.next());
499:
500: // since there is both a StartRegionPoint and EndRegionPoint for
501: // each region, but we only want to see each Region once in the
502: // output, ignore the EndRegionPoint-s
503: if (rp instanceof StartRegionPoint) {
504: Region r = rp.getRegion();
505:
506: if ((r.getStart() <= end) && (r.getEnd() >= start)) {
507: next = r;
508: break;
509: }
510: }
511: }
512: }
513: }
514:
515: /*=======================================================================*/
516: private int lockCnt = 0;
517:
518: /**
519: * Lock the console from repaints. This can be used to batch multiple
520: * updates ({@link #append}, {@link #zap}, {@link #addRegion},
521: * {@link #removeRegion}) and only trigger a single repaint at the end.
522: *
523: * @see #unlock
524: */
525: public synchronized void lock() {
526: lockCnt++;
527: }
528:
529: /**
530: * Unlock the console, rerendering if needed.
531: *
532: * @see #lock
533: */
534: public synchronized void unlock() {
535: if (--lockCnt == 0)
536: refresh();
537: }
538:
539: /**
540: * Is the console locked from rerendering?
541: *
542: * @return <code>true</code> if rerendering shouldn't happen yet
543: */
544: final boolean locked() {
545: return lockCnt > 0;
546: }
547:
548: /*=======================================================================*/
549: private transient Image offScreen = null;
550:
551: /**
552: * If not locked, trigger a repaint.
553: */
554: private final void refresh() {
555: if (!locked()) {
556: javax.swing.SwingUtilities.invokeLater(new Runnable() {
557: public void run() {
558: console.repaint();
559: }
560: });
561: }
562: }
563:
564: /**
565: * The actual rerender of the offscreen buffer is done here... this should
566: * only be called directly from paintBuffer(), and only if the buffer is
567: * not locked(). Other code should call {@link #refresh}.
568: */
569: private void refreshOffscreenBuffer() {
570: ConsoleGraphics g = new ConsoleGraphics(offScreen.getGraphics());
571:
572: g.setFont(console.getFont());
573:
574: int fontHeight = console.getRowHeight();
575: int fontWidth = console.getColumnWidth();
576:
577: Iterator itr = pointSet.iterator();
578: RegionPoint p = null;
579: int pOffset = 0;
580:
581: for (int i = 0; i < nrows; i++) {
582: int rowNum = (lastRow + 1 + i) % nrows;
583: Row row = rows[rowNum];
584: String str = row.getData();
585:
586: int x = 0;
587: int y = rowNum * fontHeight;
588:
589: int offsetIntoRow = 0;
590: int rowOffset = row.getOffset();
591: int rowLength = row.getLength();
592:
593: // loop to handle all "segments" within a row:
594: while (offsetIntoRow < rowLength) {
595: if (p == null) {
596: if (itr.hasNext()) {
597: p = (RegionPoint) (itr.next());
598: pOffset = p.getOffset();
599: } else {
600: pOffset = Integer.MAX_VALUE;
601: }
602: }
603:
604: if (pOffset <= (rowOffset + offsetIntoRow)) {
605: p.handle(g);
606: p = null;
607: continue;
608: }
609:
610: // number of chars to draw:
611: int n = Math.min(rowLength, pOffset - rowOffset)
612: - offsetIntoRow;
613:
614: if (row.needsRedraw())
615: g.drawString(str.substring(offsetIntoRow,
616: offsetIntoRow + n), x, y);
617:
618: offsetIntoRow += n;
619: x += fontWidth * n;
620: }
621:
622: // handle special case of region ending at last char in row:
623: if ((p != null) && (pOffset == (rowOffset + offsetIntoRow))) {
624: p.handle(g);
625: p = null;
626: }
627:
628: if (row.needsRedraw()) {
629: g.clearRect(x, y,
630: (fontWidth * (ncols - offsetIntoRow)),
631: fontHeight);
632:
633: row.markClean();
634: }
635: }
636:
637: if (p != null)
638: p.handle(g);
639:
640: while (itr.hasNext())
641: ((RegionPoint) (itr.next())).handle(g);
642: }
643:
644: /**
645: * Render the buffer into the provided graphics, starting at the origin.
646: */
647: synchronized void paintBuffer(Graphics g) {
648: int fontHeight = console.getRowHeight();
649: int fontWidth = console.getColumnWidth();
650: int width = (fontWidth * ncols);
651: int height = (fontHeight * nrows);
652:
653: if (offScreen == null)
654: if ((width > 0) && (height > 0))
655: offScreen = console.createImage(width, height);
656:
657: /* blit the offScreen in two parts, because it is a circular buffer... the
658: * top half of the on-screen image starts potentially somewhere other than
659: * the top of the off-screen, so we have to swap the two halves.
660: */
661: if (offScreen != null) {
662: Rectangle clipRect = g.getClipBounds();
663:
664: if (!locked()) {
665: int firstRow = (lastRow + 1) % nrows;
666: int y1 = clipRect.y;
667: int y2 = y1 + clipRect.height;
668:
669: for (int i = 0; i < nrows; i++) {
670: int rowNum = (firstRow + i) % nrows;
671: int y = i * fontHeight;
672: rows[rowNum].setVisible((y1 <= (y + fontHeight))
673: && (y <= y2));
674: }
675:
676: refreshOffscreenBuffer();
677: lastRowAtRefresh = lastRow;
678: }
679:
680: // top vs. bottom refer to on-screen:
681: int trow = (lastRowAtRefresh + 1) % nrows;
682: int th = (nrows - trow) * fontHeight; // height of top section
683: int bh = trow * fontHeight; // height of bottom section
684:
685: g.clearRect(clipRect.x, clipRect.y, clipRect.width,
686: clipRect.height);
687:
688: Image img;
689: // if( console.isEnabled() )
690: img = offScreen;
691: // else // XXX for some reason the GrayFilter is causing a NPE deep within drawImage()
692: // img = javax.swing.GrayFilter.createDisabledImage(offScreen);
693:
694: // draw top half:
695: g.drawImage(img, 0, 0, 0 + width, 0 + th, 0, bh, 0 + width,
696: bh + th, this );
697:
698: // draw bottom half:
699: g.drawImage(img, 0, th, 0 + width, th + bh, 0, 0,
700: 0 + width, 0 + bh, this );
701: }
702: }
703:
704: // called when image changes... we just ignore it..
705: public boolean imageUpdate(Image img, int infoflags, int x, int y,
706: int width, int height) {
707: return false;
708: }
709: }
710:
711: /**
712: * Kinda weird to extend number, but thats what NumericalSortingVisitor
713: * wants... represents the start of end of a region. FWIW, two regions
714: * are equal (in the .equals() sense) iff the offsets are the same, and
715: * the regions are the same.
716: */
717: abstract class RegionPoint {
718: protected int offset;
719: protected Region r;
720: protected double adj; // a hack to make EndRegionPoint go before StartRegionPoint
721:
722: protected RegionPoint(int offset, Region r, double adj) {
723: this .offset = offset;
724: this .r = r;
725: this .adj = adj;
726: }
727:
728: public int hashCode() {
729: return getOffset();
730: }
731:
732: public boolean equals(Object obj) {
733: return ((obj instanceof RegionPoint)
734: && (((RegionPoint) obj).offset == offset) && ((RegionPoint) obj).r
735: .equals(r));
736: }
737:
738: public int compareTo(RegionPoint rp) {
739: // scale up difference to prevent truncation when cast to int...
740: // (remember adj could be 0.1)
741: int rc = (int) ((doubleValue() - rp.doubleValue()) * 10.0);
742:
743: // if offset is the same, defer to the region's count, which
744: // is a unique sequence number for each region. This ensures
745: // that there are never two points that are equal (unless, of
746: // course, they are end points of the same region, in which
747: // case rc != 0 because the region length >= 1)
748: if (rc == 0)
749: rc = r.getCount() - rp.r.getCount();
750:
751: return rc;
752: }
753:
754: public Region getRegion() {
755: return r;
756: }
757:
758: public int getOffset() {
759: return offset;
760: }
761:
762: public final double doubleValue() {
763: return (double) getOffset() + adj;
764: }
765:
766: public abstract void handle(ConsoleGraphics g);
767:
768: public String toString() {
769: return "[" + getClass().getName() + ": region=" + getRegion()
770: + ", offset=" + getOffset() + "]";
771: }
772: }
773:
774: class StartRegionPoint extends RegionPoint {
775: StartRegionPoint(Region r) {
776: super (r.getStart(), r, 0.1);
777: }
778:
779: public void handle(ConsoleGraphics g) {
780: r.enter(g);
781: }
782: }
783:
784: class EndRegionPoint extends RegionPoint {
785: EndRegionPoint(Region r) {
786: super (r.getEnd(), r, 0.0);
787: }
788:
789: public void handle(ConsoleGraphics g) {
790: r.leave(g);
791: }
792: }
793:
794: /*
795: * Local Variables:
796: * tab-width: 2
797: * indent-tabs-mode: nil
798: * mode: java
799: * c-indentation-style: java
800: * c-basic-offset: 2
801: * eval: (c-set-offset 'substatement-open '0)
802: * eval: (c-set-offset 'case-label '+)
803: * eval: (c-set-offset 'inclass '+)
804: * eval: (c-set-offset 'inline-open '0)
805: * End:
806: */
|