001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2001 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013:
014: package org.netbeans.editor;
015:
016: import java.awt.Color;
017: import java.awt.Dimension;
018: import java.awt.Font;
019: import java.awt.FontMetrics;
020: import java.awt.Graphics;
021: import java.awt.Image;
022: import java.awt.Insets;
023: import java.awt.Rectangle;
024: import java.awt.Toolkit;
025: import java.awt.event.ActionEvent;
026: import java.awt.event.InputEvent;
027: import java.awt.event.MouseAdapter;
028: import java.awt.event.MouseEvent;
029: import java.awt.event.MouseMotionListener;
030: import java.awt.image.ImageObserver;
031: import java.beans.PropertyChangeEvent;
032: import java.beans.PropertyChangeListener;
033: import java.net.MalformedURLException;
034: import java.net.URL;
035:
036: import javax.accessibility.Accessible;
037: import javax.accessibility.AccessibleContext;
038: import javax.accessibility.AccessibleRole;
039: import javax.swing.Action;
040: import javax.swing.JComponent;
041: import javax.swing.JPopupMenu;
042: import javax.swing.event.PopupMenuEvent;
043: import javax.swing.event.PopupMenuListener;
044: import javax.swing.text.BadLocationException;
045:
046: /**
047: * GlyphGutter is component for displaying line numbers and annotation glyph
048: * icons. Component also allow to "cycle" through the annotations. It means that
049: * if there is more than one annotation on the line, only one of them might be
050: * visible. And clicking the special cycling button in the gutter the user can
051: * cycle through the annotations.
052: *
053: * @author David Konecny
054: * @since 07/2001
055: */
056:
057: public class GlyphGutter extends JComponent implements
058: Annotations.AnnotationsListener, Accessible {
059:
060: /** EditorUI which part this gutter is */
061: private EditorUI editorUI;
062:
063: /** Document to which this gutter is attached */
064: private BaseDocument doc;
065:
066: /** Annotations manager responsible for annotations for this line */
067: private Annotations annos;
068:
069: /** Cycling button image */
070: private Image gutterButton;
071:
072: /** Backroung color of the gutter */
073: private Color backgroundColor;
074:
075: /** Foreground color of the gutter. Used for drawing line numbers. */
076: private Color foreColor;
077:
078: /** Font used for drawing line numbers */
079: private Font font;
080:
081: /** Height of the line as it was calculated in EditorUI. */
082: private int lineHeight;
083:
084: /**
085: * Flag whther the gutter was initialized or not. The painting is disabled
086: * till the gutter is not initialized
087: */
088: private boolean init;
089:
090: /**
091: * Width of the column used for drawing line numbers. The value contains
092: * also line number margins.
093: */
094: private int numberWidth;
095:
096: /** Predefined width of the glyph icons */
097: private final static int glyphWidth = 16;
098:
099: /** Preddefined width of the cycling button */
100: private final static int glyphButtonWidth = 9;
101:
102: /** Whether the line numbers are shown or not */
103: private boolean showLineNumbers = true;
104:
105: /** Image observer used for glyph icons */
106: private ImageObserver imgObserver = null;
107:
108: /**
109: * The gutter height is enlarged by number of lines which specifies this
110: * constant
111: */
112: private static final int ENLARGE_GUTTER_HEIGHT = 300;
113:
114: /**
115: * The hightest line number. This value is used for calculating width of the
116: * gutter
117: */
118: private int highestLineNumber = 0;
119:
120: /** Whether the annotation glyph can be drawn over the line numbers */
121: private boolean drawOverLineNumbers = false;
122:
123: /*
124: * These two variables are used for caching of count of line annos on the
125: * line over which is the mouse caret. Just for sake of optimalization.
126: */
127: private int cachedCountOfAnnos = -1;
128: private int cachedCountOfAnnosForLine = -1;
129:
130: /** Property change listener on AnnotationTypes changes */
131: private PropertyChangeListener annoTypesListener;
132:
133: public GlyphGutter(EditorUI editorUI) {
134: super ();
135: this .editorUI = editorUI;
136: init = false;
137: doc = editorUI.getDocument();
138: annos = doc.getAnnotations();
139:
140: // Annotations class is model for this view, so the listener on changes
141: // in
142: // Annotations must be added here
143: annos.addAnnotationsListener(this );
144:
145: // do initialization
146: init();
147: update();
148: }
149:
150: /*
151: * Read accessible context @return - accessible context
152: */
153: public AccessibleContext getAccessibleContext() {
154: if (accessibleContext == null) {
155: accessibleContext = new AccessibleJComponent() {
156: public AccessibleRole getAccessibleRole() {
157: return AccessibleRole.PANEL;
158: }
159: };
160: }
161: return accessibleContext;
162: }
163:
164: /** Do initialization of the glyph gutter */
165: protected void init() {
166: URL imageURL = null;
167:
168: try {
169: // cycling button
170: imageURL = new URL(
171: "nbresloc:/org/netbeans/editor/resources/glyphbutton.gif"); // NOI18N
172: } catch (MalformedURLException ex) {
173: if (Boolean.getBoolean("netbeans.debug.exceptions")) // NOI18N
174: ex.printStackTrace();
175: return;
176: }
177:
178: if (imageURL != null)
179: gutterButton = Toolkit.getDefaultToolkit().getImage(
180: imageURL);
181:
182: setToolTipText("");
183: getAccessibleContext().setAccessibleName(
184: LocaleSupport.getString("ACSN_Glyph_Gutter")); // NOI18N
185: getAccessibleContext().setAccessibleDescription(
186: LocaleSupport.getString("ACSD_Glyph_Gutter")); // NOI18N
187:
188: // add mouse listener for cycling button
189: // TODO: clicking the line number should select whole line
190: // TODO: clicking the line number abd dragging the mouse should select
191: // block of lines
192: GutterMouseListener gutterMouseListener = new GutterMouseListener();
193: addMouseListener(gutterMouseListener);
194: addMouseMotionListener(gutterMouseListener);
195:
196: // after the glyph icons are loaded it is necessary to repaint the
197: // gutter
198: imgObserver = new ImageObserver() {
199: public boolean imageUpdate(Image img, int infoflags, int x,
200: int y, int width, int height) {
201: if ((infoflags & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
202: repaint();
203: return true;
204: }
205: return true;
206: }
207: };
208:
209: AnnotationTypes.getTypes().addPropertyChangeListener(
210: annoTypesListener = new PropertyChangeListener() {
211: public void propertyChange(PropertyChangeEvent evt) {
212: if (evt.getPropertyName() == AnnotationTypes.PROP_GLYPHS_OVER_LINE_NUMBERS
213: || evt.getPropertyName() == AnnotationTypes.PROP_SHOW_GLYPH_GUTTER) {
214: update();
215: }
216: }
217: });
218:
219: }
220:
221: /**
222: * Update colors, fonts, sizes and invalidate itself. This method is called
223: * from EditorUI.update()
224: */
225: public void update() {
226: Coloring lineColoring = (Coloring) editorUI.getColoringMap()
227: .get(SettingsNames.LINE_NUMBER_COLORING);
228: Coloring defaultColoring = (Coloring) editorUI
229: .getDefaultColoring();
230:
231: // fix for issue #16940
232: // the real cause of this problem is that closed document is not garbage
233: // collected,
234: // because of *some* references (see #16072) and so any change in
235: // AnnotationTypes.PROP_*
236: // properties is fired which must update this component although it is
237: // not visible anymore
238: if (lineColoring == null)
239: return;
240:
241: if (lineColoring.getBackColor() != null)
242: backgroundColor = lineColoring.getBackColor();
243: else
244: backgroundColor = defaultColoring.getBackColor();
245:
246: if (lineColoring.getForeColor() != null)
247: foreColor = lineColoring.getForeColor();
248: else
249: foreColor = defaultColoring.getForeColor();
250:
251: if (lineColoring.getFont() != null)
252: font = lineColoring.getFont();
253: else
254: font = defaultColoring.getFont();
255:
256: lineHeight = editorUI.getLineHeight();
257:
258: showLineNumbers = editorUI.lineNumberVisibleSetting;
259:
260: drawOverLineNumbers = AnnotationTypes.getTypes()
261: .isGlyphsOverLineNumbers().booleanValue();
262:
263: init = true;
264:
265: // initialize the value with current number of lines
266: highestLineNumber = getLineCount();
267:
268: repaint();
269: resize();
270: }
271:
272: protected void resize() {
273: Dimension dim = new Dimension();
274: dim.width = getWidthDimension();
275: dim.height = getHeightDimension();
276:
277: // enlarge the gutter so that inserting new lines into
278: // document does not cause resizing too often
279: dim.height += ENLARGE_GUTTER_HEIGHT * lineHeight;
280:
281: numberWidth = getLineNumberWidth();
282: if (!showLineNumbers)
283: numberWidth = 0;
284:
285: setPreferredSize(dim);
286:
287: revalidate();
288: }
289:
290: /** Return number of lines in the document */
291: protected int getLineCount() {
292: int lineCnt;
293: try {
294: lineCnt = Utilities.getLineOffset(doc, doc.getLength()) + 1;
295: } catch (BadLocationException e) {
296: lineCnt = 1;
297: }
298: return lineCnt;
299: }
300:
301: /** Gets number of digits in the number */
302: protected int getDigitCount(int number) {
303: return Integer.toString(number).length();
304: }
305:
306: protected int getLineNumberWidth() {
307: int newWidth = 0;
308: Insets insets = editorUI.getLineNumberMargin();
309: if (insets != null) {
310: newWidth += insets.left + insets.right;
311: }
312: newWidth += getDigitCount(highestLineNumber)
313: * editorUI.getLineNumberDigitWidth();
314: return newWidth;
315: }
316:
317: protected int getWidthDimension() {
318: int newWidth = 0;
319:
320: if (annos.isGlyphColumn()
321: || AnnotationTypes.getTypes().isShowGlyphGutter()
322: .booleanValue())
323: newWidth += glyphWidth;
324:
325: if (annos.isGlyphButtonColumn())
326: newWidth += glyphButtonWidth;
327:
328: if (showLineNumbers) {
329: int lineNumberWidth = getLineNumberWidth();
330: if (drawOverLineNumbers) {
331: if (lineNumberWidth > newWidth)
332: newWidth = lineNumberWidth;
333: } else
334: newWidth += lineNumberWidth;
335: }
336:
337: return newWidth;
338: }
339:
340: protected int getHeightDimension() {
341: JComponent comp = editorUI.getComponent();
342: if (comp == null)
343: return 0;
344: return highestLineNumber * lineHeight
345: + (int) comp.getSize().getHeight();
346: }
347:
348: /** Paint the gutter itself */
349: public void paintComponent(Graphics g) {
350:
351: super .paintComponent(g);
352:
353: // if the gutter was not initialized yet, skip the painting
354: if (!init)
355: return;
356:
357: Rectangle drawHere = g.getClipBounds();
358:
359: // Fill clipping area with dirty brown/orange.
360: g.setColor(backgroundColor);
361: g.fillRect(drawHere.x, drawHere.y, drawHere.width,
362: drawHere.height);
363:
364: // @JMT -- added to netbeans src. Draws a dark vertical line to make the
365: // editor boundary more pronounced
366: g.setColor(Color.black);
367: int e_x = getWidth() - 1;
368: g.drawLine(e_x, drawHere.y, e_x, drawHere.y + drawHere.height);
369:
370: g.setFont(font);
371: g.setColor(foreColor);
372:
373: FontMetrics fm = FontMetricsCache.getFontMetrics(font, this );
374: int rightMargin = 0;
375: Insets margin = editorUI.getLineNumberMargin();
376: if (margin != null)
377: rightMargin = margin.right;
378:
379: // calculate the first line which must be drawn
380: int line = (int) ((float) drawHere.y / (float) lineHeight);
381: if (line > 0)
382: line--;
383:
384: // calculate the Y of the first line
385: int y = line * lineHeight;
386:
387: int lineCount = Integer.MAX_VALUE;
388:
389: if (showLineNumbers) {
390: lineCount = getLineCount();
391: int lastLine = (int) ((float) (drawHere.y + drawHere.height) / (float) lineHeight) + 1;
392: if (lastLine > highestLineNumber) {
393: int prevHighest = highestLineNumber;
394: highestLineNumber = lastLine;
395: if (getDigitCount(highestLineNumber) > getDigitCount(prevHighest)) {
396: resize();
397: return;
398: }
399: }
400: }
401:
402: // find the nearest visible line with an annotation
403: int lineWithAnno = annos.getNextLineWithAnnotation(line);
404:
405: // draw liune numbers and annotations while we are in visible area
406: // "+(lineHeight/2)" means to don't draw less than half of the line
407: // number
408: while ((y + (lineHeight / 2)) <= (drawHere.y + drawHere.height)) {
409: // draw line numbers if they are turned on
410: if (showLineNumbers
411: && ((!drawOverLineNumbers) || (drawOverLineNumbers && line != lineWithAnno))) {
412: if (line < lineCount) {
413: int lineNumberWidth = fm.stringWidth(""
414: + (line + 1));
415: g.drawString("" + (line + 1), numberWidth
416: - lineNumberWidth - rightMargin, y
417: + editorUI.getLineAscent());
418: }
419: }
420:
421: // draw anotation if we get to the line with some annotation
422: if (line == lineWithAnno) {
423:
424: int count = annos.getNumberOfAnnotations(line);
425: AnnotationDesc anno = annos.getActiveAnnotation(line);
426:
427: int xPos = numberWidth;
428: if (drawOverLineNumbers) {
429: xPos = getWidth() - glyphWidth;
430: if (count > 1)
431: xPos -= glyphButtonWidth;
432: }
433:
434: if (anno != null) {
435: // draw the glyph only when the annotation type has its own
436: // icon (no the default one)
437: // or in case there is more than one annotations on the line
438: if (!(count == 1 && anno.isDefaultGlyph())) {
439: if (prepareImage(anno.getGlyph(), imgObserver))
440: g.drawImage(anno.getGlyph(), xPos, y
441: + (lineHeight - anno.getGlyph()
442: .getHeight(null)) / 2 + 1,
443: null);
444: }
445: }
446:
447: // draw cycling button if there is more than one annotations on
448: // the line
449: if (count > 1)
450: if (prepareImage(gutterButton, imgObserver)
451: && prepareImage(anno.getGlyph(),
452: imgObserver))
453: g.drawImage(gutterButton, xPos + glyphWidth, y
454: + (lineHeight - anno.getGlyph()
455: .getHeight(null)) / 2, null);
456:
457: // update the value with next line with some anntoation
458: lineWithAnno = annos
459: .getNextLineWithAnnotation(line + 1);
460: }
461:
462: y += lineHeight;
463: line++;
464: }
465: }
466:
467: /** Data for the line has changed and the line must be redraw. */
468: public void changedLine(int line) {
469:
470: if (!init)
471: return;
472:
473: // reset cache if there was some change
474: cachedCountOfAnnos = -1;
475:
476: // redraw also lines around - three lines will be redrawn
477: if (line > 0)
478: line--;
479: int y = line * lineHeight;
480:
481: repaint(0, y, (int) getSize().getWidth(), 3 * lineHeight);
482: checkSize();
483: }
484:
485: /** Repaint whole gutter. */
486: public void changedAll() {
487:
488: if (!init)
489: return;
490:
491: // reset cache if there was some change
492: cachedCountOfAnnos = -1;
493:
494: int lineCnt;
495: try {
496: lineCnt = Utilities.getLineOffset(doc, doc.getLength()) + 1;
497: } catch (BadLocationException e) {
498: lineCnt = 1;
499: }
500:
501: repaint();
502: checkSize();
503: }
504:
505: /** Check whether it is not necessary to resize the gutter */
506: protected void checkSize() {
507: int count = getLineCount();
508: if (count > highestLineNumber) {
509: highestLineNumber = count;
510: }
511: Dimension dim = getPreferredSize();
512: if (getWidthDimension() > dim.width
513: || getHeightDimension() > dim.height) {
514: resize();
515: }
516:
517: }
518:
519: /** Get tooltip text for the mouse position */
520: // TODO: does not work for asynchronous tooltip texts
521: public String getToolTipText(MouseEvent e) {
522: int line = (int) ((float) e.getY() / (float) lineHeight);
523: if (annos.getNumberOfAnnotations(line) == 0)
524: return null;
525: if (isMouseOverCycleButton(e)
526: && annos.getNumberOfAnnotations(line) > 1) {
527: return java.text.MessageFormat.format(LocaleSupport
528: .getString("cycling-glyph_tooltip"), // NOI18N
529: new Object[] { new Integer(annos
530: .getNumberOfAnnotations(line)) });
531: } else if (isMouseOverGlyph(e)) {
532: return annos.getActiveAnnotation(line)
533: .getShortDescription();
534: } else
535: return null;
536: }
537:
538: /** Count the X position of the glyph on the line. */
539: private int getXPosOfGlyph(int line) {
540: int xPos = numberWidth;
541: if (drawOverLineNumbers) {
542: xPos = getWidth() - glyphWidth;
543: if (cachedCountOfAnnos == -1
544: || cachedCountOfAnnosForLine != line) {
545: cachedCountOfAnnos = annos.getNumberOfAnnotations(line);
546: cachedCountOfAnnosForLine = line;
547: }
548: if (cachedCountOfAnnos > 1)
549: xPos -= glyphButtonWidth;
550: }
551: return xPos;
552: }
553:
554: /** Check whether the mouse is over some glyph icon or not */
555: private boolean isMouseOverGlyph(MouseEvent e) {
556: int line = (int) ((float) e.getY() / (float) lineHeight);
557: if (e.getX() >= getXPosOfGlyph(line)
558: && e.getX() <= getXPosOfGlyph(line) + glyphWidth)
559: return true;
560: else
561: return false;
562: }
563:
564: /** Check whether the mouse is over the cycling button or not */
565: private boolean isMouseOverCycleButton(MouseEvent e) {
566: int line = (int) ((float) e.getY() / (float) lineHeight);
567: if (e.getX() >= getXPosOfGlyph(line) + glyphWidth
568: && e.getX() <= getXPosOfGlyph(line) + glyphWidth
569: + glyphButtonWidth)
570: return true;
571: else
572: return false;
573: }
574:
575: class GutterMouseListener extends MouseAdapter implements
576: MouseMotionListener {
577:
578: /** start line of the dragging. */
579: private int dragStartLine;
580: /** end line of the dragging. */
581: private int dragEndLine;
582: /** end line of last selection. */
583: private int currentEndLine;
584: /** If true, the selection goes forwards. */
585: private boolean selectForward;
586:
587: public void mouseClicked(MouseEvent e) {
588: // cycling button was clicked by left mouse button
589: if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
590: if (isMouseOverCycleButton(e)) {
591: int line = (int) ((float) e.getY() / (float) lineHeight);
592: annos.activateNextAnnotation(line);
593: } else {
594: Action a = ImplementationProvider.getDefault()
595: .getToggleBreakpointAction();
596: if (a != null) {
597: int line = (int) ((float) e.getY() / (float) lineHeight);
598: int currentLine = -1;
599: try {
600: currentLine = Utilities.getLineOffset(doc,
601: editorUI.getComponent().getCaret()
602: .getDot());
603: } catch (BadLocationException ex) {
604: }
605: if (line != currentLine) {
606: int offset = Utilities
607: .getRowStartFromLineOffset(doc,
608: line);
609: JumpList.checkAddEntry();
610: editorUI.getComponent().getCaret().setDot(
611: offset);
612: }
613: a.actionPerformed(new ActionEvent(editorUI
614: .getComponent(), 0, ""));
615: }
616: }
617: }
618:
619: // annotation glyph was clicked by right mouse button
620: if ((e.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) {
621: int line = (int) ((float) e.getY() / (float) lineHeight);
622: int offset;
623: if (annos.getActiveAnnotation(line) != null)
624: offset = annos.getActiveAnnotation(line)
625: .getOffset();
626: else
627: offset = Utilities.getRowStartFromLineOffset(doc,
628: line);
629: if (editorUI.getComponent().getCaret().getDot() != offset)
630: JumpList.checkAddEntry();
631: editorUI.getComponent().getCaret().setDot(offset);
632: JPopupMenu pm = annos.createPopupMenu(Utilities
633: .getKit(editorUI.getComponent()), line);
634: if (pm != null) {
635: pm.show(GlyphGutter.this , e.getX(), e.getY());
636: }
637: pm.addPopupMenuListener(new PopupMenuListener() {
638: public void popupMenuCanceled(PopupMenuEvent e2) {
639: editorUI.getComponent().requestFocus();
640: }
641:
642: public void popupMenuWillBecomeInvisible(
643: PopupMenuEvent e2) {
644: editorUI.getComponent().requestFocus();
645: }
646:
647: public void popupMenuWillBecomeVisible(
648: PopupMenuEvent e2) {
649: }
650: });
651: }
652: }
653:
654: public void mousePressed(MouseEvent e) {
655: // "click gutter selects line" functionality was disabled
656: // // only react when it is not a cycling button
657: // if ((e.getModifiers() & InputEvent.BUTTON1_MASK) ==
658: // InputEvent.BUTTON1_MASK) {
659: // if (! isMouseOverCycleButton(e)) {
660: // dragStartLine = (int)( (float)e.getY() / (float)lineHeight );
661: // updateSelection (true);
662: // }
663: // }
664: }
665:
666: public void mouseDragged(MouseEvent e) {
667: // "click gutter selects line" functionality was disabled
668: // dragEndLine = (int)( (float)e.getY() / (float)lineHeight );
669: // updateSelection (false);
670: }
671:
672: public void mouseMoved(MouseEvent e) {
673: }
674:
675: /** Updates the selection */
676: private void updateSelection(boolean newSelection) {
677: javax.swing.text.JTextComponent comp = Utilities
678: .getLastActiveComponent();
679: try {
680: if (newSelection) {
681: selectForward = true;
682: // try to get the startOffset. In case of -1 it is most
683: // likely the end of the document
684: int rowStart = Utilities.getRowStartFromLineOffset(
685: doc, dragStartLine);
686: if (rowStart < 0) {
687: rowStart = Utilities.getRowStart(doc, doc
688: .getLength());
689: dragStartLine = Utilities.getLineOffset(doc,
690: rowStart);
691: }
692: comp.setCaretPosition(rowStart);
693: int offSet = Utilities.getRowEnd(doc, rowStart);
694: if (offSet < doc.getLength()) {
695: offSet = offSet + 1;
696: }
697: comp.moveCaretPosition(offSet);
698: currentEndLine = dragEndLine = dragStartLine;
699: } else {
700: if (currentEndLine == dragEndLine)
701: return;
702: // select backwards
703: if (dragEndLine < dragStartLine) {
704: if (selectForward) {
705: // selection start should be at start of (dragLine +
706: // 1)
707: int offSet = Utilities
708: .getRowStartFromLineOffset(doc,
709: dragStartLine + 1);
710: if (offSet < 0) {
711: offSet = Utilities
712: .getRowEnd(
713: doc,
714: Utilities
715: .getRowStartFromLineOffset(
716: doc,
717: dragStartLine));
718: }
719: comp.setCaretPosition(offSet);
720: selectForward = false;
721: }
722: int rowStart = Utilities
723: .getRowStartFromLineOffset(doc,
724: dragEndLine);
725: if (rowStart < 0)
726: rowStart = 0;
727: comp.moveCaretPosition(rowStart);
728: }
729: // select forwards
730: else {
731: if (!selectForward) {
732: // select start should be at dragStartLine
733: comp.setCaretPosition(Utilities
734: .getRowStartFromLineOffset(doc,
735: dragStartLine));
736: selectForward = true;
737: }
738: // try to get the begin of (endLine + 1)
739: int offSet = Utilities
740: .getRowStartFromLineOffset(doc,
741: dragEndLine + 1);
742: ;
743: // for last line or more -1 is returned, so set to
744: // docLength...
745: if (offSet < 0) {
746: offSet = doc.getLength();
747: }
748: comp.moveCaretPosition(offSet);
749: }
750: }
751: currentEndLine = dragEndLine;
752: } catch (BadLocationException ble) {
753: System.err.println(ble);
754: }
755: }
756: }
757:
758: }
|