source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rtextarea/LineNumberList.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 18.9 KB
Line 
1/*
2 * 02/11/2009
3 *
4 * LineNumberList.java - Renders line numbers in an RTextScrollPane.
5 *
6 * This library is distributed under a modified BSD license. See the included
7 * RSyntaxTextArea.License.txt file for details.
8 */
9package org.fife.ui.rtextarea;
10
11import java.awt.Color;
12import java.awt.Dimension;
13import java.awt.Font;
14import java.awt.FontMetrics;
15import java.awt.Graphics;
16import java.awt.Graphics2D;
17import java.awt.Insets;
18import java.awt.Point;
19import java.awt.Rectangle;
20import java.awt.event.MouseEvent;
21import java.beans.PropertyChangeEvent;
22import java.beans.PropertyChangeListener;
23import java.util.Map;
24import javax.swing.event.CaretEvent;
25import javax.swing.event.CaretListener;
26import javax.swing.event.DocumentEvent;
27import javax.swing.event.MouseInputListener;
28import javax.swing.text.BadLocationException;
29import javax.swing.text.Document;
30import javax.swing.text.Element;
31import javax.swing.text.View;
32
33import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
34import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
35import org.fife.ui.rsyntaxtextarea.folding.Fold;
36import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
37
38
39/**
40 * Renders line numbers in the gutter.
41 *
42 * @author Robert Futrell
43 * @version 1.0
44 */
45public class LineNumberList extends AbstractGutterComponent
46 implements MouseInputListener {
47
48 private int currentLine; // The last line the caret was on.
49 private int lastY = -1; // Used to check if caret changes lines when line wrap is enabled.
50
51 private int cellHeight; // Height of a line number "cell" when word wrap is off.
52 private int cellWidth; // The width used for all line number cells.
53 private int ascent; // The ascent to use when painting line numbers.
54
55 private Map aaHints;
56
57 private int mouseDragStartOffset;
58
59 /**
60 * Listens for events from the current text area.
61 */
62 private Listener l;
63
64 /**
65 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
66 * each paint.
67 */
68 private Insets textAreaInsets;
69
70 /**
71 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
72 * each paint.
73 */
74 private Rectangle visibleRect;
75
76 /**
77 * The index at which line numbering should start. The default value is
78 * <code>1</code>, but applications can change this if, for example, they
79 * are displaying a subset of lines in a file.
80 */
81 private int lineNumberingStartIndex;
82
83
84 /**
85 * Constructs a new <code>LineNumberList</code> using default values for
86 * line number color (gray) and highlighting the current line.
87 *
88 * @param textArea The text component for which line numbers will be
89 * displayed.
90 */
91 public LineNumberList(RTextArea textArea) {
92 this(textArea, null);
93 }
94
95
96 /**
97 * Constructs a new <code>LineNumberList</code>.
98 *
99 * @param textArea The text component for which line numbers will be
100 * displayed.
101 * @param numberColor The color to use for the line numbers. If this is
102 * <code>null</code>, gray will be used.
103 */
104 public LineNumberList(RTextArea textArea, Color numberColor) {
105
106 super(textArea);
107
108 if (numberColor!=null) {
109 setForeground(numberColor);
110 }
111 else {
112 setForeground(Color.GRAY);
113 }
114
115 // Initialize currentLine; otherwise, the current line won't start
116 // off as highlighted.
117 currentLine = 0;
118 setLineNumberingStartIndex(1);
119
120 visibleRect = new Rectangle(); // Must be initialized
121
122 addMouseListener(this);
123 addMouseMotionListener(this);
124
125 aaHints = RSyntaxUtilities.getDesktopAntiAliasHints();
126
127 }
128
129
130 /**
131 * Overridden to set width of this component correctly when we are first
132 * displayed (as keying off of the RTextArea gives us (0,0) when it isn't
133 * yet displayed.
134 */
135 public void addNotify() {
136 super.addNotify();
137 if (textArea!=null) {
138 l.install(textArea); // Won't double-install
139 }
140 updateCellWidths();
141 updateCellHeights();
142 }
143
144
145 /**
146 * Returns the starting line's line number. The default value is
147 * <code>1</code>.
148 *
149 * @return The index
150 * @see #setLineNumberingStartIndex(int)
151 */
152 public int getLineNumberingStartIndex() {
153 return lineNumberingStartIndex;
154 }
155
156
157 /**
158 * {@inheritDoc}
159 */
160 public Dimension getPreferredSize() {
161 int h = textArea!=null ? textArea.getHeight() : 100; // Arbitrary
162 return new Dimension(cellWidth, h);
163 }
164
165
166 /**
167 * Returns the width of the empty border on this component's right-hand
168 * side (or left-hand side, if the orientation is RTL).
169 *
170 * @return The border width.
171 */
172 private int getRhsBorderWidth() {
173 int w = 4;
174 if (textArea instanceof RSyntaxTextArea) {
175 if (((RSyntaxTextArea)textArea).isCodeFoldingEnabled()) {
176 w = 0;
177 }
178 }
179 return w;
180 }
181
182
183 /**
184 * {@inheritDoc}
185 */
186 void handleDocumentEvent(DocumentEvent e) {
187 int newLineCount = textArea!=null ? textArea.getLineCount() : 0;
188 if (newLineCount!=currentLineCount) {
189 // Adjust the amount of space the line numbers take up,
190 // if necessary.
191 if (newLineCount/10 != currentLineCount/10) {
192 updateCellWidths();
193 }
194 currentLineCount = newLineCount;
195 repaint();
196 }
197 }
198
199
200 /**
201 * {@inheritDoc}
202 */
203 void lineHeightsChanged() {
204 updateCellHeights();
205 }
206
207
208 public void mouseClicked(MouseEvent e) {
209 }
210
211
212 public void mouseDragged(MouseEvent e) {
213 if (mouseDragStartOffset>-1) {
214 int pos = textArea.viewToModel(new Point(0, e.getY()));
215 if (pos>=0) { // Not -1
216 textArea.setCaretPosition(mouseDragStartOffset);
217 textArea.moveCaretPosition(pos);
218 }
219 }
220 }
221
222
223 public void mouseEntered(MouseEvent e) {
224 }
225
226
227 public void mouseExited(MouseEvent e) {
228 }
229
230
231 public void mouseMoved(MouseEvent e) {
232 }
233
234
235 public void mousePressed(MouseEvent e) {
236 if (textArea==null) {
237 return;
238 }
239 if (e.getButton()==MouseEvent.BUTTON1) {
240 int pos = textArea.viewToModel(new Point(0, e.getY()));
241 if (pos>=0) { // Not -1
242 textArea.setCaretPosition(pos);
243 }
244 mouseDragStartOffset = pos;
245 }
246 else {
247 mouseDragStartOffset = -1;
248 }
249 }
250
251
252 public void mouseReleased(MouseEvent e) {
253 }
254
255
256 /**
257 * Paints this component.
258 *
259 * @param g The graphics context.
260 */
261 protected void paintComponent(Graphics g) {
262
263 if (textArea==null) {
264 return;
265 }
266
267 visibleRect = g.getClipBounds(visibleRect);
268 if (visibleRect==null) { // ???
269 visibleRect = getVisibleRect();
270 }
271 //System.out.println("LineNumberList repainting: " + visibleRect);
272 if (visibleRect==null) {
273 return;
274 }
275
276 Color bg = getBackground();
277 if (getGutter()!=null) { // Should always be true
278 bg = getGutter().getBackground();
279 }
280 g.setColor(bg);
281 g.fillRect(0,visibleRect.y, cellWidth,visibleRect.height);
282 g.setFont(getFont());
283 if (aaHints!=null) {
284 ((Graphics2D)g).addRenderingHints(aaHints);
285 }
286
287 if (textArea.getLineWrap()) {
288 paintWrappedLineNumbers(g, visibleRect);
289 return;
290 }
291
292 // Get where to start painting (top of the row), and where to paint
293 // the line number (drawString expects y==baseline).
294 // We need to be "scrolled up" just enough for the missing part of
295 // the first line.
296 int topLine = visibleRect.y/cellHeight;
297 int actualTopY = topLine*cellHeight;
298 textAreaInsets = textArea.getInsets(textAreaInsets);
299 actualTopY += textAreaInsets.top;
300 int y = actualTopY + ascent;
301
302 // Get the actual first line to paint, taking into account folding.
303 FoldManager fm = null;
304 if (textArea instanceof RSyntaxTextArea) {
305 fm = ((RSyntaxTextArea)textArea).getFoldManager();
306 topLine += fm.getHiddenLineCountAbove(topLine, true);
307 }
308 final int RHS_BORDER_WIDTH = getRhsBorderWidth();
309
310/*
311 // Highlight the current line's line number, if desired.
312 if (textArea.getHighlightCurrentLine() && currentLine>=topLine &&
313 currentLine<=bottomLine) {
314 g.setColor(textArea.getCurrentLineHighlightColor());
315 g.fillRect(0,actualTopY+(currentLine-topLine)*cellHeight,
316 cellWidth,cellHeight);
317 }
318*/
319
320 // Paint line numbers
321 g.setColor(getForeground());
322 boolean ltr = getComponentOrientation().isLeftToRight();
323 if (ltr) {
324 FontMetrics metrics = g.getFontMetrics();
325 int rhs = getWidth() - RHS_BORDER_WIDTH;
326 int line = topLine + 1;
327 while (y<visibleRect.y+visibleRect.height+ascent && line<=textArea.getLineCount()) {
328 String number = Integer.toString(line + getLineNumberingStartIndex() - 1);
329 int width = metrics.stringWidth(number);
330 g.drawString(number, rhs-width,y);
331 y += cellHeight;
332 Fold fold = fm.getFoldForLine(line-1);
333 // Skip to next line to paint, taking extra care for lines with
334 // block ends and begins together, e.g. "} else {"
335 while (fold!=null && fold.isCollapsed()) {
336 int hiddenLineCount = fold.getLineCount();
337 if (hiddenLineCount==0) {
338 // Fold parser identified a 0-line fold region... This
339 // is really a bug, but we'll handle it gracefully.
340 break;
341 }
342 line += hiddenLineCount;
343 fold = fm.getFoldForLine(line-1);
344 }
345 line++;
346 }
347 }
348 else { // rtl
349 int line = topLine + 1;
350 while (y<visibleRect.y+visibleRect.height && line<textArea.getLineCount()) {
351 String number = Integer.toString(line + getLineNumberingStartIndex() - 1);
352 g.drawString(number, RHS_BORDER_WIDTH, y);
353 y += cellHeight;
354 Fold fold = fm.getFoldForLine(line-1);
355 // Skip to next line to paint, taking extra care for lines with
356 // block ends and begins together, e.g. "} else {"
357 while (fold!=null && fold.isCollapsed()) {
358 line += fold.getLineCount();
359 fold = fm.getFoldForLine(line);
360 }
361 line++;
362 }
363 }
364
365 }
366
367
368 /**
369 * Paints line numbers for text areas with line wrap enabled.
370 *
371 * @param g The graphics context.
372 * @param visibleRect The visible rectangle of these line numbers.
373 */
374 private void paintWrappedLineNumbers(Graphics g, Rectangle visibleRect) {
375
376 // The variables we use are as follows:
377 // - visibleRect is the "visible" area of the text area; e.g.
378 // [0,100, 300,100+(lineCount*cellHeight)-1].
379 // actualTop.y is the topmost-pixel in the first logical line we
380 // paint. Note that we may well not paint this part of the logical
381 // line, as it may be broken into many physical lines, with the first
382 // few physical lines scrolled past. Note also that this is NOT the
383 // visible rect of this line number list; this line number list has
384 // visible rect == [0,0, insets.left-1,visibleRect.height-1].
385 // - offset (<=0) is the y-coordinate at which we begin painting when
386 // we begin painting with the first logical line. This can be
387 // negative, signifying that we've scrolled past the actual topmost
388 // part of this line.
389
390 // The algorithm is as follows:
391 // - Get the starting y-coordinate at which to paint. This may be
392 // above the first visible y-coordinate as we're in line-wrapping
393 // mode, but we always paint entire logical lines.
394 // - Paint that line's line number and highlight, if appropriate.
395 // Increment y to be just below the are we just painted (i.e., the
396 // beginning of the next logical line's view area).
397 // - Get the ending visual position for that line. We can now loop
398 // back, paint this line, and continue until our y-coordinate is
399 // past the last visible y-value.
400
401 // We avoid using modelToView/viewToModel where possible, as these
402 // methods trigger a parsing of the line into syntax tokens, which is
403 // costly. It's cheaper to just grab the child views' bounds.
404
405 // Some variables we'll be using.
406 int width = getWidth();
407
408 RTextAreaUI ui = (RTextAreaUI)textArea.getUI();
409 View v = ui.getRootView(textArea).getView(0);
410 //boolean currentLineHighlighted = textArea.getHighlightCurrentLine();
411 Document doc = textArea.getDocument();
412 Element root = doc.getDefaultRootElement();
413 int lineCount = root.getElementCount();
414 int topPosition = textArea.viewToModel(
415 new Point(visibleRect.x,visibleRect.y));
416 int topLine = root.getElementIndex(topPosition);
417 FoldManager fm = ((RSyntaxTextArea)textArea).getFoldManager();
418
419 // Compute the y at which to begin painting text, taking into account
420 // that 1 logical line => at least 1 physical line, so it may be that
421 // y<0. The computed y-value is the y-value of the top of the first
422 // (possibly) partially-visible view.
423 Rectangle visibleEditorRect = ui.getVisibleEditorRect();
424 Rectangle r = LineNumberList.getChildViewBounds(v, topLine,
425 visibleEditorRect);
426 int y = r.y;
427 final int RHS_BORDER_WIDTH = getRhsBorderWidth();
428 int rhs;
429 boolean ltr = getComponentOrientation().isLeftToRight();
430 if (ltr) {
431 rhs = width - RHS_BORDER_WIDTH;
432 }
433 else { // rtl
434 rhs = RHS_BORDER_WIDTH;
435 }
436 int visibleBottom = visibleRect.y + visibleRect.height;
437 FontMetrics metrics = g.getFontMetrics();
438
439 // Keep painting lines until our y-coordinate is past the visible
440 // end of the text area.
441 g.setColor(getForeground());
442
443 while (y < visibleBottom) {
444
445 r = LineNumberList.getChildViewBounds(v, topLine, visibleEditorRect);
446
447 /*
448 // Highlight the current line's line number, if desired.
449 if (currentLineHighlighted && topLine==currentLine) {
450 g.setColor(textArea.getCurrentLineHighlightColor());
451 g.fillRect(0,y, width,(r.y+r.height)-y);
452 g.setColor(getForeground());
453 }
454 */
455
456 // Paint the line number.
457 int index = (topLine+1) + getLineNumberingStartIndex() - 1;
458 String number = Integer.toString(index);
459 if (ltr) {
460 int strWidth = metrics.stringWidth(number);
461 g.drawString(number, rhs-strWidth,y+ascent);
462 }
463 else {
464 int x = RHS_BORDER_WIDTH;
465 g.drawString(number, x, y+ascent);
466 }
467
468 // The next possible y-coordinate is just after the last line
469 // painted.
470 y += r.height;
471
472 // Update topLine (we're actually using it for our "current line"
473 // variable now).
474 Fold fold = fm.getFoldForLine(topLine);
475 if (fold!=null && fold.isCollapsed()) {
476 topLine += fold.getCollapsedLineCount();
477 }
478 topLine++;
479 if (topLine>=lineCount)
480 break;
481
482 }
483
484 }
485
486
487 /**
488 * Called when this component is removed from the view hierarchy.
489 */
490 public void removeNotify() {
491 super.removeNotify();
492 if (textArea!=null) {
493 l.uninstall(textArea);
494 }
495 }
496
497
498 /**
499 * Repaints a single line in this list.
500 *
501 * @param line The line to repaint.
502 */
503 private void repaintLine(int line) {
504 int y = textArea.getInsets().top;
505 y += line*cellHeight;
506 repaint(0,y, cellWidth,cellHeight);
507 }
508
509
510 /**
511 * Overridden to ensure line number cell sizes are updated with the
512 * font size change.
513 *
514 * @param font The new font to use for line numbers.
515 */
516 public void setFont(Font font) {
517 super.setFont(font);
518 updateCellWidths();
519 updateCellHeights();
520 }
521
522
523 /**
524 * Sets the starting line's line number. The default value is
525 * <code>1</code>. Applications can call this method to change this value
526 * if they are displaying a subset of lines in a file, for example.
527 *
528 * @param index The new index.
529 * @see #getLineNumberingStartIndex()
530 */
531 public void setLineNumberingStartIndex(int index) {
532 lineNumberingStartIndex = index;
533 }
534
535
536 /**
537 * Sets the text area being displayed.
538 *
539 * @param textArea The text area.
540 */
541 public void setTextArea(RTextArea textArea) {
542
543 if (l==null) {
544 l = new Listener();
545 }
546
547 if (this.textArea!=null) {
548 l.uninstall(textArea);
549 }
550
551 super.setTextArea(textArea);
552
553 if (textArea!=null) {
554 l.install(textArea); // Won't double-install
555 updateCellHeights();
556 updateCellWidths();
557 }
558
559 }
560
561
562 /**
563 * Changes the height of the cells in the JList so that they are as tall as
564 * font. This function should be called whenever the user changes the Font
565 * of <code>textArea</code>.
566 */
567 private void updateCellHeights() {
568 if (textArea!=null) {
569 cellHeight = textArea.getLineHeight();
570 ascent = textArea.getMaxAscent();
571 }
572 else {
573 cellHeight = 20; // Arbitrary number.
574 ascent = 5; // Also arbitrary
575 }
576 repaint();
577 }
578
579
580 /**
581 * Changes the width of the cells in the JList so you can see every digit
582 * of each.
583 */
584 void updateCellWidths() {
585
586 int oldCellWidth = cellWidth;
587 cellWidth = getRhsBorderWidth();
588
589 // Adjust the amount of space the line numbers take up, if necessary.
590 if (textArea!=null) {
591 Font font = getFont();
592 if (font!=null) {
593 FontMetrics fontMetrics = getFontMetrics(font);
594 int count = 0;
595 int lineCount = textArea.getLineCount();
596 do {
597 lineCount = lineCount/10;
598 count++;
599 } while (lineCount >= 10);
600 cellWidth += fontMetrics.charWidth('9')*(count+1) + 3;
601 }
602 }
603
604 if (cellWidth!=oldCellWidth) { // Always true
605 revalidate();
606 }
607
608 }
609
610
611 /**
612 * Listens for events in the text area we're interested in.
613 */
614 private class Listener implements CaretListener, PropertyChangeListener {
615
616 private boolean installed;
617
618 public void caretUpdate(CaretEvent e) {
619
620 int dot = textArea.getCaretPosition();
621
622 // We separate the line wrap/no line wrap cases because word wrap
623 // can make a single line from the model (document) be on multiple
624 // lines on the screen (in the view); thus, we have to enhance the
625 // logic for that case a bit - we check the actual y-coordinate of
626 // the caret when line wrap is enabled. For the no-line-wrap case,
627 // getting the line number of the caret suffices. This increases
628 // efficiency in the no-line-wrap case.
629
630 if (textArea.getLineWrap()==false) {
631 int line = textArea.getDocument().getDefaultRootElement().
632 getElementIndex(dot);
633 if (currentLine!=line) {
634 repaintLine(line);
635 repaintLine(currentLine);
636 currentLine = line;
637 }
638 }
639 else { // lineWrap enabled; must check actual y position of caret
640 try {
641 int y = textArea.yForLineContaining(dot);
642 if (y!=lastY) {
643 lastY = y;
644 currentLine = textArea.getDocument().
645 getDefaultRootElement().getElementIndex(dot);
646 repaint(); // *Could* be optimized...
647 }
648 } catch (BadLocationException ble) {
649 ble.printStackTrace();
650 }
651 }
652
653 }
654
655 public void install(RTextArea textArea) {
656 if (!installed) {
657 //System.out.println("Installing");
658 textArea.addCaretListener(this);
659 textArea.addPropertyChangeListener(this);
660 caretUpdate(null); // Force current line highlight repaint
661 installed = true;
662 }
663 }
664
665 public void propertyChange(PropertyChangeEvent e) {
666
667 String name = e.getPropertyName();
668
669 // If they change the current line highlight in any way...
670 if (RTextArea.HIGHLIGHT_CURRENT_LINE_PROPERTY.equals(name) ||
671 RTextArea.CURRENT_LINE_HIGHLIGHT_COLOR_PROPERTY.equals(name)) {
672 repaintLine(currentLine);
673 }
674
675 }
676
677 public void uninstall(RTextArea textArea) {
678 if (installed) {
679 //System.out.println("Uninstalling");
680 textArea.removeCaretListener(this);
681 textArea.removePropertyChangeListener(this);
682 installed = false;
683 }
684 }
685
686 }
687
688
689}
Note: See TracBrowser for help on using the repository browser.