source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rtextarea/IconRowHeader.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: 21.0 KB
Line 
1/*
2 * 02/17/2009
3 *
4 * IconRowHeader.java - Renders icons in the gutter.
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.Graphics;
14import java.awt.Insets;
15import java.awt.Point;
16import java.awt.Rectangle;
17import java.awt.event.MouseEvent;
18import java.awt.event.MouseListener;
19import java.util.ArrayList;
20import java.util.Collections;
21import java.util.Iterator;
22import java.util.List;
23import javax.swing.Icon;
24import javax.swing.JPanel;
25import javax.swing.UIManager;
26import javax.swing.event.DocumentEvent;
27import javax.swing.text.BadLocationException;
28import javax.swing.text.Document;
29import javax.swing.text.Element;
30import javax.swing.text.Position;
31import javax.swing.text.View;
32
33
34/**
35 * Renders icons in the {@link Gutter}. This can be used to visually mark
36 * lines containing syntax errors, lines with breakpoints set on them, etc.<p>
37 *
38 * This component has built-in support for displaying icons representing
39 * "bookmarks;" that is, lines a user can cycle through via F2 and Shift+F2.
40 * Bookmarked lines are toggled via Ctrl+F2, or by clicking in the icon area
41 * at the line to bookmark. In order to enable bookmarking, you must first
42 * assign an icon to represent a bookmarked line, then actually enable the
43 * feature. This is actually done on the parent {@link Gutter} component:<p>
44 *
45 * <pre>
46 * Gutter gutter = scrollPane.getGutter();
47 * gutter.setBookmarkIcon(new ImageIcon("bookmark.png"));
48 * gutter.setBookmarkingEnabled(true);
49 * </pre>
50 *
51 * @author Robert Futrell
52 * @version 1.0
53 */
54public class IconRowHeader extends AbstractGutterComponent implements MouseListener {
55
56 /**
57 * The icons to render.
58 */
59 protected List trackingIcons;
60
61 /**
62 * The width of this component.
63 */
64 protected int width;
65
66 /**
67 * Whether this component listens for mouse clicks and toggles "bookmark"
68 * icons on them.
69 */
70 private boolean bookmarkingEnabled;
71
72 /**
73 * The icon to use for bookmarks.
74 */
75 private Icon bookmarkIcon;
76
77 /**
78 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
79 * each paint.
80 */
81 protected Rectangle visibleRect;
82
83 /**
84 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
85 * each paint.
86 */
87 protected Insets textAreaInsets;
88
89 /**
90 * The first line in the active line range.
91 */
92 protected int activeLineRangeStart;
93
94 /**
95 * The end line in the active line range.
96 */
97 protected int activeLineRangeEnd;
98
99 /**
100 * The color used to highlight the active code block.
101 */
102 private Color activeLineRangeColor;
103
104
105 /**
106 * Constructor.
107 *
108 * @param textArea The parent text area.
109 */
110 public IconRowHeader(RTextArea textArea) {
111
112 super(textArea);
113 visibleRect = new Rectangle();
114 width = 16;
115 addMouseListener(this);
116 activeLineRangeStart = activeLineRangeEnd = -1;
117 setActiveLineRangeColor(null);
118
119 // Must explicitly set our background color, otherwise we inherit that
120 // of the parent Gutter.
121 updateBackground();
122
123 }
124
125
126 /**
127 * Adds an icon that tracks an offset in the document, and is displayed
128 * adjacent to the line numbers. This is useful for marking things such
129 * as source code errors.
130 *
131 * @param offs The offset to track.
132 * @param icon The icon to display. This should be small (say 16x16).
133 * @return A tag for this icon.
134 * @throws BadLocationException If <code>offs</code> is an invalid offset
135 * into the text area.
136 * @see #removeTrackingIcon(Object)
137 */
138 public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon)
139 throws BadLocationException {
140 Position pos = textArea.getDocument().createPosition(offs);
141 GutterIconImpl ti = new GutterIconImpl(icon, pos);
142 if (trackingIcons==null) {
143 trackingIcons = new ArrayList(1); // Usually small
144 }
145 int index = Collections.binarySearch(trackingIcons, ti);
146 if (index<0) {
147 index = -(index+1);
148 }
149 trackingIcons.add(index, ti);
150 repaint();
151 return ti;
152 }
153
154
155 /**
156 * Clears the active line range.
157 *
158 * @see #setActiveLineRange(int, int)
159 */
160 public void clearActiveLineRange() {
161 if (activeLineRangeStart!=-1 || activeLineRangeEnd!=-1) {
162 activeLineRangeStart = activeLineRangeEnd = -1;
163 repaint();
164 }
165 }
166
167
168 /**
169 * Returns the color used to paint the active line range, if any.
170 *
171 * @return The color.
172 * @see #setActiveLineRangeColor(Color)
173 */
174 public Color getActiveLineRangeColor() {
175 return activeLineRangeColor;
176 }
177
178
179 /**
180 * Returns the icon to use for bookmarks.
181 *
182 * @return The icon to use for bookmarks. If this is <code>null</code>,
183 * bookmarking is effectively disabled.
184 * @see #setBookmarkIcon(Icon)
185 * @see #isBookmarkingEnabled()
186 */
187 public Icon getBookmarkIcon() {
188 return bookmarkIcon;
189 }
190
191
192 /**
193 * Returns the bookmarks known to this gutter.
194 *
195 * @return The bookmarks. If there are no bookmarks, an empty array is
196 * returned.
197 */
198 public GutterIconInfo[] getBookmarks() {
199
200 List retVal = new ArrayList(1);
201
202 if (trackingIcons!=null) {
203 for (int i=0; i<trackingIcons.size(); i++) {
204 GutterIconImpl ti = getTrackingIcon(i);
205 if (ti.getIcon()==bookmarkIcon) {
206 retVal.add(ti);
207 }
208 }
209 }
210
211 GutterIconInfo[] array = new GutterIconInfo[retVal.size()];
212 return (GutterIconInfo[])retVal.toArray(array);
213
214 }
215
216
217 /**
218 * {@inheritDoc}
219 */
220 void handleDocumentEvent(DocumentEvent e) {
221 int newLineCount = textArea.getLineCount();
222 if (newLineCount!=currentLineCount) {
223 currentLineCount = newLineCount;
224 repaint();
225 }
226 }
227
228
229 /**
230 * {@inheritDoc}
231 */
232 public Dimension getPreferredSize() {
233 int h = textArea!=null ? textArea.getHeight() : 100; // Arbitrary
234 return new Dimension(width, h);
235 }
236
237
238 protected GutterIconImpl getTrackingIcon(int index) {
239 return (GutterIconImpl)trackingIcons.get(index);
240 }
241
242
243 /**
244 * Returns the tracking icons at the specified line.
245 *
246 * @param line The line.
247 * @return The tracking icons at that line. If there are no tracking
248 * icons there, this will be an empty array.
249 * @throws BadLocationException If <code>line</code> is invalid.
250 */
251 public GutterIconInfo[] getTrackingIcons(int line)
252 throws BadLocationException {
253
254 List retVal = new ArrayList(1);
255
256 if (trackingIcons!=null) {
257 int start = textArea.getLineStartOffset(line);
258 int end = textArea.getLineEndOffset(line);
259 if (line==textArea.getLineCount()-1) {
260 end++; // Hack
261 }
262 for (int i=0; i<trackingIcons.size(); i++) {
263 GutterIconImpl ti = getTrackingIcon(i);
264 int offs = ti.getMarkedOffset();
265 if (offs>=start && offs<end) {
266 retVal.add(ti);
267 }
268 else if (offs>=end) {
269 break; // Quit early
270 }
271 }
272 }
273
274 GutterIconInfo[] array = new GutterIconInfo[retVal.size()];
275 return (GutterIconInfo[])retVal.toArray(array);
276
277 }
278
279
280 /**
281 * Returns whether bookmarking is enabled.
282 *
283 * @return Whether bookmarking is enabled.
284 * @see #setBookmarkingEnabled(boolean)
285 */
286 public boolean isBookmarkingEnabled() {
287 return bookmarkingEnabled;
288 }
289
290
291 /**
292 * {@inheritDoc}
293 */
294 void lineHeightsChanged() {
295 repaint();
296 }
297
298
299 public void mouseClicked(MouseEvent e) {
300 }
301
302
303 public void mouseEntered(MouseEvent e) {
304 }
305
306
307 public void mouseExited(MouseEvent e) {
308 }
309
310
311 public void mousePressed(MouseEvent e) {
312 if (bookmarkingEnabled && bookmarkIcon!=null) {
313 try {
314 int offs = textArea.viewToModel(e.getPoint());
315 if (offs>-1) {
316 int line = textArea.getLineOfOffset(offs);
317 toggleBookmark(line);
318 }
319 } catch (BadLocationException ble) {
320 ble.printStackTrace(); // Never happens
321 }
322 }
323 }
324
325
326 public void mouseReleased(MouseEvent e) {
327 }
328
329
330 /**
331 * {@inheritDoc}
332 */
333 protected void paintComponent(Graphics g) {
334
335 if (textArea==null) {
336 return;
337 }
338
339 visibleRect = g.getClipBounds(visibleRect);
340 if (visibleRect==null) { // ???
341 visibleRect = getVisibleRect();
342 }
343 //System.out.println("IconRowHeader repainting: " + visibleRect);
344 if (visibleRect==null) {
345 return;
346 }
347
348 g.setColor(getBackground());
349 g.fillRect(0,visibleRect.y, width,visibleRect.height);
350
351 if (textArea.getLineWrap()) {
352 paintComponentWrapped(g);
353 return;
354 }
355
356 Document doc = textArea.getDocument();
357 Element root = doc.getDefaultRootElement();
358 textAreaInsets = textArea.getInsets(textAreaInsets);
359
360 // Get the first and last lines to paint.
361 int cellHeight = textArea.getLineHeight();
362 int topLine = (visibleRect.y-textAreaInsets.top)/cellHeight;
363 int bottomLine = Math.min(topLine+visibleRect.height/cellHeight+1,
364 root.getElementCount());
365
366 // Get where to start painting (top of the row).
367 // We need to be "scrolled up" up just enough for the missing part of
368 // the first line.
369 int y = topLine*cellHeight + textAreaInsets.top;
370
371 if ((activeLineRangeStart>=topLine&&activeLineRangeStart<=bottomLine) ||
372 (activeLineRangeEnd>=topLine && activeLineRangeEnd<=bottomLine) ||
373 (activeLineRangeStart<=topLine && activeLineRangeEnd>=bottomLine)) {
374
375 g.setColor(activeLineRangeColor);
376 int firstLine = Math.max(activeLineRangeStart, topLine);
377 int y1 = firstLine * cellHeight + textAreaInsets.top;
378 int lastLine = Math.min(activeLineRangeEnd, bottomLine);
379 int y2 = (lastLine+1) * cellHeight + textAreaInsets.top - 1;
380
381 int j = y1;
382 while (j<=y2) {
383 int yEnd = Math.min(y2, j+getWidth());
384 int xEnd = yEnd-j;
385 g.drawLine(0,j, xEnd,yEnd);
386 j += 2;
387 }
388
389 int i = 2;
390 while (i<getWidth()) {
391 int yEnd = y1 + getWidth() - i;
392 g.drawLine(i,y1, getWidth(),yEnd);
393 i += 2;
394 }
395
396 if (firstLine==activeLineRangeStart) {
397 g.drawLine(0,y1, getWidth(),y1);
398 }
399 if (lastLine==activeLineRangeEnd) {
400 g.drawLine(0,y2, getWidth(),y2);
401 }
402
403 }
404
405 if (trackingIcons!=null) {
406 int lastLine = bottomLine;
407 for (int i=trackingIcons.size()-1; i>=0; i--) { // Last to first
408 GutterIconInfo ti = getTrackingIcon(i);
409 int offs = ti.getMarkedOffset();
410 if (offs>=0 && offs<=doc.getLength()) {
411 int line = root.getElementIndex(offs);
412 if (line<=lastLine && line>=topLine) {
413 Icon icon = ti.getIcon();
414 if (icon!=null) {
415 int y2 = y + (line-topLine)*cellHeight;
416 y2 += (cellHeight-icon.getIconHeight())/2;
417 ti.getIcon().paintIcon(this, g, 0, y2);
418 lastLine = line-1; // Paint only 1 icon per line
419 }
420 }
421 else if (line<topLine) {
422 break;
423 }
424 }
425 }
426 }
427
428 }
429
430
431 /**
432 * Paints icons when line wrapping is enabled.
433 *
434 * @param g The graphics context.
435 */
436 private void paintComponentWrapped(Graphics g) {
437
438 // The variables we use are as follows:
439 // - visibleRect is the "visible" area of the text area; e.g.
440 // [0,100, 300,100+(lineCount*cellHeight)-1].
441 // actualTop.y is the topmost-pixel in the first logical line we
442 // paint. Note that we may well not paint this part of the logical
443 // line, as it may be broken into many physical lines, with the first
444 // few physical lines scrolled past. Note also that this is NOT the
445 // visible rect of this line number list; this line number list has
446 // visible rect == [0,0, insets.left-1,visibleRect.height-1].
447 // - offset (<=0) is the y-coordinate at which we begin painting when
448 // we begin painting with the first logical line. This can be
449 // negative, signifying that we've scrolled past the actual topmost
450 // part of this line.
451
452 // The algorithm is as follows:
453 // - Get the starting y-coordinate at which to paint. This may be
454 // above the first visible y-coordinate as we're in line-wrapping
455 // mode, but we always paint entire logical lines.
456 // - Paint that line's line number and highlight, if appropriate.
457 // Increment y to be just below the are we just painted (i.e., the
458 // beginning of the next logical line's view area).
459 // - Get the ending visual position for that line. We can now loop
460 // back, paint this line, and continue until our y-coordinate is
461 // past the last visible y-value.
462
463 // We avoid using modelToView/viewToModel where possible, as these
464 // methods trigger a parsing of the line into syntax tokens, which is
465 // costly. It's cheaper to just grab the child views' bounds.
466
467 RTextAreaUI ui = (RTextAreaUI)textArea.getUI();
468 View v = ui.getRootView(textArea).getView(0);
469// boolean currentLineHighlighted = textArea.getHighlightCurrentLine();
470 Document doc = textArea.getDocument();
471 Element root = doc.getDefaultRootElement();
472 int lineCount = root.getElementCount();
473 int topPosition = textArea.viewToModel(
474 new Point(visibleRect.x,visibleRect.y));
475 int topLine = root.getElementIndex(topPosition);
476
477 // Compute the y at which to begin painting text, taking into account
478 // that 1 logical line => at least 1 physical line, so it may be that
479 // y<0. The computed y-value is the y-value of the top of the first
480 // (possibly) partially-visible view.
481 Rectangle visibleEditorRect = ui.getVisibleEditorRect();
482 Rectangle r = IconRowHeader.getChildViewBounds(v, topLine,
483 visibleEditorRect);
484 int y = r.y;
485
486 int visibleBottom = visibleRect.y + visibleRect.height;
487
488 // Get the first possibly visible icon index.
489 int currentIcon = -1;
490 if (trackingIcons!=null) {
491 for (int i=0; i<trackingIcons.size(); i++) {
492 GutterIconImpl icon = getTrackingIcon(i);
493 int offs = icon.getMarkedOffset();
494 if (offs>=0 && offs<=doc.getLength()) {
495 int line = root.getElementIndex(offs);
496 if (line>=topLine) {
497 currentIcon = i;
498 break;
499 }
500 }
501 }
502 }
503
504 // Keep painting lines until our y-coordinate is past the visible
505 // end of the text area.
506 g.setColor(getForeground());
507 int cellHeight = textArea.getLineHeight();
508 while (y < visibleBottom) {
509
510 r = getChildViewBounds(v, topLine, visibleEditorRect);
511// int lineEndY = r.y+r.height;
512
513 /*
514 // Highlight the current line's line number, if desired.
515 if (currentLineHighlighted && topLine==currentLine) {
516 g.setColor(textArea.getCurrentLineHighlightColor());
517 g.fillRect(0,y, width,lineEndY-y);
518 g.setColor(getForeground());
519 }
520 */
521
522 // Possibly paint an icon.
523 if (currentIcon>-1) {
524 // We want to paint the last icon added for this line.
525 GutterIconImpl toPaint = null;
526 while (currentIcon<trackingIcons.size()) {
527 GutterIconImpl ti = getTrackingIcon(currentIcon);
528 int offs = ti.getMarkedOffset();
529 if (offs>=0 && offs<=doc.getLength()) {
530 int line = root.getElementIndex(offs);
531 if (line==topLine) {
532 toPaint = ti;
533 }
534 else if (line>topLine) {
535 break;
536 }
537 }
538 currentIcon++;
539 }
540 if (toPaint!=null) {
541 Icon icon = toPaint.getIcon();
542 if (icon!=null) {
543 int y2 = y + (cellHeight-icon.getIconHeight())/2;
544 icon.paintIcon(this, g, 0, y2);
545 }
546 }
547 }
548
549 // The next possible y-coordinate is just after the last line
550 // painted.
551 y += r.height;
552
553 // Update topLine (we're actually using it for our "current line"
554 // variable now).
555 topLine++;
556 if (topLine>=lineCount)
557 break;
558
559 }
560
561 }
562
563
564 /**
565 * Removes the specified tracking icon.
566 *
567 * @param tag A tag for a tracking icon.
568 * @see #removeAllTrackingIcons()
569 * @see #addOffsetTrackingIcon(int, Icon)
570 */
571 public void removeTrackingIcon(Object tag) {
572 if (trackingIcons!=null && trackingIcons.remove(tag)) {
573 repaint();
574 }
575 }
576
577
578 /**
579 * Removes all tracking icons.
580 *
581 * @see #removeTrackingIcon(Object)
582 * @see #addOffsetTrackingIcon(int, Icon)
583 */
584 public void removeAllTrackingIcons() {
585 if (trackingIcons!=null && trackingIcons.size()>0) {
586 trackingIcons.clear();
587 repaint();
588 }
589 }
590
591
592 /**
593 * Removes all bookmark tracking icons.
594 */
595 private void removeBookmarkTrackingIcons() {
596 if (trackingIcons!=null) {
597 for (Iterator i=trackingIcons.iterator(); i.hasNext(); ) {
598 GutterIconImpl ti = (GutterIconImpl)i.next();
599 if (ti.getIcon()==bookmarkIcon) {
600 i.remove();
601 }
602 }
603 }
604 }
605
606
607 /**
608 * Highlights a range of lines in the icon area.
609 *
610 * @param startLine The start of the line range.
611 * @param endLine The end of the line range.
612 * @see #clearActiveLineRange()
613 */
614 public void setActiveLineRange(int startLine, int endLine) {
615 if (startLine!=activeLineRangeStart ||
616 endLine!=activeLineRangeEnd) {
617 activeLineRangeStart = startLine;
618 activeLineRangeEnd = endLine;
619 repaint();
620 }
621 }
622
623
624 /**
625 * Sets the color to use to render active line ranges.
626 *
627 * @param color The color to use. If this is null, then the default
628 * color is used.
629 * @see #getActiveLineRangeColor()
630 * @see Gutter#DEFAULT_ACTIVE_LINE_RANGE_COLOR
631 */
632 public void setActiveLineRangeColor(Color color) {
633 if (color==null) {
634 color = Gutter.DEFAULT_ACTIVE_LINE_RANGE_COLOR;
635 }
636 if (!color.equals(activeLineRangeColor)) {
637 activeLineRangeColor = color;
638 repaint();
639 }
640 }
641
642
643 /**
644 * Sets the icon to use for bookmarks. Any previous bookmark icons
645 * are removed.
646 *
647 * @param icon The new bookmark icon. If this is <code>null</code>,
648 * bookmarking is effectively disabled.
649 * @see #getBookmarkIcon()
650 * @see #isBookmarkingEnabled()
651 */
652 public void setBookmarkIcon(Icon icon) {
653 removeBookmarkTrackingIcons();
654 bookmarkIcon = icon;
655 repaint();
656 }
657
658
659 /**
660 * Sets whether bookmarking is enabled. Note that a bookmarking icon
661 * must be set via {@link #setBookmarkIcon(Icon)} before bookmarks are
662 * truly enabled.
663 *
664 * @param enabled Whether bookmarking is enabled. If this is
665 * <code>false</code>, any bookmark icons are removed.
666 * @see #isBookmarkingEnabled()
667 * @see #setBookmarkIcon(Icon)
668 */
669 public void setBookmarkingEnabled(boolean enabled) {
670 if (enabled!=bookmarkingEnabled) {
671 bookmarkingEnabled = enabled;
672 if (!enabled) {
673 removeBookmarkTrackingIcons();
674 }
675 repaint();
676 }
677 }
678
679
680 /**
681 * Sets the text area being displayed. This will clear any tracking
682 * icons currently displayed.
683 *
684 * @param textArea The text area.
685 */
686 public void setTextArea(RTextArea textArea) {
687 removeAllTrackingIcons();
688 super.setTextArea(textArea);
689 }
690
691
692 /**
693 * Programatically toggles whether there is a bookmark for the specified
694 * line. If bookmarking is not enabled, this method does nothing.
695 *
696 * @param line The line.
697 * @return Whether a bookmark is now at the specified line.
698 * @throws BadLocationException If <code>line</code> is an invalid line
699 * number in the text area.
700 */
701 public boolean toggleBookmark(int line) throws BadLocationException {
702
703 if (!isBookmarkingEnabled() || getBookmarkIcon()==null) {
704 return false;
705 }
706
707 GutterIconInfo[] icons = getTrackingIcons(line);
708 if (icons.length==0) {
709 int offs = textArea.getLineStartOffset(line);
710 addOffsetTrackingIcon(offs, bookmarkIcon);
711 return true;
712 }
713
714 boolean found = false;
715 for (int i=0; i<icons.length; i++) {
716 if (icons[i].getIcon()==bookmarkIcon) {
717 removeTrackingIcon(icons[i]);
718 found = true;
719 // Don't quit, in case they manipulate the document so > 1
720 // bookmark is on a single line (kind of flaky, but it
721 // works...). If they delete all chars in the document,
722 // AbstractDocument gets a little flaky with the returned line
723 // number for viewToModel(), so this is just us trying to save
724 // face a little.
725 }
726 }
727 if (!found) {
728 int offs = textArea.getLineStartOffset(line);
729 addOffsetTrackingIcon(offs, bookmarkIcon);
730 }
731
732 return !found;
733
734 }
735
736
737 /**
738 * Sets our background color to that of standard "panels" in this
739 * LookAndFeel. This is necessary because, otherwise, we'd inherit the
740 * background color of our parent component (the Gutter).
741 */
742 private void updateBackground() {
743 Color bg = UIManager.getColor("Panel.background");
744 if (bg==null) { // UIManager properties aren't guaranteed to exist
745 bg = new JPanel().getBackground();
746 }
747 setBackground(bg);
748 }
749
750
751 /**
752 * {@inheritDoc}
753 */
754 public void updateUI() {
755 super.updateUI(); // Does nothing
756 updateBackground();
757 }
758
759
760 /**
761 * Implementation of the icons rendered.
762 *
763 * @author Robert Futrell
764 * @version 1.0
765 */
766 private static class GutterIconImpl implements GutterIconInfo, Comparable {
767
768 private Icon icon;
769 private Position pos;
770
771 public GutterIconImpl(Icon icon, Position pos) {
772 this.icon = icon;
773 this.pos = pos;
774 }
775
776 public int compareTo(Object o) {
777 if (o instanceof GutterIconImpl) {
778 return pos.getOffset() - ((GutterIconImpl)o).getMarkedOffset();
779 }
780 return -1;
781 }
782
783 public boolean equals(Object o) {
784 return o==this;
785 }
786
787 public Icon getIcon() {
788 return icon;
789 }
790
791 public int getMarkedOffset() {
792 return pos.getOffset();
793 }
794
795 public int hashCode() {
796 return icon.hashCode(); // FindBugs
797 }
798
799 }
800
801
802}
Note: See TracBrowser for help on using the repository browser.