001: package net.xoetrope.builder.editor.syntaxhighlight;
002:
003: import java.awt.Color;
004: import java.awt.Dimension;
005: import java.awt.Font;
006: import java.awt.FontMetrics;
007: import java.awt.Graphics;
008: import java.awt.Graphics2D;
009: import java.awt.Insets;
010: import java.awt.Rectangle;
011: import java.awt.event.MouseEvent;
012: import javax.swing.JComponent;
013: import javax.swing.JFrame;
014: import javax.swing.SwingConstants;
015: import javax.swing.SwingUtilities;
016: import javax.swing.border.Border;
017: import javax.swing.border.CompoundBorder;
018: import javax.swing.border.MatteBorder;
019: import java.awt.SystemColor;
020:
021: /**
022: * The gutter is the component that displays folding triangles and line
023: * numbers to the left of the text area. The only methods in this class
024: * that should be called by plugins are those for adding and removing
025: * text area extensions.
026: *
027: */
028: public class Gutter extends JComponent implements SwingConstants {
029: /**
030: * The lowest possible layer.
031: */
032: public static final int LOWEST_LAYER = Integer.MIN_VALUE;
033:
034: /**
035: * Default extension layer. This is above the wrap guide but below the
036: * bracket highlight.
037: */
038: public static final int DEFAULT_LAYER = 0;
039:
040: /**
041: * Highest possible layer.
042: */
043: public static final int HIGHEST_LAYER = Integer.MAX_VALUE;
044:
045: // Gutter constructor
046: public Gutter(JFrame view, JEditTextArea textArea) {
047: this .view = view;
048: this .textArea = textArea;
049:
050: setAutoscrolls(true);
051: setOpaque(true);
052: setBackground(SystemColor.inactiveCaption);
053: setForeground(SystemColor.inactiveCaptionText);
054: }
055:
056: // paintComponent() method
057: public void paintComponent(Graphics _gfx) {
058: Graphics2D gfx = (Graphics2D) _gfx;
059:
060: // fill the background
061: Rectangle clip = gfx.getClipBounds();
062: gfx.setColor(getBackground());
063: gfx.fillRect(clip.x, clip.y, clip.width, clip.height);
064:
065: // if buffer is loading, don't paint anything
066: if (textArea.getLineCount() == 0)
067: return;
068:
069: int lineHeight = textArea.getPainter().getFontMetrics()
070: .getHeight();
071:
072: int firstLine = textArea.getFirstLine();// clip.y / lineHeight;
073: int lastLine = textArea.getLineCount();// clip.y + clip.height - 1 ) / lineHeight;
074:
075: int y = (clip.y - clip.y % lineHeight);
076:
077: // textArea.chunkCache.updateChunksUpTo(lastLine);
078:
079: for (int line = firstLine; line <= lastLine; line++, y += lineHeight)
080: paintLine(gfx, line, y);
081: }
082:
083: /**
084: * Adds a text area highlight, which can perform custom painting and
085: * tool tip handling.
086: * @param extension The extension
087: */
088: public void addHighlight(Highlight highlight) {
089: highlight.init(textArea, highlights);
090: highlights = highlight;
091: repaint();
092: }
093:
094: /**
095: * Returns the tool tip to display at the specified location.
096: * @param evt The mouse event
097: */
098: public String getToolTipText(MouseEvent evt) {
099: if (highlights != null)
100: return highlights.getToolTipText(evt);
101: else
102: return null;
103: }
104:
105: /**
106: * Convenience method for setting a default matte border on the right
107: * with the specified border width and color
108: * @param width The border width (in pixels)
109: * @param color1 The focused border color
110: * @param color2 The unfocused border color
111: * @param color3 The gutter/text area gap color
112: */
113: public void setBorder(int width, Color color1, Color color2,
114: Color color3) {
115: this .borderWidth = width;
116:
117: focusBorder = new CompoundBorder(new MatteBorder(0, 0, 0,
118: width, color3), new MatteBorder(0, 0, 0, width, color1));
119: noFocusBorder = new CompoundBorder(new MatteBorder(0, 0, 0,
120: width, color3), new MatteBorder(0, 0, 0, width, color2));
121: updateBorder();
122: }
123:
124: /**
125: * Sets the border differently if the text area has focus or not.
126: */
127: public void updateBorder() {
128: // because we are called from the text area's focus handler,
129: // we do an invokeLater() so that the view's focus handler
130: // has a chance to execute and set the edit pane properly
131: SwingUtilities.invokeLater(new Runnable() {
132: public void run() {
133: // if(view.getEditPane() == null)
134: // return;
135: //
136: // if(view.getEditPane().getTextArea() == textArea)
137: // setBorder(focusBorder);
138: // else
139: setBorder(noFocusBorder);
140: }
141: });
142: }
143:
144: /*
145: * JComponent.setBorder(Border) is overridden here to cache the left
146: * inset of the border (if any) to avoid having to fetch it during every
147: * repaint.
148: */
149: public void setBorder(Border border) {
150: super .setBorder(border);
151:
152: if (border == null) {
153: collapsedSize.width = 0;
154: collapsedSize.height = 0;
155: } else {
156: Insets insets = border.getBorderInsets(this );
157: collapsedSize.width = FOLD_MARKER_SIZE + insets.right;
158: collapsedSize.height = gutterSize.height = insets.top
159: + insets.bottom;
160: gutterSize.width = FOLD_MARKER_SIZE + insets.right
161: + fm.stringWidth("1234");
162: }
163:
164: revalidate();
165: }
166:
167: /*
168: * JComponent.setFont(Font) is overridden here to cache the baseline for
169: * the font. This avoids having to get the font metrics during every
170: * repaint.
171: */
172: public void setFont(Font font) {
173: super .setFont(font);
174:
175: fm = getFontMetrics(font);
176:
177: baseline = fm.getAscent();
178:
179: Border border = getBorder();
180: if (border != null)
181: gutterSize.width = FOLD_MARKER_SIZE
182: + border.getBorderInsets(this ).right
183: + fm.stringWidth("1234");
184: else
185: gutterSize.width = FOLD_MARKER_SIZE
186: + fm.stringWidth("1234");
187: revalidate();
188: }
189:
190: /**
191: * Get the foreground color for highlighted line numbers
192: * @return The highlight color
193: */
194: public Color getHighlightedForeground() {
195: return intervalHighlight;
196: }
197:
198: public void setHighlightedForeground(Color highlight) {
199: intervalHighlight = highlight;
200: }
201:
202: public Color getCurrentLineForeground() {
203: return currentLineHighlight;
204: }
205:
206: public void setCurrentLineForeground(Color highlight) {
207: currentLineHighlight = highlight;
208: }
209:
210: /*
211: * Component.getPreferredSize() is overridden here to support the
212: * collapsing behavior.
213: */
214: public Dimension getPreferredSize() {
215: if (expanded)
216: return gutterSize;
217: else
218: return collapsedSize;
219: }
220:
221: public Dimension getMinimumSize() {
222: return getPreferredSize();
223: }
224:
225: /**
226: * Identifies whether the horizontal alignment of the line numbers.
227: * @return Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
228: */
229: public int getLineNumberAlignment() {
230: return alignment;
231: }
232:
233: /**
234: * Sets the horizontal alignment of the line numbers.
235: * @param alignment Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
236: */
237: public void setLineNumberAlignment(int alignment) {
238: if (this .alignment == alignment)
239: return;
240:
241: this .alignment = alignment;
242:
243: repaint();
244: }
245:
246: /**
247: * Identifies whether the gutter is collapsed or expanded.
248: * @return true if the gutter is expanded, false if it is collapsed
249: */
250: public boolean isExpanded() {
251: return expanded;
252: }
253:
254: /**
255: * Sets whether the gutter is collapsed or expanded and force the text
256: * area to update its layout if there is a change.
257: * @param collapsed true if the gutter is expanded,
258: * false if it is collapsed
259: */
260: public void setExpanded(boolean expanded) {
261: if (this .expanded == expanded)
262: return;
263:
264: this .expanded = expanded;
265:
266: textArea.revalidate();
267: }
268:
269: /**
270: * Toggles whether the gutter is collapsed or expanded.
271: */
272: public void toggleExpanded() {
273: setExpanded(!expanded);
274: }
275:
276: /**
277: * Sets the number of lines between highlighted line numbers.
278: * @return The number of lines between highlighted line numbers or
279: * zero if highlighting is disabled
280: */
281: public int getHighlightInterval() {
282: return interval;
283: }
284:
285: /**
286: * Sets the number of lines between highlighted line numbers. Any value
287: * less than or equal to one will result in highlighting being disabled.
288: * @param interval The number of lines between highlighted line numbers
289: */
290: public void setHighlightInterval(int interval) {
291: if (interval <= 1)
292: interval = 0;
293: this .interval = interval;
294: repaint();
295: }
296:
297: public boolean isCurrentLineHighlightEnabled() {
298: return currentLineHighlightEnabled;
299: }
300:
301: public void setCurrentLineHighlightEnabled(boolean enabled) {
302: if (currentLineHighlightEnabled == enabled)
303: return;
304:
305: currentLineHighlightEnabled = enabled;
306:
307: repaint();
308: }
309:
310: /**
311: * Returns the bracket highlight color.
312: */
313: public final Color getBracketHighlightColor() {
314: return bracketHighlightColor;
315: }
316:
317: /**
318: * Sets the bracket highlight color.
319: * @param bracketHighlightColor The bracket highlight color
320: */
321: public final void setBracketHighlightColor(
322: Color bracketHighlightColor) {
323: this .bracketHighlightColor = bracketHighlightColor;
324: repaint();
325: }
326:
327: /**
328: * Returns true if bracket highlighting is enabled, false otherwise.
329: * When bracket highlighting is enabled, the bracket matching the
330: * one before the caret (if any) is highlighted.
331: */
332: public final boolean isBracketHighlightEnabled() {
333: return bracketHighlight;
334: }
335:
336: /**
337: * Enables or disables bracket highlighting.
338: * When bracket highlighting is enabled, the bracket matching the
339: * one before the caret (if any) is highlighted.
340: * @param bracketHighlight True if bracket highlighting should be
341: * enabled, false otherwise
342: */
343: public final void setBracketHighlightEnabled(
344: boolean bracketHighlight) {
345: this .bracketHighlight = bracketHighlight;
346: repaint();
347: }
348:
349: public Color getMarkerHighlightColor() {
350: return markerHighlightColor;
351: }
352:
353: public void setMarkerHighlightColor(Color markerHighlightColor) {
354: this .markerHighlightColor = markerHighlightColor;
355: }
356:
357: public boolean isMarkerHighlightEnabled() {
358: return markerHighlight;
359: }
360:
361: public void setMarkerHighlightEnabled(boolean markerHighlight) {
362: this .markerHighlight = markerHighlight;
363: }
364:
365: private static final int FOLD_MARKER_SIZE = 0;//12;
366:
367: private JFrame view;
368: private JEditTextArea textArea;
369:
370: protected Highlight highlights;
371:
372: private int baseline;
373:
374: private Dimension gutterSize = new Dimension(0, 0);
375: private Dimension collapsedSize = new Dimension(0, 0);
376:
377: private Color intervalHighlight;
378: private Color currentLineHighlight;
379: private Color foldColor;
380:
381: private FontMetrics fm;
382:
383: private int alignment = RIGHT;
384:
385: private int interval;
386: private boolean currentLineHighlightEnabled;
387: private boolean expanded = true;
388:
389: private boolean bracketHighlight;
390: private Color bracketHighlightColor;
391:
392: private boolean markerHighlight;
393: private Color markerHighlightColor;
394:
395: private int borderWidth;
396: private Border focusBorder, noFocusBorder;
397:
398: private void paintLine(Graphics2D gfx, int line, int y) {
399: int lineHeight = textArea.getPainter().getFontMetrics()
400: .getHeight();
401:
402: if (highlights != null)
403: highlights.paintHighlight(gfx, line, y);
404:
405: // Skip lines beyond EOF
406: if (line == -1)
407: return;
408:
409: // Paint bracket scope
410: if (bracketHighlight) {
411: int bracketLine = textArea.getBracketLine();
412: int caretLine = textArea.getCaretLine();
413:
414: if (textArea.isBracketHighlightVisible()
415: && line >= Math.min(caretLine, bracketLine)
416: && line <= Math.max(caretLine, bracketLine)) {
417: int caretScreenLine;
418: if (caretLine > (textArea.getVisibleLines() + textArea
419: .getFirstLine()))
420: caretScreenLine = Integer.MAX_VALUE;
421: else {
422: caretScreenLine = textArea.getLineOfOffset(textArea
423: .getCaretPosition());
424: }
425:
426: int bracketScreenLine;
427: bracketScreenLine = Integer.MAX_VALUE;
428:
429: if (caretScreenLine > bracketScreenLine) {
430: int tmp = caretScreenLine;
431: caretScreenLine = bracketScreenLine;
432: bracketScreenLine = tmp;
433: }
434:
435: gfx.setColor(bracketHighlightColor);
436: if (bracketScreenLine == caretScreenLine) {
437: // do nothing
438: } else if (line == caretScreenLine) {
439: gfx.fillRect(5, y + lineHeight / 2, 5, 2);
440: gfx.fillRect(5, y + lineHeight / 2, 2, lineHeight
441: - lineHeight / 2);
442: } else if (line == bracketScreenLine) {
443: gfx.fillRect(5, y, 2, lineHeight / 2);
444: gfx.fillRect(5, y + lineHeight / 2, 5, 2);
445: } else if (line > caretScreenLine
446: && line < bracketScreenLine) {
447: gfx.fillRect(5, y, 2, lineHeight);
448: }
449: }
450: }
451:
452: // Paint line numbers
453: String number = Integer.toString(line + 1);
454:
455: int offset;
456: switch (alignment) {
457: case RIGHT:
458: offset = gutterSize.width - collapsedSize.width
459: - (fm.stringWidth(number) + 1);
460: break;
461: case CENTER:
462: offset = ((gutterSize.width - collapsedSize.width) - fm
463: .stringWidth(number)) / 2;
464: break;
465: case LEFT:
466: default:
467: offset = 0;
468: break;
469: }
470:
471: boolean highlightCurrentLine = currentLineHighlightEnabled
472: && textArea.getSelectedText().length() == 0;
473: if (line == textArea.getCaretLine() && highlightCurrentLine)
474: gfx.setColor(currentLineHighlight);
475: else if (interval > 1
476: && (line + textArea.getFirstLine() + 1) % interval == 0)
477: gfx.setColor(intervalHighlight);
478: else
479: gfx.setColor(getForeground());
480:
481: gfx.drawString(number, FOLD_MARKER_SIZE + offset, baseline + y);
482: }
483: }
|