source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/ErrorStrip.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: 19.5 KB
Line 
1/*
2 * 08/10/2009
3 *
4 * ErrorStrip.java - A component that can visually show Parser messages (syntax
5 * errors, etc.) in an RSyntaxTextArea.
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.rsyntaxtextarea;
11
12import java.awt.Color;
13import java.awt.Component;
14import java.awt.Cursor;
15import java.awt.Dimension;
16import java.awt.Graphics;
17import java.awt.Rectangle;
18import java.awt.event.MouseAdapter;
19import java.awt.event.MouseEvent;
20import java.beans.PropertyChangeEvent;
21import java.beans.PropertyChangeListener;
22import java.text.MessageFormat;
23import java.util.ArrayList;
24import java.util.HashMap;
25import java.util.Iterator;
26import java.util.List;
27import java.util.Map;
28import java.util.ResourceBundle;
29import javax.swing.JComponent;
30import javax.swing.ToolTipManager;
31import javax.swing.UIManager;
32import javax.swing.event.CaretEvent;
33import javax.swing.event.CaretListener;
34import javax.swing.text.BadLocationException;
35
36import org.fife.ui.rsyntaxtextarea.parser.Parser;
37import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
38import org.fife.ui.rsyntaxtextarea.parser.TaskTagParser.TaskNotice;
39
40
41/**
42 * A component to sit alongside an {@link RSyntaxTextArea} that displays
43 * colored markers for locations of interest (parser errors, marked
44 * occurrences, etc.).<p>
45 *
46 * <code>ErrorStrip</code>s display <code>ParserNotice</code>s from
47 * {@link Parser}s. Currently, the only way to get lines flagged in this
48 * component is to register a <code>Parser</code> on an RSyntaxTextArea and
49 * return <code>ParserNotice</code>s for each line to display an icon for.
50 * The severity of each notice must be at least the threshold set by
51 * {@link #setLevelThreshold(int)} to be displayed in this error strip. The
52 * default threshold is {@link ParserNotice#WARNING}.<p>
53 *
54 * An <code>ErrorStrip</code> can be added to a UI like so:
55 * <pre>
56 * textArea = createTextArea();
57 * textArea.addParser(new MyParser(textArea)); // Identifies lines to display
58 * scrollPane = new RTextScrollPane(textArea, true);
59 * ErrorStrip es = new ErrorStrip(textArea);
60 * JPanel temp = new JPanel(new BorderLayout());
61 * temp.add(scrollPane);
62 * temp.add(es, BorderLayout.LINE_END);
63 * </pre>
64 *
65 * @author Robert Futrell
66 * @version 0.5
67 */
68/*
69 * Possible improvements:
70 * 1. Handle marked occurrence changes separately from parser changes.
71 * For each property change, call a method that removes the notices
72 * being reloaded from the Markers (removing any Markers that are now
73 * "empty").
74 * 2. When 1.4 support is dropped, replace new Integer(int) with
75 * Integer.valueOf(int).
76 */
77public class ErrorStrip extends JComponent {
78
79 /**
80 * The text area.
81 */
82 private RSyntaxTextArea textArea;
83
84 /**
85 * Listens for events in this component.
86 */
87 private Listener listener;
88
89 /**
90 * Whether "marked occurrences" in the text area should be shown in this
91 * error strip.
92 */
93 private boolean showMarkedOccurrences;
94
95 /**
96 * Mapping of colors to brighter colors. This is kept to prevent
97 * unnecessary creation of the same Colors over and over.
98 */
99 private Map brighterColors;
100
101 /**
102 * Only notices of this severity (or worse) will be displayed in this
103 * error strip.
104 */
105 private int levelThreshold;
106
107 /**
108 * Whether the caret marker's location should be rendered.
109 */
110 private boolean followCaret;
111
112 /**
113 * The color to use for the caret marker.
114 */
115 private Color caretMarkerColor;
116
117 /**
118 * Where we paint the caret marker.
119 */
120 private int caretLineY;
121
122 /**
123 * The last location of the caret marker.
124 */
125 private int lastLineY;
126
127 /**
128 * The preferred width of this component.
129 */
130 private static final int PREFERRED_WIDTH = 14;
131
132 private static final String MSG = "org.fife.ui.rsyntaxtextarea.ErrorStrip";
133 private static final ResourceBundle msg = ResourceBundle.getBundle(MSG);
134
135
136 /**
137 * Constructor.
138 *
139 * @param textArea The text area we are examining.
140 */
141 public ErrorStrip(RSyntaxTextArea textArea) {
142 this.textArea = textArea;
143 listener = new Listener();
144 ToolTipManager.sharedInstance().registerComponent(this);
145 setLayout(null); // Manually layout Markers as they can overlap
146 addMouseListener(listener);
147 setShowMarkedOccurrences(true);
148 setLevelThreshold(ParserNotice.WARNING);
149 setFollowCaret(true);
150 setCaretMarkerColor(Color.BLACK);
151 }
152
153
154 /**
155 * Overridden so we only start listening for parser notices when this
156 * component (and presumably the text area) are visible.
157 */
158 public void addNotify() {
159 super.addNotify();
160 textArea.addCaretListener(listener);
161 textArea.addPropertyChangeListener(
162 RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
163 textArea.addPropertyChangeListener(
164 RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
165 textArea.addPropertyChangeListener(
166 RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
167 refreshMarkers();
168 }
169
170
171 /**
172 * Manually manages layout since this component uses no layout manager.
173 */
174 public void doLayout() {
175 for (int i=0; i<getComponentCount(); i++) {
176 Marker m = (Marker)getComponent(i);
177 m.updateLocation();
178 }
179 listener.caretUpdate(null); // Force recalculation of caret line pos
180 }
181
182
183 /**
184 * Returns a "brighter" color.
185 *
186 * @param c The color.
187 * @return A brighter color.
188 */
189 private Color getBrighterColor(Color c) {
190 if (brighterColors==null) {
191 brighterColors = new HashMap(5); // Usually small
192 }
193 Color brighter = (Color)brighterColors.get(c);
194 if (brighter==null) {
195 // Don't use c.brighter() as it doesn't work well for blue, and
196 // also doesn't return something brighter "enough."
197 int r = possiblyBrighter(c.getRed());
198 int g = possiblyBrighter(c.getGreen());
199 int b = possiblyBrighter(c.getBlue());
200 brighter = new Color(r, g, b);
201 brighterColors.put(c, brighter);
202 }
203 return brighter;
204 }
205
206
207 /**
208 * returns the color to use when painting the caret marker.
209 *
210 * @return The caret marker color.
211 * @see #setCaretMarkerColor(Color)
212 */
213 public Color getCaretMarkerColor() {
214 return caretMarkerColor;
215 }
216
217
218 /**
219 * Returns whether the caret's position should be drawn.
220 *
221 * @return Whether the caret's position should be drawn.
222 * @see #setFollowCaret(boolean)
223 */
224 public boolean getFollowCaret() {
225 return followCaret;
226 }
227
228
229 /**
230 * {@inheritDoc}
231 */
232 public Dimension getPreferredSize() {
233 int height = textArea.getPreferredScrollableViewportSize().height;
234 return new Dimension(PREFERRED_WIDTH, height);
235 }
236
237
238 /**
239 * Returns the minimum severity a parser notice must be for it to be
240 * displayed in this error strip. This will be one of the constants
241 * defined in the <code>ParserNotice</code> class.
242 *
243 * @return The minimum severity.
244 * @see #setLevelThreshold(int)
245 */
246 public int getLevelThreshold() {
247 return levelThreshold;
248 }
249
250
251 /**
252 * Returns whether marked occurrences are shown in this error strip.
253 *
254 * @return Whether marked occurrences are shown.
255 * @see #setShowMarkedOccurrences(boolean)
256 */
257 public boolean getShowMarkedOccurrences() {
258 return showMarkedOccurrences;
259 }
260
261
262 /**
263 * {@inheritDoc}
264 */
265 public String getToolTipText(MouseEvent e) {
266 String text = null;
267 int line = yToLine(e.getY());
268 if (line>-1) {
269 text = msg.getString("Line");
270 // TODO: 1.5: Use Integer.valueOf(line)
271 text = MessageFormat.format(text,
272 new Object[] { new Integer(line) });
273 }
274 return text;
275 }
276
277
278 /**
279 * Returns the y-offset in this component corresponding to a line in the
280 * text component.
281 *
282 * @param line The line.
283 * @return The y-offset.
284 * @see #yToLine(int)
285 */
286 private int lineToY(int line) {
287 int h = textArea.getVisibleRect().height;
288 float lineCount = textArea.getLineCount();
289 return (int)((line/lineCount) * h) - 2;
290 }
291
292
293 /**
294 * Overridden to (possibly) draw the caret's position.
295 *
296 * @param g The graphics context.
297 */
298 protected void paintComponent(Graphics g) {
299 super.paintComponent(g);
300 if (caretLineY>-1) {
301 g.setColor(getCaretMarkerColor());
302 g.fillRect(0, caretLineY, getWidth(), 2);
303 }
304 }
305
306
307 /**
308 * Returns a possibly brighter component for a color.
309 *
310 * @param i An RGB component for a color (0-255).
311 * @return A possibly brighter value for the component.
312 */
313 private static final int possiblyBrighter(int i) {
314 if (i<255) {
315 i += (int)((255-i)*0.8f);
316 }
317 return i;
318 }
319
320
321 /**
322 * Refreshes the markers displayed in this error strip.
323 */
324 private void refreshMarkers() {
325
326 removeAll(); // listener is removed in Marker.removeNotify()
327 Map markerMap = new HashMap();
328
329 List notices = textArea.getParserNotices();
330 for (Iterator i=notices.iterator(); i.hasNext(); ) {
331 ParserNotice notice = (ParserNotice)i.next();
332 if (notice.getLevel()<=levelThreshold ||
333 (notice instanceof TaskNotice)) {
334 // 1.5: Use Integer.valueOf(notice.getLine())
335 Integer key = new Integer(notice.getLine());
336 Marker m = (Marker)markerMap.get(key);
337 if (m==null) {
338 m = new Marker(notice);
339 m.addMouseListener(listener);
340 markerMap.put(key, m);
341 add(m);
342 }
343 else {
344 m.addNotice(notice);
345 }
346 }
347 }
348
349 if (getShowMarkedOccurrences() && textArea.getMarkOccurrences()) {
350 List occurrences = textArea.getMarkedOccurrences();
351 for (Iterator i=occurrences.iterator(); i.hasNext(); ) {
352 DocumentRange range = (DocumentRange)i.next();
353 int line = 0;
354 try {
355 line = textArea.getLineOfOffset(range.getStartOffset());
356 } catch (BadLocationException ble) { // Never happens
357 continue;
358 }
359 ParserNotice notice = new MarkedOccurrenceNotice(range);
360 // 1.5: Use Integer.valueOf(notice.getLine())
361 Integer key = new Integer(line);
362 Marker m = (Marker)markerMap.get(key);
363 if (m==null) {
364 m = new Marker(notice);
365 m.addMouseListener(listener);
366 markerMap.put(key, m);
367 add(m);
368 }
369 else {
370 if (!m.containsMarkedOccurence()) {
371 m.addNotice(notice);
372 }
373 }
374 }
375 }
376
377 revalidate();
378 repaint();
379
380 }
381
382
383 /**
384 * {@inheritDoc}
385 */
386 public void removeNotify() {
387 super.removeNotify();
388 textArea.removeCaretListener(listener);
389 textArea.removePropertyChangeListener(
390 RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
391 textArea.removePropertyChangeListener(
392 RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
393 textArea.removePropertyChangeListener(
394 RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
395 }
396
397
398 /**
399 * Sets the color to use when painting the caret marker.
400 *
401 * @param color The new caret marker color.
402 * @see #getCaretMarkerColor()
403 */
404 public void setCaretMarkerColor(Color color) {
405 if (color!=null) {
406 caretMarkerColor = color;
407 listener.caretUpdate(null); // Force repaint
408 }
409 }
410
411
412 /**
413 * Toggles whether the caret's current location should be drawn.
414 *
415 * @param follow Whether the caret's current location should be followed.
416 * @see #getFollowCaret()
417 */
418 public void setFollowCaret(boolean follow) {
419 if (followCaret!=follow) {
420 if (followCaret) {
421 repaint(0,caretLineY, getWidth(),2); // Erase
422 }
423 caretLineY = -1;
424 lastLineY = -1;
425 followCaret = follow;
426 listener.caretUpdate(null); // Possibly repaint
427 }
428 }
429
430
431 /**
432 * Sets the minimum severity a parser notice must be for it to be displayed
433 * in this error strip. This should be one of the constants defined in
434 * the <code>ParserNotice</code> class. The default value is
435 * {@link ParserNotice#WARNING}.
436 *
437 * @param level The new severity threshold.
438 * @see #getLevelThreshold()
439 * @see ParserNotice
440 */
441 public void setLevelThreshold(int level) {
442 levelThreshold = level;
443 if (isDisplayable()) {
444 refreshMarkers();
445 }
446 }
447
448
449 /**
450 * Sets whether marked occurrences are shown in this error strip.
451 *
452 * @param show Whether to show marked occurrences.
453 * @see #getShowMarkedOccurrences()
454 */
455 public void setShowMarkedOccurrences(boolean show) {
456 if (show!=showMarkedOccurrences) {
457 showMarkedOccurrences = show;
458 if (isDisplayable()) { // Skip this when we're first created
459 refreshMarkers();
460 }
461 }
462 }
463
464
465 /**
466 * Returns the line in the text area corresponding to a y-offset in this
467 * component.
468 *
469 * @param y The y-offset.
470 * @return The line.
471 * @see #lineToY(int)
472 */
473 private final int yToLine(int y) {
474 int line = -1;
475 int h = textArea.getVisibleRect().height;
476 if (y<h) {
477 float at = y/(float)h;
478 line = (int)(textArea.getLineCount()*at);
479 }
480 return line;
481 }
482
483
484 /**
485 * Listens for events in the error strip and its markers.
486 */
487 private class Listener extends MouseAdapter
488 implements PropertyChangeListener, CaretListener {
489
490 private Rectangle visibleRect = new Rectangle();
491
492 public void caretUpdate(CaretEvent e) {
493 if (getFollowCaret()) {
494 int line = textArea.getCaretLineNumber();
495 float percent = line / ((float)textArea.getLineCount());
496 textArea.computeVisibleRect(visibleRect);
497 caretLineY = (int)(visibleRect.height*percent);
498 if (caretLineY!=lastLineY) {
499 repaint(0,lastLineY, getWidth(), 2); // Erase old position
500 repaint(0,caretLineY, getWidth(), 2);
501 lastLineY = caretLineY;
502 }
503 }
504 }
505
506 public void mouseClicked(MouseEvent e) {
507
508 Component source = (Component)e.getSource();
509 if (source instanceof Marker) {
510 ((Marker)source).mouseClicked(e);
511 return;
512 }
513
514 int line = yToLine(e.getY());
515 if (line>-1) {
516 try {
517 int offs = textArea.getLineStartOffset(line);
518 textArea.setCaretPosition(offs);
519 } catch (BadLocationException ble) { // Never happens
520 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
521 }
522 }
523
524 }
525
526 public void propertyChange(PropertyChangeEvent e) {
527
528 String propName = e.getPropertyName();
529
530 // If they change whether marked occurrences are visible in editor
531 if (RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY.equals(propName)) {
532 if (getShowMarkedOccurrences()) {
533 refreshMarkers();
534 }
535 }
536
537 // If parser notices changed.
538 else if (RSyntaxTextArea.PARSER_NOTICES_PROPERTY.equals(propName)) {
539 refreshMarkers();
540 }
541
542 // If marked occurrences changed.
543 else if (RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY.
544 equals(propName)) {
545 if (getShowMarkedOccurrences()) {
546 refreshMarkers();
547 }
548 }
549
550 }
551
552 }
553
554
555private static final Color COLOR = new Color(220, 220, 220);
556 /**
557 * A notice that wraps a "marked occurrence."
558 */
559 private class MarkedOccurrenceNotice implements ParserNotice {
560
561 private DocumentRange range;
562
563 public MarkedOccurrenceNotice(DocumentRange range) {
564 this.range = range;
565 }
566
567 public int compareTo(Object o) {
568 return 0; // Value doesn't matter
569 }
570
571 public boolean containsPosition(int pos) {
572 return pos>=range.getStartOffset() && pos<range.getEndOffset();
573 }
574
575 public boolean equals(Object o) {
576 // FindBugs - Define equals() when defining compareTo()
577 return compareTo(o)==0;
578 }
579
580 public Color getColor() {
581 return COLOR;
582 //return textArea.getMarkOccurrencesColor();
583 }
584
585 public int getLength() {
586 return range.getEndOffset() - range.getStartOffset();
587 }
588
589 public int getLevel() {
590 return INFO; // Won't matter
591 }
592
593 public int getLine() {
594 try {
595 return textArea.getLineOfOffset(range.getStartOffset());
596 } catch (BadLocationException ble) {
597 return 0;
598 }
599 }
600
601 public String getMessage() {
602 String text = null;
603 try {
604 String word = textArea.getText(range.getStartOffset(),
605 getLength());
606 text = msg.getString("OccurrenceOf");
607 text = MessageFormat.format(text, new String[] { word });
608 } catch (BadLocationException ble) {
609 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
610 }
611 return text;
612 }
613
614 public int getOffset() {
615 return range.getStartOffset();
616 }
617
618 public Parser getParser() {
619 return null;
620 }
621
622 public boolean getShowInEditor() {
623 return false; // Value doesn't matter
624 }
625
626 public String getToolTipText() {
627 return null;
628 }
629
630 public int hashCode() { // FindBugs, since we override equals()
631 return 0; // Value doesn't matter for us.
632 }
633
634 }
635
636
637 /**
638 * A "marker" in this error strip, representing one or more notices.
639 */
640 private class Marker extends JComponent {
641
642 private List notices;
643
644 public Marker(ParserNotice notice) {
645 notices = new ArrayList(1); // Usually just 1
646 addNotice(notice);
647 setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
648 setSize(getPreferredSize());
649 ToolTipManager.sharedInstance().registerComponent(this);
650 }
651
652 public void addNotice(ParserNotice notice) {
653 notices.add(notice);
654 }
655
656 public boolean containsMarkedOccurence() {
657 boolean result = false;
658 for (int i=0; i<notices.size(); i++) {
659 if (notices.get(i) instanceof MarkedOccurrenceNotice) {
660 result = true;
661 break;
662 }
663 }
664 return result;
665 }
666
667 public Color getColor() {
668 // Return the color for the highest-level parser.
669 Color c = null;
670 int lowestLevel = Integer.MAX_VALUE; // ERROR is 0
671 for (Iterator i=notices.iterator(); i.hasNext(); ) {
672 ParserNotice notice = (ParserNotice)i.next();
673 if (notice.getLevel()<lowestLevel) {
674 lowestLevel = notice.getLevel();
675 c = notice.getColor();
676 }
677 }
678 return c;
679 }
680
681 public Dimension getPreferredSize() {
682 int w = PREFERRED_WIDTH - 4; // 2-pixel empty border
683 return new Dimension(w, 5);
684 }
685
686 public String getToolTipText() {
687
688 String text = null;
689
690 if (notices.size()==1) {
691 text = ((ParserNotice)notices.get(0)).getMessage();
692 }
693 else { // > 1
694 StringBuffer sb = new StringBuffer("<html>");
695 sb.append(msg.getString("MultipleMarkers"));
696 sb.append("<br>");
697 for (int i=0; i<notices.size(); i++) {
698 ParserNotice pn = (ParserNotice)notices.get(i);
699 sb.append("&nbsp;&nbsp;&nbsp;- ");
700 sb.append(pn.getMessage());
701 sb.append("<br>");
702 }
703 text = sb.toString();
704 }
705
706 return text;
707
708 }
709
710 protected void mouseClicked(MouseEvent e) {
711 ParserNotice pn = (ParserNotice)notices.get(0);
712 int offs = pn.getOffset();
713 int len = pn.getLength();
714 if (offs>-1 && len>-1) { // These values are optional
715 textArea.setSelectionStart(offs);
716 textArea.setSelectionEnd(offs+len);
717 }
718 else {
719 int line = pn.getLine();
720 try {
721 offs = textArea.getLineStartOffset(line);
722 textArea.setCaretPosition(offs);
723 } catch (BadLocationException ble) { // Never happens
724 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
725 }
726 }
727 }
728
729 protected void paintComponent(Graphics g) {
730
731 // TODO: Give "priorities" and always pick color of a notice with
732 // highest priority (e.g. parsing errors will usually be red).
733
734 Color borderColor = getColor();
735 if (borderColor==null) {
736 borderColor = Color.DARK_GRAY;
737 }
738 Color fillColor = getBrighterColor(borderColor);
739
740 int w = getWidth();
741 int h = getHeight();
742
743 g.setColor(fillColor);
744 g.fillRect(0,0, w,h);
745
746 g.setColor(borderColor);
747 g.drawRect(0,0, w-1,h-1);
748
749 }
750
751 public void removeNotify() {
752 super.removeNotify();
753 ToolTipManager.sharedInstance().unregisterComponent(this);
754 removeMouseListener(listener);
755 }
756
757 public void updateLocation() {
758 int line = ((ParserNotice)notices.get(0)).getLine();
759 int y = lineToY(line);
760 setLocation(2, y);
761 }
762
763 }
764
765
766}
Note: See TracBrowser for help on using the repository browser.