001: /*
002: * TextAreaPainter.java - Paints the text area
003: * Copyright (C) 1999 Slava Pestov
004: *
005: * You may use and modify this package for any purpose. Redistribution is
006: * permitted, in both source and binary form, provided that this notice
007: * remains intact in all source distributions of this package.
008: */
009: package workbench.gui.editor;
010:
011: import java.awt.Color;
012: import java.awt.Cursor;
013: import java.awt.Font;
014: import java.awt.FontMetrics;
015: import java.awt.Graphics;
016: import java.awt.Rectangle;
017: import java.beans.PropertyChangeEvent;
018: import java.beans.PropertyChangeListener;
019:
020: import javax.swing.JComponent;
021: import javax.swing.text.PlainDocument;
022: import javax.swing.text.Segment;
023: import javax.swing.text.TabExpander;
024: import javax.swing.text.Utilities;
025: import workbench.resource.Settings;
026: import workbench.util.NumberStringCache;
027: import workbench.util.StringUtil;
028:
029: /**
030: * The text area repaint manager. It performs double buffering and paints
031: * lines of text.
032: * @author Slava Pestov
033: */
034: public class TextAreaPainter extends JComponent implements TabExpander,
035: PropertyChangeListener {
036: int currentLineIndex;
037: Token currentLineTokens;
038: Segment currentLine;
039:
040: // protected members
041: protected JEditTextArea textArea;
042: protected SyntaxStyle[] styles;
043: protected Color caretColor;
044: protected Color selectionColor;
045: protected Color bracketHighlightColor;
046: protected Color errorColor;
047: protected Color currentLineColor;
048:
049: protected boolean bracketHighlight;
050:
051: protected int tabSize = -1;
052: protected FontMetrics fm;
053:
054: protected boolean showLineNumbers = false;
055: protected int gutterWidth = 0;
056: protected int gutterCharWidth = 0;
057:
058: protected static final int GUTTER_MARGIN = 2;
059: private static final Color GUTTER_BACKGROUND = new Color(238, 240,
060: 238);
061: private static final Color GUTTER_COLOR = Color.DARK_GRAY;
062:
063: public TextAreaPainter(JEditTextArea textArea) {
064: this .textArea = textArea;
065:
066: setDoubleBuffered(true);
067: setOpaque(true);
068:
069: currentLine = new Segment();
070: currentLineIndex = -1;
071:
072: setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
073:
074: setFont(new Font("Monospaced", Font.PLAIN, 12));
075: setForeground(Color.BLACK);
076: setBackground(Color.WHITE);
077:
078: caretColor = Color.BLACK;
079: errorColor = Settings.getInstance().getEditorErrorColor();
080: selectionColor = Settings.getInstance()
081: .getEditorSelectionColor();
082: currentLineColor = Settings.getInstance()
083: .getEditorCurrentLineColor();
084: bracketHighlightColor = Color.BLACK;
085: bracketHighlight = true;
086: Settings.getInstance().addPropertyChangeListener(this ,
087: Settings.PROPERTY_EDITOR_TAB_WIDTH,
088: Settings.PROPERTY_EDITOR_CURRENT_LINE_COLOR);
089: }
090:
091: public void dispose() {
092: Settings.getInstance().removePropertyChangeListener(this );
093: }
094:
095: public void propertyChange(PropertyChangeEvent evt) {
096: if (Settings.PROPERTY_EDITOR_TAB_WIDTH.equals(evt
097: .getPropertyName())) {
098: this .calculateTabSize();
099: } else if (Settings.PROPERTY_EDITOR_CURRENT_LINE_COLOR
100: .equals(evt.getPropertyName())) {
101: this .currentLineColor = Settings.getInstance()
102: .getEditorCurrentLineColor();
103: invalidate();
104: }
105: }
106:
107: /**
108: * Switches display of line numbers in the left gutter area.
109: *
110: * @param aFlag
111: */
112: public final void setShowLineNumbers(boolean aFlag) {
113: this .showLineNumbers = aFlag;
114: this .invalidate();
115: }
116:
117: public final boolean getShowLineNumbers() {
118: return this .showLineNumbers;
119: }
120:
121: /**
122: * Returns if this component can be traversed by pressing the
123: * Tab key. This returns false.
124: */
125: @SuppressWarnings("deprecation")
126: public final boolean isManagingFocus() {
127: return false;
128: }
129:
130: /**
131: * Returns the syntax styles used to paint colorized text. Entry <i>n</i>
132: * will be used to paint tokens with id = <i>n</i>.
133: * @see Token
134: */
135: public final SyntaxStyle[] getStyles() {
136: return styles;
137: }
138:
139: /**
140: * Sets the syntax styles used to paint colorized text. Entry <i>n</i>
141: * will be used to paint tokens with id = <i>n</i>.
142: * @param styles The syntax styles
143: * @see Token
144: */
145: public final void setStyles(SyntaxStyle[] styles) {
146: this .styles = styles;
147: repaint();
148: }
149:
150: /**
151: * Returns the caret color.
152: */
153: public final Color getCaretColor() {
154: return caretColor;
155: }
156:
157: /**
158: * Sets the caret color.
159: * @param caretColor The caret color
160: */
161: public final void setCaretColor(Color caretColor) {
162: this .caretColor = caretColor;
163: invalidateSelectedLines();
164: }
165:
166: /**
167: * Returns the selection color.
168: */
169: public final Color getSelectionColor() {
170: return selectionColor;
171: }
172:
173: /**
174: * Sets the selection color.
175: * @param selectionColor The selection color
176: */
177: public final void setSelectionColor(Color selectionColor) {
178: this .selectionColor = selectionColor;
179: invalidateSelectedLines();
180: }
181:
182: /**
183: * Returns the bracket highlight color.
184: */
185: public final Color getBracketHighlightColor() {
186: return bracketHighlightColor;
187: }
188:
189: /**
190: * Sets the bracket highlight color.
191: * @param bracketHighlightColor The bracket highlight color
192: */
193: public final void setBracketHighlightColor(
194: Color bracketHighlightColor) {
195: this .bracketHighlightColor = bracketHighlightColor;
196: invalidateLine(textArea.getBracketLine());
197: }
198:
199: /**
200: * Returns true if bracket highlighting is enabled, false otherwise.
201: * When bracket highlighting is enabled, the bracket matching the
202: * one before the caret (if any) is highlighted.
203: */
204: public final boolean isBracketHighlightEnabled() {
205: return bracketHighlight;
206: }
207:
208: /**
209: * Enables or disables bracket highlighting.
210: * When bracket highlighting is enabled, the bracket matching the
211: * one before the caret (if any) is highlighted.
212: * @param bracketHighlight True if bracket highlighting should be
213: * enabled, false otherwise
214: */
215: public final void setBracketHighlightEnabled(
216: boolean bracketHighlight) {
217: this .bracketHighlight = bracketHighlight;
218: invalidateLine(textArea.getBracketLine());
219: }
220:
221: /**
222: * Returns the font metrics used by this component.
223: */
224: public FontMetrics getFontMetrics() {
225: if (fm == null) {
226: this .fm = getFontMetrics(getFont());
227: }
228: return fm;
229: }
230:
231: /**
232: * Sets the font for this component. This is overridden to update the
233: * cached font metrics and to recalculate which lines are visible.
234: * @param font The font
235: */
236: public void setFont(Font font) {
237: super .setFont(font);
238: this .fm = getFontMetrics(font);
239: calculateTabSize();
240: calculateGutterWidth();
241: }
242:
243: private void calculateGutterWidth() {
244: FontMetrics cfm = getFontMetrics();
245: if (cfm == null) {
246: this .gutterCharWidth = 18;
247: } else {
248: this .gutterCharWidth = cfm.charWidth('9');
249: }
250: if (this .showLineNumbers) {
251: int lastLine = textArea.getLineCount();
252: int chars = StringUtil.numDigits(lastLine);
253: this .gutterWidth = (chars * gutterCharWidth)
254: + (GUTTER_MARGIN * 2);
255: } else {
256: this .gutterWidth = 0;
257: }
258: }
259:
260: public void calculateTabSize() {
261: this .tabSize = -1;
262: if (this .textArea == null)
263: return;
264: if (this .textArea.getDocument() == null)
265: return;
266: FontMetrics cfm = this .getFontMetrics();
267: if (cfm == null)
268: return;
269:
270: Object tab = textArea.getDocument().getProperty(
271: PlainDocument.tabSizeAttribute);
272: int t = -1;
273: if (tab == null) {
274: t = Settings.getInstance().getEditorTabWidth();
275: } else {
276: Integer tsize = (Integer) tab;
277: t = tsize.intValue();
278: }
279: this .tabSize = cfm.charWidth(' ') * t;
280: }
281:
282: public void paint(Graphics gfx) {
283:
284: calculateGutterWidth();
285:
286: Rectangle clipRect = gfx.getClipBounds();
287:
288: if (clipRect != null) {
289: gfx.setColor(this .getBackground());
290: gfx.fillRect(clipRect.x, clipRect.y, clipRect.width,
291: clipRect.height);
292:
293: if (this .showLineNumbers) {
294: gfx.setColor(GUTTER_BACKGROUND);
295: gfx.fillRect(clipRect.x, clipRect.y, gutterWidth
296: - clipRect.x, clipRect.height);
297: }
298: }
299:
300: final int lastLine = textArea.getLineCount();
301: final int visibleCount = textArea.getVisibleLines();
302: final int firstVisible = textArea.getFirstLine();
303:
304: int fheight = fm.getHeight();
305: int firstInvalid = firstVisible + clipRect.y / fheight;
306: int lastInvalid = firstVisible
307: + ((clipRect.y + clipRect.height) / fheight) + 1;
308: if (lastInvalid > lastLine)
309: lastInvalid = lastLine;
310:
311: try {
312: TokenMarker tokenMarker = textArea.getDocument()
313: .getTokenMarker();
314: int x = textArea.getHorizontalOffset();
315:
316: int endLine = firstVisible + visibleCount;
317: if (endLine > lastLine)
318: endLine = lastLine;
319:
320: int cw = this .getWidth() - gutterWidth;
321: int ch = getHeight();
322: int gutterX = this .gutterWidth - GUTTER_MARGIN;
323:
324: final int caretLine = textArea.getCaretLine();
325:
326: for (int line = firstVisible; line <= endLine; line++) {
327: int y = textArea.lineToY(line);
328:
329: if (this .showLineNumbers) {
330: // It seems that the Objects created by Integer.toString()
331: // that are passed to drawString() are not collected
332: // correctly (as seen in the profiler). So each time
333: // the editor gets redrawn a small amount of memory is lost
334: // To workaround this, I'm caching (some of) the values
335: // that are needed here.
336:
337: String s = NumberStringCache.getNumberString(line);
338: int w = s.length() * this .gutterCharWidth;
339: gfx.setColor(GUTTER_COLOR);
340: gfx.drawString(s, gutterX - w, y);
341: }
342:
343: if (line >= firstInvalid && line < lastInvalid) {
344: if (this .showLineNumbers) {
345: gfx.setClip(this .gutterWidth, 0, cw, ch);
346: gfx.translate(this .gutterWidth, 0);
347: }
348:
349: if (line == caretLine
350: && this .currentLineColor != null) {
351: gfx.setColor(currentLineColor);
352: gfx.fillRect(0, y + fm.getLeading()
353: + fm.getMaxDescent(), getWidth(),
354: fheight);
355: gfx.setColor(getBackground());
356: }
357:
358: paintLine(gfx, tokenMarker, line, y, x);
359:
360: if (this .showLineNumbers) {
361: gfx.translate(-this .gutterWidth, 0);
362: gfx.setClip(null);
363: }
364: }
365: }
366: } catch (Exception e) {
367: System.err.println("Error repainting line range {"
368: + firstInvalid + "," + lastInvalid + "}:"
369: + e.getMessage());
370: //e.printStackTrace();
371: }
372: }
373:
374: /**
375: * Marks a line as needing a repaint.
376: * @param line The line to invalidate
377: */
378: public final void invalidateLine(int line) {
379: repaint(0, textArea.lineToY(line) + fm.getMaxDescent()
380: + fm.getLeading(), getWidth(), fm.getHeight());
381: }
382:
383: public int getGutterWidth() {
384: return this .gutterWidth; // + GUTTER_MARGIN;
385: }
386:
387: /**
388: * Marks a range of lines as needing a repaint.
389: * @param firstLine The first line to invalidate
390: * @param lastLine The last line to invalidate
391: */
392: public final void invalidateLineRange(int firstLine, int lastLine) {
393: repaint(0, textArea.lineToY(firstLine) + fm.getMaxDescent()
394: + fm.getLeading(), getWidth(),
395: (lastLine - firstLine + 1) * fm.getHeight());
396: }
397:
398: /**
399: * Repaints the lines containing the selection.
400: */
401: public final void invalidateSelectedLines() {
402: invalidateLineRange(textArea.getSelectionStartLine(), textArea
403: .getSelectionEndLine());
404: }
405:
406: /**
407: * Implementation of TabExpander interface. Returns next tab stop after
408: * a specified point.
409: * @param x The x co-ordinate
410: * @param tabOffset Ignored
411: * @return The next tab stop after <i>x</i>
412: */
413: public float nextTabStop(float x, int tabOffset) {
414: if (tabSize == -1) {
415: this .calculateTabSize();
416: }
417: int offset = textArea.getHorizontalOffset();
418: int ntabs = ((int) x - offset) / tabSize;
419: return (ntabs + 1) * tabSize + offset;
420: }
421:
422: protected void paintLine(Graphics gfx, TokenMarker tokenMarker,
423: int line, int y, int x) {
424: Font defaultFont = getFont();
425: Color defaultColor = getForeground();
426:
427: currentLineIndex = line;
428:
429: if (tokenMarker == null) {
430: paintPlainLine(gfx, line, defaultFont, defaultColor, x, y);
431: } else {
432: paintSyntaxLine(gfx, tokenMarker, line, defaultFont,
433: defaultColor, x, y);
434: }
435: }
436:
437: protected void paintPlainLine(Graphics gfx, int line,
438: Font defaultFont, Color defaultColor, int x, int y) {
439: textArea.getLineText(line, currentLine);
440:
441: paintHighlight(gfx, line, y);
442:
443: gfx.setFont(defaultFont);
444: gfx.setColor(defaultColor);
445:
446: y += fm.getHeight();
447: Utilities.drawTabbedText(currentLine, x, y, gfx, this , 0);
448: }
449:
450: protected void paintSyntaxLine(Graphics gfx,
451: TokenMarker tokenMarker, int line, Font defaultFont,
452: Color defaultColor, int x, int y) {
453: textArea.getLineText(line, currentLine);
454:
455: currentLineTokens = tokenMarker.markTokens(currentLine, line);
456:
457: paintHighlight(gfx, line, y);
458:
459: gfx.setFont(defaultFont);
460: gfx.setColor(defaultColor);
461: y += fm.getHeight();
462: SyntaxUtilities.paintSyntaxLine(currentLine, currentLineTokens,
463: styles, this , gfx, x, y, 0);
464: }
465:
466: protected void paintHighlight(Graphics gfx, int line, int y) {
467: if (line >= textArea.getSelectionStartLine()
468: && line <= textArea.getSelectionEndLine()) {
469: paintLineHighlight(gfx, line, y);
470: }
471:
472: if (bracketHighlight && line == textArea.getBracketLine()) {
473: paintBracketHighlight(gfx, line, y);
474: }
475:
476: if (line == textArea.getCaretLine()) {
477: paintCaret(gfx, line, y);
478: }
479: }
480:
481: protected void paintLineHighlight(Graphics gfx, int line, int y) {
482: int selectionStart = textArea.getSelectionStart();
483: int selectionEnd = textArea.getSelectionEnd();
484:
485: if (selectionStart == selectionEnd)
486: return;
487:
488: int height = fm.getHeight();
489: y += fm.getLeading() + fm.getMaxDescent();
490:
491: Color c = this .textArea.getAlternateSelectionColor();
492: if (c != null) {
493: gfx.setColor(c);
494: } else {
495: gfx.setColor(selectionColor);
496: }
497:
498: int selectionStartLine = textArea.getSelectionStartLine();
499: int selectionEndLine = textArea.getSelectionEndLine();
500: int lineStart = textArea.getLineStartOffset(line);
501:
502: int x1, x2;
503: if (textArea.isSelectionRectangular()) {
504: int lineLen = textArea.getLineLength(line);
505: x1 = textArea
506: ._offsetToX(
507: line,
508: Math
509: .min(
510: lineLen,
511: selectionStart
512: - textArea
513: .getLineStartOffset(selectionStartLine)));// + this.gutterWidth;
514: x2 = textArea
515: ._offsetToX(
516: line,
517: Math
518: .min(
519: lineLen,
520: selectionEnd
521: - textArea
522: .getLineStartOffset(selectionEndLine)));// + this.gutterWidth;
523: if (x1 == x2)
524: x2++;
525: } else if (selectionStartLine == selectionEndLine) {
526: x1 = textArea._offsetToX(line, selectionStart - lineStart);
527: x2 = textArea._offsetToX(line, selectionEnd - lineStart);
528: } else if (line == selectionStartLine) {
529: x1 = textArea._offsetToX(line, selectionStart - lineStart);
530: x2 = getWidth();
531: } else if (line == selectionEndLine) {
532: x1 = 0;
533: x2 = textArea._offsetToX(line, selectionEnd - lineStart);
534: } else {
535: x1 = 0; //gutterWidth;
536: x2 = getWidth();
537: }
538:
539: // "inlined" min/max()
540: gfx.fillRect(x1 > x2 ? x2 : x1, y, x1 > x2 ? (x1 - x2)
541: : (x2 - x1), height);
542:
543: }
544:
545: protected void paintBracketHighlight(Graphics gfx, int line, int y) {
546: int position = textArea.getBracketPosition();
547: if (position == -1)
548: return;
549:
550: y += fm.getLeading() + fm.getMaxDescent();
551: int x = textArea._offsetToX(line, position);// + this.gutterWidth;
552: gfx.setColor(bracketHighlightColor);
553: // Hack!!! Since there is no fast way to get the character
554: // from the bracket matching routine, we use ( since all
555: // brackets probably have the same width anyway
556: gfx.drawRect(x, y, fm.charWidth('(') - 1, fm.getHeight() - 1);
557: }
558:
559: protected void paintCaret(Graphics gfx, int line, int y) {
560: if (textArea.isCaretVisible()) {
561: int offset = textArea.getCaretPosition()
562: - textArea.getLineStartOffset(line);
563: int caretX = textArea._offsetToX(line, offset);// + this.gutterWidth;
564: int caretWidth = (textArea.isOverwriteEnabled() ? fm
565: .charWidth('w') : 2);
566: y += fm.getLeading() + fm.getMaxDescent();
567: int height = fm.getHeight();
568:
569: gfx.setColor(caretColor);
570:
571: if (textArea.isOverwriteEnabled()) {
572: gfx.fillRect(caretX, y + height - 1, caretWidth, 1);
573: } else {
574: gfx.drawRect(caretX, y, caretWidth - 1, height - 1);
575: }
576: }
577: }
578: }
|