source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rtextarea/FoldIndicator.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.1 KB
Line 
1/*
2 * 10/08/2011
3 *
4 * FoldIndicator.java - Gutter component allowing the user to expand and
5 * collapse folds.
6 *
7 * This library is distributed under a modified BSD license. See the included
8 * RSyntaxTextArea.License.txt file for details.
9 */
10package org.fife.ui.rtextarea;
11
12import java.awt.Color;
13import java.awt.Component;
14import java.awt.Dimension;
15import java.awt.Graphics;
16import java.awt.Insets;
17import java.awt.Point;
18import java.awt.Rectangle;
19import java.awt.event.MouseEvent;
20import java.beans.PropertyChangeEvent;
21import java.beans.PropertyChangeListener;
22import javax.swing.Icon;
23import javax.swing.JToolTip;
24import javax.swing.ToolTipManager;
25import javax.swing.event.DocumentEvent;
26import javax.swing.event.MouseInputAdapter;
27import javax.swing.text.BadLocationException;
28import javax.swing.text.Document;
29import javax.swing.text.Element;
30import javax.swing.text.View;
31
32import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
33import org.fife.ui.rsyntaxtextarea.Token;
34import org.fife.ui.rsyntaxtextarea.focusabletip.TipUtil;
35import org.fife.ui.rsyntaxtextarea.folding.Fold;
36import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
37
38
39/**
40 * Component in the gutter that displays +/- icons to expand and collapse
41 * fold regions in the editor.
42 *
43 * @author Robert Futrell
44 * @version 1.0
45 */
46public class FoldIndicator extends AbstractGutterComponent {
47
48 /**
49 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
50 * each paint.
51 */
52 private Insets textAreaInsets;
53
54 /**
55 * Used in {@link #paintComponent(Graphics)} to prevent reallocation on
56 * each paint.
57 */
58 private Rectangle visibleRect;
59
60 /**
61 * The fold to show the outline line for.
62 */
63 private Fold foldWithOutlineShowing;
64
65 /**
66 * The color to use for fold icon backgrounds, if the default icons
67 * are used.
68 */
69 private Color foldIconBackground;
70
71 /**
72 * The icon used for collapsed folds.
73 */
74 private Icon collapsedFoldIcon;
75
76 /**
77 * The icon used for expanded folds.
78 */
79 private Icon expandedFoldIcon;
80
81 /**
82 * Whether tool tips are displayed showing the contents of collapsed
83 * fold regions.
84 */
85 private boolean showFoldRegionTips;
86
87 /**
88 * The color used to paint fold outlines.
89 */
90 static final Color DEFAULT_FOREGROUND = Color.gray;
91
92 /**
93 * The default color used to paint the "inside" of fold icons.
94 */
95 static final Color DEFAULT_FOLD_BACKGROUND = Color.white;
96
97 /**
98 * Listens for events in this component.
99 */
100 private Listener listener;
101
102 /**
103 * Width of this component.
104 */
105 private static final int WIDTH = 12;
106
107
108 public FoldIndicator(RTextArea textArea) {
109 super(textArea);
110 setForeground(DEFAULT_FOREGROUND);
111 setFoldIconBackground(DEFAULT_FOLD_BACKGROUND);
112 collapsedFoldIcon = new FoldIcon(true);
113 expandedFoldIcon = new FoldIcon(false);
114 listener = new Listener(this);
115 visibleRect = new Rectangle();
116 setShowCollapsedRegionToolTips(true);
117 }
118
119
120 /**
121 * Overridden to use the editor's background if it's detected that the
122 * user isn't using white as the editor bg, but the system's tool tip
123 * background is yellow-ish.
124 *
125 * @return The tool tip.
126 */
127 public JToolTip createToolTip() {
128 JToolTip tip = super.createToolTip();
129 Color textAreaBG = textArea.getBackground();
130 if (textAreaBG!=null && !Color.white.equals(textAreaBG)) {
131 Color bg = TipUtil.getToolTipBackground();
132 // If current L&F's tool tip color is close enough to "yellow",
133 // and we're not using the default text background of white, use
134 // the editor background as the tool tip background.
135 if (bg.getRed()>=240 && bg.getGreen()>=240 && bg.getBlue()>=200) {
136 tip.setBackground(textAreaBG);
137 }
138 }
139 return tip;
140 }
141
142
143 private Fold findOpenFoldClosestTo(Point p) {
144
145 Fold fold = null;
146
147 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
148 if (rsta.isCodeFoldingEnabled()) { // Should always be true
149 int offs = rsta.viewToModel(p); // TODO: Optimize me
150 if (offs>-1) {
151 try {
152 int line = rsta.getLineOfOffset(offs);
153 FoldManager fm = rsta.getFoldManager();
154 fold = fm.getFoldForLine(line);
155 if (fold==null) {
156 fold = fm.getDeepestOpenFoldContaining(offs);
157 }
158 } catch (BadLocationException ble) {
159 ble.printStackTrace(); // Never happens
160 }
161 }
162 }
163
164 return fold;
165
166 }
167
168
169 /**
170 * Returns the color to use for the "background" of fold icons. This
171 * is be ignored if custom icons are used.
172 *
173 * @return The background color.
174 * @see #setFoldIconBackground(Color)
175 */
176 public Color getFoldIconBackground() {
177 return foldIconBackground;
178 }
179
180
181 public Dimension getPreferredSize() {
182 int h = textArea!=null ? textArea.getHeight() : 100; // Arbitrary
183 return new Dimension(WIDTH, h);
184 }
185
186
187 /**
188 * Returns whether tool tips are displayed showing the contents of
189 * collapsed fold regions when the mouse hovers over a +/- icon.
190 *
191 * @return Whether these tool tips are displayed.
192 * @see #setShowCollapsedRegionToolTips(boolean)
193 */
194 public boolean getShowCollapsedRegionToolTips() {
195 return showFoldRegionTips;
196 }
197
198
199 /**
200 * Positions tool tips to be aligned in the text component, so that the
201 * displayed content is shown (almost) exactly where it would be in the
202 * editor.
203 *
204 * @param e The mouse location.
205 */
206 public Point getToolTipLocation(MouseEvent e) {
207 //return super.getToolTipLocation(e);
208 Point p = e.getPoint();
209 p.y = (p.y/textArea.getLineHeight()) * textArea.getLineHeight();
210 p.x = getWidth();
211 return p;
212 }
213
214
215 /**
216 * Overridden to show the content of a collapsed fold on mouse-overs.
217 *
218 * @param e The mouse location.
219 */
220 public String getToolTipText(MouseEvent e) {
221
222 String text = null;
223
224 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
225 if (rsta.isCodeFoldingEnabled()) {
226 FoldManager fm = rsta.getFoldManager();
227 int pos = rsta.viewToModel(new Point(0, e.getY()));
228 if (pos>=0) { // Not -1
229 int line = 0;
230 try {
231 line = rsta.getLineOfOffset(pos);
232 } catch (BadLocationException ble) {
233 ble.printStackTrace(); // Never happens
234 return null;
235 }
236 Fold fold = fm.getFoldForLine(line);
237 if (fold!=null && fold.isCollapsed()) {
238
239 int endLine = fold.getEndLine();
240 if (fold.getLineCount()>25) { // Not too big
241 endLine = fold.getStartLine() + 25;
242 }
243
244 StringBuffer sb = new StringBuffer("<html><nobr>");
245 while (line<=endLine && line<rsta.getLineCount()) { // Sanity
246 Token t = rsta.getTokenListForLine(line);
247 while (t!=null && t.isPaintable()) {
248 t.appendHTMLRepresentation(sb, rsta, true, true);
249 t = t.getNextToken();
250 }
251 sb.append("<br>");
252 line++;
253 }
254
255 text = sb.toString();
256
257 }
258 }
259 }
260
261 return text;
262
263 }
264
265
266 void handleDocumentEvent(DocumentEvent e) {
267 int newLineCount = textArea.getLineCount();
268 if (newLineCount!=currentLineCount) {
269 currentLineCount = newLineCount;
270 repaint();
271 }
272 }
273
274
275 void lineHeightsChanged() {
276 // TODO Auto-generated method stub
277 }
278
279
280 protected void paintComponent(Graphics g) {
281
282 if (textArea==null) {
283 return;
284 }
285
286 visibleRect = g.getClipBounds(visibleRect);
287 if (visibleRect==null) { // ???
288 visibleRect = getVisibleRect();
289 }
290 //System.out.println("IconRowHeader repainting: " + visibleRect);
291 if (visibleRect==null) {
292 return;
293 }
294
295 Color bg = getBackground();
296 if (getGutter()!=null) { // Should always be true
297 bg = getGutter().getBackground();
298 }
299 g.setColor(bg);
300 g.fillRect(0,visibleRect.y, getWidth(),visibleRect.height);
301
302 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
303 if (!rsta.isCodeFoldingEnabled()) {
304 return; // We should be hidden in this case, but still...
305 }
306
307 if (textArea.getLineWrap()) {
308 paintComponentWrapped(g);
309 return;
310 }
311
312 // Get where to start painting (top of the row).
313 // We need to be "scrolled up" up just enough for the missing part of
314 // the first line.
315 int cellHeight = textArea.getLineHeight();
316 int topLine = visibleRect.y/cellHeight;
317 int y = topLine*cellHeight +
318 (cellHeight-collapsedFoldIcon.getIconHeight())/2;
319 textAreaInsets = textArea.getInsets(textAreaInsets);
320 if (textAreaInsets!=null) {
321 y += textAreaInsets.top;
322 }
323
324 // Get the first and last lines to paint.
325 FoldManager fm = rsta.getFoldManager();
326 topLine += fm.getHiddenLineCountAbove(topLine, true);
327
328 int width = getWidth();
329 int x = width - 10;
330 int line = topLine;
331 boolean paintingOutlineLine = foldWithOutlineShowing!=null &&
332 foldWithOutlineShowing.containsLine(line);
333
334 while (y<visibleRect.y+visibleRect.height) {
335 if (paintingOutlineLine) {
336 g.setColor(getForeground());
337 int w2 = width/2;
338 if (line==foldWithOutlineShowing.getEndLine()) {
339 int y2 = y+cellHeight/2;
340 g.drawLine(w2,y, w2,y2);
341 g.drawLine(w2,y2, width-2,y2);
342 paintingOutlineLine = false;
343 }
344 else {
345 g.drawLine(w2,y, w2,y+cellHeight);
346 }
347 }
348 Fold fold = fm.getFoldForLine(line);
349 if (fold!=null) {
350 if (fold==foldWithOutlineShowing && !fold.isCollapsed()) {
351 g.setColor(getForeground());
352 int w2 = width/2;
353 g.drawLine(w2,y+cellHeight/2, w2,y+cellHeight);
354 paintingOutlineLine = true;
355 }
356 if (fold.isCollapsed()) {
357 collapsedFoldIcon.paintIcon(this, g, x, y);
358 // Skip to next line to paint, taking extra care for lines with
359 // block ends and begins together, e.g. "} else {"
360 do {
361 int hiddenLineCount = fold.getLineCount();
362 if (hiddenLineCount==0) {
363 // Fold parser identified a zero-line fold region.
364 // This is really a bug, but we'll be graceful here
365 // and avoid an infinite loop.
366 break;
367 }
368 line += hiddenLineCount;
369 fold = fm.getFoldForLine(line);
370 } while (fold!=null && fold.isCollapsed());
371 }
372 else {
373 expandedFoldIcon.paintIcon(this, g, x, y);
374 }
375 }
376 line++;
377 y += cellHeight;
378 }
379
380 }
381
382
383 /**
384 * Paints folding icons when line wrapping is enabled.
385 *
386 * @param g The graphics context.
387 */
388 private void paintComponentWrapped(Graphics g) {
389
390 // The variables we use are as follows:
391 // - visibleRect is the "visible" area of the text area; e.g.
392 // [0,100, 300,100+(lineCount*cellHeight)-1].
393 // actualTop.y is the topmost-pixel in the first logical line we
394 // paint. Note that we may well not paint this part of the logical
395 // line, as it may be broken into many physical lines, with the first
396 // few physical lines scrolled past. Note also that this is NOT the
397 // visible rect of this line number list; this line number list has
398 // visible rect == [0,0, insets.left-1,visibleRect.height-1].
399 // - offset (<=0) is the y-coordinate at which we begin painting when
400 // we begin painting with the first logical line. This can be
401 // negative, signifying that we've scrolled past the actual topmost
402 // part of this line.
403
404 // The algorithm is as follows:
405 // - Get the starting y-coordinate at which to paint. This may be
406 // above the first visible y-coordinate as we're in line-wrapping
407 // mode, but we always paint entire logical lines.
408 // - Paint that line's indicator, if appropriate. Increment y to be
409 // just below the are we just painted (i.e., the beginning of the
410 // next logical line's view area).
411 // - Get the ending visual position for that line. We can now loop
412 // back, paint this line, and continue until our y-coordinate is
413 // past the last visible y-value.
414
415 // We avoid using modelToView/viewToModel where possible, as these
416 // methods trigger a parsing of the line into syntax tokens, which is
417 // costly. It's cheaper to just grab the child views' bounds.
418
419 // Some variables we'll be using.
420 int width = getWidth();
421
422 RTextAreaUI ui = (RTextAreaUI)textArea.getUI();
423 View v = ui.getRootView(textArea).getView(0);
424 Document doc = textArea.getDocument();
425 Element root = doc.getDefaultRootElement();
426 int topPosition = textArea.viewToModel(
427 new Point(visibleRect.x,visibleRect.y));
428 int topLine = root.getElementIndex(topPosition);
429 int cellHeight = textArea.getLineHeight();
430 FoldManager fm = ((RSyntaxTextArea)textArea).getFoldManager();
431
432 // Compute the y at which to begin painting text, taking into account
433 // that 1 logical line => at least 1 physical line, so it may be that
434 // y<0. The computed y-value is the y-value of the top of the first
435 // (possibly) partially-visible view.
436 Rectangle visibleEditorRect = ui.getVisibleEditorRect();
437 Rectangle r = LineNumberList.getChildViewBounds(v, topLine,
438 visibleEditorRect);
439 int y = r.y;
440 y += (cellHeight-collapsedFoldIcon.getIconHeight())/2;
441
442 int visibleBottom = visibleRect.y + visibleRect.height;
443 int x = width - 10;
444 int line = topLine;
445 boolean paintingOutlineLine = foldWithOutlineShowing!=null &&
446 foldWithOutlineShowing.containsLine(line);
447 int lineCount = root.getElementCount();
448
449 while (y<visibleBottom && line<lineCount) {
450
451 int curLineH = LineNumberList.getChildViewBounds(v, line,
452 visibleEditorRect).height;
453
454 if (paintingOutlineLine) {
455 g.setColor(getForeground());
456 int w2 = width/2;
457 if (line==foldWithOutlineShowing.getEndLine()) {
458 int y2 = y + curLineH - cellHeight/2;
459 g.drawLine(w2,y, w2,y2);
460 g.drawLine(w2,y2, width-2,y2);
461 paintingOutlineLine = false;
462 }
463 else {
464 g.drawLine(w2,y, w2,y+curLineH);
465 }
466 }
467 Fold fold = fm.getFoldForLine(line);
468 if (fold!=null) {
469 if (fold==foldWithOutlineShowing && !fold.isCollapsed()) {
470 g.setColor(getForeground());
471 int w2 = width/2;
472 g.drawLine(w2,y+cellHeight/2, w2,y+curLineH);
473 paintingOutlineLine = true;
474 }
475 if (fold.isCollapsed()) {
476 collapsedFoldIcon.paintIcon(this, g, x, y);
477 y += LineNumberList.getChildViewBounds(v, line,
478 visibleEditorRect).height;
479 line += fold.getLineCount() + 1;
480 }
481 else {
482 expandedFoldIcon.paintIcon(this, g, x, y);
483 y += curLineH;
484 line++;
485 }
486 }
487 else {
488 y += curLineH;
489 line++;
490 }
491 }
492
493 }
494
495
496 private int rowAtPoint(Point p) {
497
498 int line = 0;
499
500 try {
501 int offs = textArea.viewToModel(p);
502 if (offs>-1) {
503 line = textArea.getLineOfOffset(offs);
504 }
505 } catch (BadLocationException ble) {
506 ble.printStackTrace(); // Never happens
507 }
508
509 return line;
510
511 }
512
513
514 /**
515 * Sets the color to use for the "background" of fold icons. This will
516 * be ignored if custom icons are used.
517 *
518 * @param bg The new background color.
519 * @see #getFoldIconBackground()
520 */
521 public void setFoldIconBackground(Color bg) {
522 foldIconBackground = bg;
523 }
524
525
526 /**
527 * Sets the icons to use to represent collapsed and expanded folds.
528 *
529 * @param collapsedIcon The collapsed fold icon. This cannot be
530 * <code>null</code>.
531 * @param expandedIcon The expanded fold icon. This cannot be
532 * <code>null</code>.
533 */
534 public void setFoldIcons(Icon collapsedIcon, Icon expandedIcon) {
535 this.collapsedFoldIcon = collapsedIcon;
536 this.expandedFoldIcon = expandedIcon;
537 revalidate(); // Icons may be different sizes.
538 repaint();
539 }
540
541
542 /**
543 * Toggles whether tool tips should be displayed showing the contents of
544 * collapsed fold regions when the mouse hovers over a +/- icon.
545 *
546 * @param show Whether to show these tool tips.
547 * @see #getShowCollapsedRegionToolTips()
548 */
549 public void setShowCollapsedRegionToolTips(boolean show) {
550 if (show!=showFoldRegionTips) {
551 if (show) {
552 ToolTipManager.sharedInstance().registerComponent(this);
553 }
554 else {
555 ToolTipManager.sharedInstance().unregisterComponent(this);
556 }
557 showFoldRegionTips = show;
558 }
559 }
560
561
562 /**
563 * Overridden so we can track when code folding is enabled/disabled.
564 */
565 public void setTextArea(RTextArea textArea) {
566 if (this.textArea!=null) {
567 this.textArea.removePropertyChangeListener(
568 RSyntaxTextArea.CODE_FOLDING_PROPERTY, listener);
569 }
570 super.setTextArea(textArea);
571 if (this.textArea!=null) {
572 this.textArea.addPropertyChangeListener(
573 RSyntaxTextArea.CODE_FOLDING_PROPERTY, listener);
574 }
575 }
576
577
578 /**
579 * The default +/- icon for expanding and collapsing folds.
580 */
581 private class FoldIcon implements Icon {
582
583 private boolean collapsed;
584
585 public FoldIcon(boolean collapsed) {
586 this.collapsed = collapsed;
587 }
588
589 public int getIconHeight() {
590 return 8;
591 }
592
593 public int getIconWidth() {
594 return 8;
595 }
596
597 public void paintIcon(Component c, Graphics g, int x, int y) {
598 g.setColor(foldIconBackground);
599 g.fillRect(x,y, 8,8);
600 g.setColor(getForeground());
601 g.drawRect(x,y, 8,8);
602 g.drawLine(x+2,y+4, x+2+4,y+4);
603 if (collapsed) {
604 g.drawLine(x+4,y+2, x+4,y+6);
605 }
606 }
607
608 }
609
610
611 /**
612 * Listens for events in this component.
613 */
614 private class Listener extends MouseInputAdapter
615 implements PropertyChangeListener {
616
617 public Listener(FoldIndicator fgc) {
618 fgc.addMouseListener(this);
619 fgc.addMouseMotionListener(this);
620 }
621
622 public void mouseClicked(MouseEvent e) {
623
624// // TODO: Implement code folding with word wrap enabled
625// if (textArea.getLineWrap()) {
626// UIManager.getLookAndFeel().provideErrorFeedback(textArea);
627// return;
628// }
629
630 Point p = e.getPoint();
631 int line = rowAtPoint(p);
632
633 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
634 FoldManager fm = rsta.getFoldManager();
635
636 Fold fold = fm.getFoldForLine(line);
637 if (fold!=null) {
638 fold.toggleCollapsedState();
639 getGutter().repaint();
640 textArea.repaint();
641 }
642
643 }
644
645 public void mouseExited(MouseEvent e) {
646 if (foldWithOutlineShowing!=null) {
647 foldWithOutlineShowing = null;
648 repaint();
649 }
650 }
651
652 public void mouseMoved(MouseEvent e) {
653 Fold newSelectedFold = findOpenFoldClosestTo(e.getPoint());
654 if (newSelectedFold!=foldWithOutlineShowing) {
655 foldWithOutlineShowing = newSelectedFold;
656 repaint();
657 }
658 }
659
660 public void propertyChange(PropertyChangeEvent e) {
661 // Whether folding is enabled in the editor has changed.
662 repaint();
663 }
664
665 }
666
667
668}
Note: See TracBrowser for help on using the repository browser.