Source code: jsource/gui/Gutter.java
1 package jsource.gui;
2
3
4 /**
5 * @(#)Gutter.java 12/16/02
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU Library General Public License as published
9 * by the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Library General Public License for more details.
16 */
17 import java.awt.*;
18 import java.awt.event.*;
19 import javax.swing.*;
20
21
22 /**
23 * <code>Gutter</code> is the component that displays line numbers to the left of the text area.
24 * Based on jEdit's Gutter component by Mike Dillon and Slava Pestov.
25 *
26 * @author Panagiotis Plevrakis
27 * <br>Email: pplevrakis@hotmail.com
28 * <br>URL: http://jsource.sourceforge.net
29 */
30 public class Gutter extends JLabel implements SwingConstants {
31
32 /** Internal <code>Gutter</code> color constants */
33 final static Color DEFAULT_BG_COLOUR = new Color(133, 133, 133);
34 final static Color DEFAULT_FG_COLOUR = new Color(209, 209, 209);
35 final static Color DEFAULT_DIVIDER_COLOUR = Color.red;
36 final static Color DEFAULT_CURRENT_LINE_COLOUR = Color.blue;
37 final static Color DEFAULT_INTERVAL_COLOUR = Color.yellow;
38
39 /** A reference to the <code>JSEditor</code> object where the <code>Gutter</code> is attached */
40 private JSEditor editor;
41
42 private JPopupMenu context;
43
44 private int baseline = 0;
45 private int ileft = 0;
46
47 private int mMinWidth;
48
49 private Dimension gutterSize;
50 private Dimension collapsedSize;
51
52 private Color intervalHighlight;
53 private Color currentLineHighlight;
54 private Color dividerColour;
55
56 private FontMetrics fm;
57
58 private int alignment;
59
60 private int interval = 5;
61 private boolean lineNumberingEnabled = true;
62 private boolean currentLineHighlightEnabled = false;
63 private boolean collapsed = false;
64
65 public Gutter(JSEditor editor) {
66 this.editor = editor;
67
68 // minimum width of gutter - enough to display 5 digits
69 mMinWidth = editor.getPainter().getFontMetrics().charWidth('8') * 5;
70 // minimum size of gutter when expanded
71 gutterSize = new Dimension(mMinWidth, 100);
72 // minimum size of gutter when collapsed
73 collapsedSize = new Dimension(10, 100);
74
75 setDoubleBuffered(true);
76
77 MouseHandler ml = new MouseHandler();
78
79 addMouseListener(ml);
80
81 setBackground(DEFAULT_BG_COLOUR);
82 setForeground(DEFAULT_FG_COLOUR);
83 setDividerColour(DEFAULT_DIVIDER_COLOUR);
84 setHighlightedForeground(DEFAULT_INTERVAL_COLOUR);
85 setCurrentLineForeground(DEFAULT_CURRENT_LINE_COLOUR);
86 }
87
88 public void paintComponent(Graphics gfx) {
89 if (!collapsed) {
90 // fill the background
91 Rectangle r = gfx.getClipBounds();
92
93 gfx.setColor(getBackground());
94 gfx.fillRect(r.x, r.y, r.width, r.height);
95
96 // draw the divider on RHS of gutter
97 Color save = gfx.getColor();
98
99 gfx.setColor(dividerColour);
100 gfx.drawLine(gutterSize.width - 3, r.y, gutterSize.width - 3, r.y + r.height);
101 gfx.setColor(save);
102
103 // paint line numbers, if they are enabled
104 if (lineNumberingEnabled) paintLineNumbers(gfx);
105 }
106 }
107
108 public void paint(Graphics gfx) {
109 paintComponent(gfx);
110 }
111
112 private void paintLineNumbers(Graphics gfx) {
113 FontMetrics pfm = editor.getPainter().getFontMetrics();
114 int lineHeight = pfm.getHeight();
115
116 Rectangle clip = gfx.getClipBounds();
117
118 int baseline = editor.lineToY(editor.getFirstLine());
119
120 int firstLine = editor.getFirstLine();
121 int lastLine = firstLine + 1 + clip.height / lineHeight;
122 int caretLine = editor.getCaretLine() + 1;
123
124 int firstValidLine = firstLine > 1 ? firstLine : 1;
125 int lastValidLine = (lastLine > editor.getLineCount())
126 ? editor.getLineCount()
127 : lastLine;
128
129 boolean highlightCurrentLine = currentLineHighlightEnabled
130 && (editor.getSelectionStart() == editor.getSelectionEnd());
131
132 gfx.setFont(editor.getPainter().getFont());
133
134 Color fg = getForeground();
135 Color hfg = getHighlightedForeground();
136 Color clfg = getCurrentLineForeground();
137
138 String number;
139 int offset;
140
141 for (int line = firstLine; line <= lastLine;
142 line++, baseline += lineHeight) {
143 // only print numbers for valid lines
144 if (line < firstValidLine || line > lastValidLine)
145 continue;
146
147 number = Integer.toString(line);
148
149 // if breakpoint at line, highlight in red
150 if (editor.isBreakpoint(line)) {
151 Color save = gfx.getColor();
152
153 gfx.setColor(Color.red);
154 gfx.fillRect(0, baseline - lineHeight, gutterSize.width - 3, lineHeight);
155 gfx.setColor(save);
156 }
157
158 switch (alignment) {
159 case RIGHT:
160 offset = gutterSize.width - collapsedSize.width
161 - (fm.stringWidth(number) + 1);
162 break;
163
164 case CENTER:
165 offset = ((gutterSize.width - collapsedSize.width)
166 - fm.stringWidth(number))
167 / 2;
168 break;
169
170 case LEFT:
171 default:
172 offset = 1;
173 }
174
175 if (line == caretLine && highlightCurrentLine) {
176 gfx.setColor(clfg);
177 } else if (interval > 1 && line % interval == 0) {
178 gfx.setColor(hfg);
179 } else {
180 gfx.setColor(fg);
181 }
182
183 gfx.drawString(number, ileft + offset, baseline);
184 }
185 }
186
187 /**
188 * Marks a line as needing a repaint.
189 * @param line The line to invalidate
190 */
191 public final void invalidateLine(int line) {
192 FontMetrics pfm = editor.getPainter().getFontMetrics();
193
194 repaint(0, editor.lineToY(line) + pfm.getDescent() + pfm.getLeading(),
195 getWidth(), pfm.getHeight());
196 }
197
198 /**
199 * Marks a range of lines as needing a repaint.
200 * @param firstLine The first line to invalidate
201 * @param lastLine The last line to invalidate
202 */
203 public final void invalidateLineRange(int firstLine, int lastLine) {
204 FontMetrics pfm = editor.getPainter().getFontMetrics();
205
206 repaint(0, editor.lineToY(firstLine) + pfm.getDescent() + pfm.getLeading(),
207 getWidth(), (lastLine - firstLine + 1) * pfm.getHeight());
208 }
209
210 /*
211 * <code>JComponent.setFont(Font)</code> is overridden here to cache the baseline for
212 * the font. This avoids having to get the font metrics during every repaint.
213 */
214 public void setFont(Font font) {
215 super.setFont(font);
216
217 fm = getFontMetrics(font);
218 baseline = fm.getAscent();
219 }
220
221 /**
222 * Get the foreground color for highlighted line numbers
223 * @return The highlight color
224 */
225 public Color getHighlightedForeground() {
226 return intervalHighlight;
227 }
228
229 public void setHighlightedForeground(Color highlight) {
230 intervalHighlight = highlight;
231 }
232
233 public Color getCurrentLineForeground() {
234 return currentLineHighlight;
235 }
236
237 public void setCurrentLineForeground(Color highlight) {
238 currentLineHighlight = highlight;
239 }
240
241 /**
242 * Set the width of the expanded gutter
243 * @param width The gutter width
244 */
245 public void setGutterWidth(int width) {
246 if (width < collapsedSize.width) width = collapsedSize.width;
247
248 gutterSize.width = width;
249
250 // if the gutter is expanded, ask the text area to revalidate
251 // the layout to resize the gutter
252 if (!collapsed) editor.revalidate();
253 }
254
255 /**
256 * Get the width of the expanded gutter
257 * @return The gutter width
258 */
259 public int getGutterWidth() {
260 return gutterSize.width;
261 }
262
263 /*
264 * Component.getPreferredSize() is overridden here to support the
265 * collapsing behavior.
266 */
267 public Dimension getPreferredSize() {
268 if (collapsed) {
269 return collapsedSize;
270 } else {
271 return gutterSize;
272 }
273 }
274
275 public Dimension getMinimumSize() {
276 return getPreferredSize();
277 }
278
279 /**
280 * Identifies whether or not the line numbers are drawn in the gutter
281 * @return true if the line numbers are drawn, false otherwise
282 */
283 public boolean isLineNumberingEnabled() {
284 return lineNumberingEnabled;
285 }
286
287 /**
288 * Turns the line numbering on or off and causes the gutter to be
289 * repainted.
290 * @param enabled true if line numbers are drawn, false otherwise
291 */
292 public void setLineNumberingEnabled(boolean enabled) {
293 if (lineNumberingEnabled == enabled) return;
294
295 lineNumberingEnabled = enabled;
296
297 repaint();
298 }
299
300 /**
301 * Identifies whether the horizontal alignment of the line numbers.
302 * @return Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
303 */
304 public int getLineNumberAlignment() {
305 return alignment;
306 }
307
308 /**
309 * Sets the horizontal alignment of the line numbers.
310 * @param alignment Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
311 */
312 public void setLineNumberAlignment(int alignment) {
313 if (this.alignment == alignment) return;
314
315 this.alignment = alignment;
316
317 repaint();
318 }
319
320 /**
321 * Identifies whether the gutter is collapsed or expanded.
322 * @return true if the gutter is collapsed, false if it is expanded
323 */
324 public boolean isCollapsed() {
325 return collapsed;
326 }
327
328 /**
329 * Sets whether the gutter is collapsed or expanded and force the text
330 * area to update its layout if there is a change.
331 * @param collapsed true if the gutter is collapsed,
332 * false if it is expanded
333 */
334 public void setCollapsed(boolean collapsed) {
335 if (this.collapsed == collapsed) return;
336
337 this.collapsed = collapsed;
338
339 editor.revalidate();
340 }
341
342 /**
343 * Toggles whether the gutter is collapsed or expanded.
344 */
345 public void toggleCollapsed() {
346 setCollapsed(!collapsed);
347 }
348
349 /**
350 * Sets the number of lines between highlighted line numbers.
351 * @return The number of lines between highlighted line numbers or
352 * zero if highlighting is disabled
353 */
354 public int getHighlightInterval() {
355 return interval;
356 }
357
358 /**
359 * Sets the number of lines between highlighted line numbers. Any value
360 * less than or equal to one will result in highlighting being disabled.
361 * @param interval The number of lines between highlighted line numbers
362 */
363 public void setHighlightInterval(int interval) {
364 if (interval <= 1) interval = 0;
365 this.interval = interval;
366 repaint();
367 }
368
369 public void setDividerColour(Color c) {
370 dividerColour = c;
371 repaint();
372 }
373
374 public Color getDividerColour() {
375 return dividerColour;
376 }
377
378 public boolean isCurrentLineHighlightEnabled() {
379 return currentLineHighlightEnabled;
380 }
381
382 public void setCurrentLineHighlightEnabled(boolean enabled) {
383 if (currentLineHighlightEnabled == enabled) return;
384
385 currentLineHighlightEnabled = enabled;
386
387 repaint();
388 }
389
390 public JPopupMenu getContextMenu() {
391 return context;
392 }
393
394 public void setContextMenu(JPopupMenu context) {
395 this.context = context;
396 }
397
398 class MouseHandler extends MouseAdapter {
399 public void mouseClicked(MouseEvent e) {
400 if (e.getClickCount() == 2) toggleCollapsed();
401 }
402 }
403 }