Source code: jsource/gui/JSEditor.java
1 package jsource.gui;
2
3
4 /**
5 * JSEditor.java 03/31/03
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 javax.swing.border.*;
18 import javax.swing.event.*;
19 import javax.swing.text.*;
20 import javax.swing.undo.*;
21 import javax.swing.*;
22 import java.awt.datatransfer.*;
23 import java.awt.event.*;
24 import java.awt.*;
25 import java.util.Enumeration;
26 import java.util.Vector;
27 import java.util.HashSet;
28 import jsource.syntax.*;
29 import jsource.syntax.tokenmarker.*;
30 import jsource.util.*;
31
32
33 /**
34 * <code>JSEditor</code> is JSource's text editing component. It is more suited for editing program
35 * source code than JEditorPane, because it drops the unnecessary features (images, variable-width lines,
36 * and so on) and adds a whole bunch of useful goodies such as:
37 * <ul>
38 * <li>More flexible key binding scheme
39 * <li>Supports macro recorders
40 * <li>Rectangular selection
41 * <li>Bracket highlighting
42 * <li>Syntax highlighting
43 * <li>Command repetition
44 * <li>Block caret can be enabled
45 * </ul>
46 *
47 * Based on jEdit's JEditTextArea component by Slava Pestov.
48 *
49 * @author Panagiotis Plevrakis
50 * <br>Email: pplevrakis@hotmail.com
51 * <br>URL: http://jsource.sourceforge.net
52 */
53 public class JSEditor extends JComponent {
54
55 private static String CENTER = "center";
56 private static String RIGHT = "right";
57 private static String BOTTOM = "bottom";
58 private static String LEFT = "left";
59
60 private static JSEditor focusedComponent = null;
61 private static Timer caretTimer = null;
62 private static Timer refreshTimer = null;
63
64 private TextAreaPainter painter = null;
65 private MainFrame main = null;
66
67 private JPopupMenu popup = null;
68
69 private EventListenerList listenerList = null;
70 private MutableCaretEvent caretEvent = null;
71
72 private Gutter mGutter = null;
73 private HashSet mBreakPts = new HashSet();
74
75 private boolean caretBlinks = false;
76 private boolean caretVisible = false;
77 private boolean blink = false;
78
79 private boolean editable = false;
80
81 private int firstLine = 0;
82 private int visibleLines = 0;
83 private int electricScroll = 0;
84
85 private int horizontalOffset = 0;
86
87 private JScrollBar vertical = null;
88 private JScrollBar horizontal = null;
89 private boolean scrollBarsInitialized;
90
91 private InputHandler inputHandler = null;
92 private SyntaxDocument document = null;
93 private DocumentHandler documentHandler = null;
94
95 private Segment lineSegment = null;
96
97 private int selectionStart = 0;
98 private int selectionStartLine = 0;
99 private int selectionEnd = 0;
100 private int selectionEndLine = 0;
101 private boolean biasLeft = false;
102
103 private int bracketPosition = 0;
104 private int bracketLine = 0;
105
106 private int magicCaret = 0;
107 private boolean overwrite = false;
108 private boolean rectSelect = false;
109
110 public boolean modifiedSinceSave = false;
111
112 private boolean indent = false;
113 private String tab = " ";
114 private String lineSep = JSConstants.LINE_SEP;
115
116 /**
117 * Creates a new JSEditor with the default settings.
118 */
119 public JSEditor(MainFrame main) {
120 this(TextAreaDefaults.getDefaults(), main);
121 this.main = main;
122 }
123
124 /**
125 * Creates a new JSEditor with the specified settings.
126 * @param defaults The default settings
127 */
128 public JSEditor(TextAreaDefaults defaults, MainFrame main) {
129 // Enable the necessary events
130 enableEvents(AWTEvent.KEY_EVENT_MASK);
131
132 // Initialize some misc. stuff
133 painter = new TextAreaPainter(this, defaults);
134 documentHandler = new DocumentHandler();
135 listenerList = new EventListenerList();
136 caretEvent = new MutableCaretEvent();
137 lineSegment = new Segment();
138 bracketLine = bracketPosition = -1;
139 blink = true;
140 mGutter = new Gutter(this);
141
142 // Initialize the GUI
143 setLayout(new ScrollLayout());
144 add(LEFT, mGutter);
145 add(CENTER, painter);
146 add(RIGHT, vertical = new JScrollBar(JScrollBar.VERTICAL));
147 add(BOTTOM, horizontal = new JScrollBar(JScrollBar.HORIZONTAL));
148
149 // Add some event listeners
150 vertical.addAdjustmentListener(new AdjustHandler());
151 horizontal.addAdjustmentListener(new AdjustHandler());
152 painter.addComponentListener(new ComponentHandler());
153 painter.addMouseListener(new MouseHandler());
154 painter.addMouseMotionListener(new DragHandler());
155 addFocusListener(new FocusHandler());
156
157 // Setup the input handler
158 InputHandler inputHandler = new DefaultInputHandler(main);
159 inputHandler.addDefaultKeyBindings();
160 setInputHandler(inputHandler);
161
162 // Load the defaults
163 setDocument(defaults.document);
164 editable = defaults.editable;
165 caretVisible = defaults.caretVisible;
166 caretBlinks = defaults.caretBlinks;
167 electricScroll = defaults.electricScroll;
168
169 // We don't seem to get the initial focus event?
170 focusedComponent = this;
171 }
172
173 /**
174 * Overrides <code>update</code> for smoother repainting.
175 * Implemented 01/02/03
176 * @param g the <code>Graphics</code> context
177 */
178 public void update(Graphics g) {
179 paint(g);
180 }
181
182 /**
183 * Returns the entire text of this text area.
184 */
185 public String getText() {
186 try {
187 return document.getText(0, document.getLength());
188 } catch (BadLocationException bl) {
189 return null;
190 }
191 }
192
193 /**
194 * Sets the entire text of this text area.
195 */
196 public void setText(String text) {
197
198 try {
199 document.beginCompoundEdit();
200 document.remove(0, document.getLength());
201 document.insertString(0, text, null);
202 if(!modifiedSinceSave) {
203 modifiedSinceSave = true;
204 }
205 } catch (BadLocationException bl) {
206 } finally {
207 document.endCompoundEdit();
208 }
209 }
210
211 /**
212 * Appends the given text to the end of the document.
213 *
214 * @param str the text to append
215 */
216 public void append(String str) {
217 Document doc = getDocument();
218
219 if (doc != null) {
220 try {
221 doc.insertString(doc.getLength(), str, null);
222 if(!modifiedSinceSave) {
223 modifiedSinceSave = true;
224 }
225 } catch (BadLocationException bl) {
226
227 }
228 }
229 }
230
231 /**
232 * Inserts the given text after the current caret position.
233 *
234 * @param str the text to insert
235 */
236 public void insert(String str) {
237 Document doc = getDocument();
238
239 if (doc != null) {
240 try {
241 doc.insertString(getCaretPosition(), str, null);
242 if(!modifiedSinceSave) {
243 modifiedSinceSave = true;
244 }
245 } catch (BadLocationException bl) {
246
247 }
248 }
249 }
250
251 /**
252 * Returns the object responsible for painting this text area.
253 */
254 public final TextAreaPainter getPainter() {
255 return painter;
256 }
257
258 /**
259 * Returns the input handler.
260 */
261 public final InputHandler getInputHandler() {
262 return inputHandler;
263 }
264
265 /**
266 * Sets the input handler.
267 * @param inputHandler The new input handler
268 */
269 public void setInputHandler(InputHandler inputHandler) {
270 this.inputHandler = inputHandler;
271 }
272
273 /**
274 * Returns true if the caret is blinking, false otherwise.
275 */
276 public final boolean isCaretBlinkEnabled() {
277 return caretBlinks;
278 }
279
280 /**
281 * Toggles caret blinking.
282 * @param caretBlinks True if the caret should blink, false otherwise
283 */
284 public void setCaretBlinkEnabled(boolean caretBlinks) {
285 this.caretBlinks = caretBlinks;
286 if (!caretBlinks)
287 blink = false;
288
289 painter.invalidateSelectedLines();
290 }
291
292 /**
293 * Returns true if the caret is visible, false otherwise.
294 */
295 public final boolean isCaretVisible() {
296 return (!caretBlinks || blink) && caretVisible;
297 }
298
299 /**
300 * Sets if the caret should be visible.
301 * @param caretVisible true if the caret should be visible, false otherwise
302 */
303 public void setCaretVisible(boolean caretVisible) {
304 this.caretVisible = caretVisible;
305 blink = true;
306
307 painter.invalidateSelectedLines();
308 }
309
310 /**
311 * Blinks the caret.
312 */
313 public final void blinkCaret() {
314 if (caretBlinks) {
315 blink = !blink;
316 painter.invalidateSelectedLines();
317 } else
318 blink = true;
319 }
320
321 /**
322 * Returns the number of lines from the top and bottom of the
323 * text area that are always visible.
324 */
325 public final int getElectricScroll() {
326 return electricScroll;
327 }
328
329 /**
330 * Sets the number of lines from the top and bottom of the text
331 * area that are always visible
332 * @param electricScroll the number of lines always visible from
333 * the top or bottom
334 */
335 public final void setElectricScroll(int electricScroll) {
336 this.electricScroll = electricScroll;
337 }
338
339 /**
340 * Updates the state of the scroll bars. This should be called
341 * if the number of lines in the document changes, or when the
342 * size of the text area changes.
343 */
344 public void updateScrollBars() {
345 if (vertical != null && visibleLines != 0) {
346 vertical.setValues(firstLine, visibleLines, 0, getLineCount());
347 vertical.setUnitIncrement(2);
348 vertical.setBlockIncrement(visibleLines);
349 }
350
351 int width = painter.getWidth();
352
353 if (horizontal != null && width != 0) {
354 horizontal.setValues(-horizontalOffset, width, 0, width * 5);
355 horizontal.setUnitIncrement(painter.getFontMetrics().charWidth('w'));
356 horizontal.setBlockIncrement(width / 2);
357 }
358 }
359
360 /**
361 * Returns the line displayed at the text area's origin.
362 */
363 public final int getFirstLine() {
364 return firstLine;
365 }
366
367 /**
368 * Sets the line displayed at the text area's origin without
369 * updating the scroll bars.
370 */
371 public void setFirstLine(int firstLine) {
372 if (firstLine == this.firstLine)
373 return;
374 int oldFirstLine = this.firstLine;
375
376 this.firstLine = firstLine - 1;
377 if (firstLine != vertical.getValue())
378 updateScrollBars();
379 painter.repaint();
380 mGutter.repaint();
381 }
382
383 /**
384 * Returns the number of lines visible in this text area.
385 */
386 public final int getVisibleLines() {
387 return visibleLines;
388 }
389
390 /**
391 * Recalculates the number of visible lines. This should not
392 * be called directly.
393 */
394 public final void recalculateVisibleLines() {
395 if (painter == null)
396 return;
397 int height = painter.getHeight();
398 int lineHeight = painter.getFontMetrics().getHeight();
399 int oldVisibleLines = visibleLines;
400
401 visibleLines = height / lineHeight;
402 updateScrollBars();
403 }
404
405 /**
406 * Returns the horizontal offset of drawn lines.
407 */
408 public final int getHorizontalOffset() {
409 return horizontalOffset;
410 }
411
412 /**
413 * Sets the horizontal offset of drawn lines. This can be used to
414 * implement horizontal scrolling.
415 * @param horizontalOffset offset the new horizontal offset
416 */
417 public void setHorizontalOffset(int horizontalOffset) {
418 if (horizontalOffset == this.horizontalOffset)
419 return;
420 this.horizontalOffset = horizontalOffset;
421 if (horizontalOffset != horizontal.getValue())
422 updateScrollBars();
423 painter.repaint();
424 }
425
426 /**
427 * A fast way of changing both the first line and horizontal
428 * offset.
429 * @param firstLine the new first line
430 * @param horizontalOffset the new horizontal offset
431 * @return true if any of the values were changed, false otherwise
432 */
433 public boolean setOrigin(int firstLine, int horizontalOffset) {
434 boolean changed = false;
435 int oldFirstLine = this.firstLine;
436
437 if (horizontalOffset != this.horizontalOffset) {
438 this.horizontalOffset = horizontalOffset;
439 changed = true;
440 }
441
442 if (firstLine != this.firstLine) {
443 this.firstLine = firstLine;
444 changed = true;
445 }
446
447 if (changed) {
448 updateScrollBars();
449 painter.repaint();
450 mGutter.repaint();
451 }
452
453 return changed;
454 }
455
456 /**
457 * Ensures that the caret is visible by scrolling the text area if necessary.
458 * @return true if scrolling was actually performed, false if the
459 * caret was already visible
460 */
461 public boolean scrollToCaret() {
462 int line = getCaretLine();
463 int lineStart = getLineStartOffset(line);
464 int offset = Math.max(0, Math.min(getLineLength(line) - 1,
465 getCaretPosition() - lineStart));
466
467 return scrollTo(line, offset);
468 }
469
470 /**
471 * Ensures that the specified line and offset is visible by scrolling
472 * the text area if necessary.
473 * @param line the line to scroll to
474 * @param offset the offset in the line to scroll to
475 * @return true if scrolling was actually performed, false if the
476 * line and offset was already visible
477 */
478 public boolean scrollTo(int line, int offset) {
479 // visibleLines == 0 before the component is realized
480 // we can't do any proper scrolling then, so I have this hack...
481 if (visibleLines == 0) {
482 setFirstLine(Math.max(0, line - electricScroll));
483 return true;
484 }
485
486 int newFirstLine = firstLine;
487 int newHorizontalOffset = horizontalOffset;
488
489 if (line < firstLine + electricScroll) {
490 newFirstLine = Math.max(0, line - electricScroll);
491 } else if (line + electricScroll >= firstLine + visibleLines) {
492 newFirstLine = (line - visibleLines) + electricScroll + 1;
493 if (newFirstLine + visibleLines >= getLineCount())
494 newFirstLine = getLineCount() - visibleLines;
495 if (newFirstLine < 0)
496 newFirstLine = 0;
497 }
498
499 int x = _offsetToX(line, offset);
500 int width = painter.getFontMetrics().charWidth('w');
501
502 if (x < 0) {
503 newHorizontalOffset = Math.min(0, horizontalOffset - x + width + 5);
504 } else if (x + width >= painter.getWidth()) {
505 newHorizontalOffset = horizontalOffset + (painter.getWidth() - x)
506 - width - 5;
507 }
508
509 return setOrigin(newFirstLine, newHorizontalOffset);
510 }
511
512 /**
513 * Converts a line index to a y co-ordinate.
514 * @param line the line
515 */
516 public int lineToY(int line) {
517 FontMetrics fm = painter.getFontMetrics();
518
519 return (line - firstLine) * fm.getHeight()
520 - (fm.getLeading() + fm.getMaxDescent());
521 }
522
523 /**
524 * Converts a y co-ordinate to a line index.
525 * @param y the y co-ordinate
526 */
527 public int yToLine(int y) {
528 FontMetrics fm = painter.getFontMetrics();
529 int height = fm.getHeight();
530
531 return Math.max(0, Math.min(getLineCount() - 1,
532 y / height + firstLine));
533 }
534
535 /**
536 * Converts an offset in a line into an x co-ordinate. This is a
537 * slow version that can be used any time.
538 * @param line the line
539 * @param offset the offset, from the start of the line
540 */
541 public final int offsetToX(int line, int offset) {
542 // don't use cached tokens
543 painter.currentLineTokens = null;
544 return _offsetToX(line, offset);
545 }
546
547 /**
548 * Converts an offset in a line into an x co-ordinate. This is a
549 * fast version that should only be used if no changes were made
550 * to the text since the last repaint.
551 * @param line The line
552 * @param offset The offset, from the start of the line
553 */
554 public int _offsetToX(int line, int offset) {
555 TokenMarker tokenMarker = getTokenMarker();
556
557 /* Use painter's cached info for speed */
558 FontMetrics fm = painter.getFontMetrics();
559
560 getLineText(line, lineSegment);
561
562 int segmentOffset = lineSegment.offset;
563 int x = horizontalOffset;
564
565 /* If syntax coloring is disabled, do simple translation */
566 if (tokenMarker == null) {
567 lineSegment.count = offset;
568 return x + Utilities.getTabbedTextWidth(lineSegment,
569 fm, x, painter, 0);
570 } /* If syntax coloring is enabled, we have to do this because
571 * tokens can vary in width */ else {
572 Token tokens;
573
574 if (painter.currentLineIndex == line
575 && painter.currentLineTokens != null)
576 tokens = painter.currentLineTokens;
577 else {
578 painter.currentLineIndex = line;
579 tokens = painter.currentLineTokens
580 = tokenMarker.markTokens(lineSegment, line);
581 }
582
583 Toolkit toolkit = painter.getToolkit();
584 Font defaultFont = painter.getFont();
585 SyntaxStyle[] styles = painter.getStyles();
586
587 for (;;) {
588 byte id = tokens.id;
589
590 if (id == Token.END) {
591 return x;
592 }
593
594 if (id == Token.NULL)
595 fm = painter.getFontMetrics();
596 else
597 fm = styles[id].getFontMetrics(defaultFont);
598
599 int length = tokens.length;
600
601 if (offset + segmentOffset < lineSegment.offset + length) {
602 lineSegment.count = offset
603 - (lineSegment.offset - segmentOffset);
604 return x + Utilities.getTabbedTextWidth(
605 lineSegment, fm, x, painter, 0);
606 } else {
607 lineSegment.count = length;
608 x += Utilities.getTabbedTextWidth(
609 lineSegment, fm, x, painter, 0);
610 lineSegment.offset += length;
611 }
612 tokens = tokens.next;
613 }
614 }
615 }
616
617 /**
618 * Converts an x co-ordinate to an offset within a line.
619 * @param line The line
620 * @param x The x co-ordinate
621 */
622 public int xToOffset(int line, int x) {
623 TokenMarker tokenMarker = getTokenMarker();
624
625 /* Use painter's cached info for speed */
626 FontMetrics fm = painter.getFontMetrics();
627
628 getLineText(line, lineSegment);
629
630 char[] segmentArray = lineSegment.array;
631 int segmentOffset = lineSegment.offset;
632 int segmentCount = lineSegment.count;
633
634 int width = horizontalOffset;
635
636 if (tokenMarker == null) {
637 for (int i = 0; i < segmentCount; i++) {
638 char c = segmentArray[i + segmentOffset];
639 int charWidth;
640
641 if (c == '\t')
642 charWidth = (int) painter.nextTabStop(width, i) - width;
643 else
644 charWidth = fm.charWidth(c);
645
646 if (painter.isBlockCaretEnabled()) {
647 if (x - charWidth <= width)
648 return i;
649 } else {
650 if (x - charWidth / 2 <= width)
651 return i;
652 }
653
654 width += charWidth;
655 }
656
657 return segmentCount;
658 } else {
659 Token tokens;
660
661 if (painter.currentLineIndex == line
662 && painter.currentLineTokens != null)
663 tokens = painter.currentLineTokens;
664 else {
665 painter.currentLineIndex = line;
666 tokens = painter.currentLineTokens
667 = tokenMarker.markTokens(lineSegment, line);
668 }
669
670 int offset = 0;
671 Toolkit toolkit = painter.getToolkit();
672 Font defaultFont = painter.getFont();
673 SyntaxStyle[] styles = painter.getStyles();
674
675 for (;;) {
676 byte id = tokens.id;
677
678 if (id == Token.END)
679 return offset;
680
681 if (id == Token.NULL)
682 fm = painter.getFontMetrics();
683 else
684 fm = styles[id].getFontMetrics(defaultFont);
685
686 int length = tokens.length;
687
688 for (int i = 0; i < length; i++) {
689 char c = segmentArray[segmentOffset + offset + i];
690 int charWidth;
691
692 if (c == '\t')
693 charWidth = (int) painter.nextTabStop(width, offset + i)
694 - width;
695 else
696 charWidth = fm.charWidth(c);
697
698 if (painter.isBlockCaretEnabled()) {
699 if (x - charWidth <= width)
700 return offset + i;
701 } else {
702 if (x - charWidth / 2 <= width)
703 return offset + i;
704 }
705
706 width += charWidth;
707 }
708
709 offset += length;
710 tokens = tokens.next;
711 }
712 }
713 }
714
715 /**
716 * Converts a point to an offset, from the start of the text.
717 * @param x The x co-ordinate of the point
718 * @param y The y co-ordinate of the point
719 */
720 public int xyToOffset(int x, int y) {
721 int line = yToLine(y);
722 int start = getLineStartOffset(line);
723
724 return start + xToOffset(line, x);
725 }
726
727 /**
728 * Returns the document this text area is editing.
729 */
730 public SyntaxDocument getDocument() {
731 return document;
732 }
733
734 /**
735 * Sets the document this text area is editing.
736 * @param document The document
737 */
738 public void setDocument(SyntaxDocument document) {
739 if (this.document == document)
740 return;
741 if (this.document != null)
742 this.document.removeDocumentListener(documentHandler);
743 this.document = document;
744
745 document.addDocumentListener(documentHandler);
746
747 select(0, 0);
748 updateScrollBars();
749 painter.repaint();
750 }
751
752 /**
753 * Sets a new indent length.
754 * @param indent the int value for new indent length
755 */
756 public void setIndent(int indent) {
757 getDocument().setIndent(indent);
758 }
759
760 /**
761 * Returns the document's token marker. Equivalent to calling
762 * <code>getDocument().getTokenMarker()</code>.
763 */
764 public final TokenMarker getTokenMarker() {
765 return document.getTokenMarker();
766 }
767
768 /**
769 * Sets the document's token marker. Equivalent to caling
770 * <code>getDocument().setTokenMarker()</code>.
771 * @param tokenMarker The token marker
772 */
773 public final void setTokenMarker(TokenMarker tokenMarker) {
774 document.setTokenMarker(tokenMarker);
775 }
776
777 /**
778 * Returns the length of the document. Equivalent to calling
779 * <code>getDocument().getLength()</code>.
780 */
781 public int getDocumentLength() {
782 return document.getLength();
783 }
784
785 /**
786 * Returns the number of lines in the document.
787 */
788 public int getLineCount() {
789 return document.getDefaultRootElement().getElementCount();
790 }
791
792 /**
793 * Returns the line containing the specified offset.
794 * @param offset The offset
795 */
796 public final int getLineOfOffset(int offset) {
797 return document.getDefaultRootElement().getElementIndex(offset);
798 }
799
800 /**
801 * Returns the start offset of the specified line.
802 * @param line The line
803 * @return The start offset of the specified line, or -1 if the line is
804 * invalid
805 */
806 public int getLineStartOffset(int line) {
807 Element lineElement = document.getDefaultRootElement().getElement(line);
808
809 if (lineElement == null)
810 return -1;
811 else
812 return lineElement.getStartOffset();
813 }
814
815 /**
816 * Returns the end offset of the specified line.
817 * @param line The line
818 * @return The end offset of the specified line, or -1 if the line is
819 * invalid.
820 */
821 public int getLineEndOffset(int line) {
822 Element lineElement = document.getDefaultRootElement().getElement(line);
823
824 if (lineElement == null)
825 return -1;
826 else
827 return lineElement.getEndOffset();
828 }
829
830 /**
831 * Returns the length of the specified line.
832 * @param line The line
833 */
834 public int getLineLength(int line) {
835 Element lineElement = document.getDefaultRootElement().getElement(line);
836
837 if (lineElement == null)
838 return -1;
839 else
840 return lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
841 }
842
843 /**
844 * Returns the specified substring of the document.
845 * @param start The start offset
846 * @param len The length of the substring
847 * @return The substring, or null if the offsets are invalid
848 */
849 public final String getText(int start, int len) {
850 try {
851 return document.getText(start, len);
852 } catch (BadLocationException bl) {
853
854 return null;
855 }
856 }
857
858 /**
859 * Copies the specified substring of the document into a segment.
860 * If the offsets are invalid, the segment will contain a null string.
861 * @param start The start offset
862 * @param len The length of the substring
863 * @param segment The segment
864 */
865 public final void getText(int start, int len, Segment segment) {
866 try {
867 document.getText(start, len, segment);
868 } catch (BadLocationException bl) {
869
870 segment.offset = segment.count = 0;
871 }
872 }
873
874 /**
875 * Returns the text on the specified line.
876 * @param lineIndex The line
877 * @return The text, or null if the line is invalid
878 */
879 public String getLineText(int lineIndex) {
880 int start = getLineStartOffset(lineIndex);
881
882 return getText(start, getLineEndOffset(lineIndex) - start - 1);
883 }
884
885 /**
886 * Copies the text on the specified line into a segment. If the line
887 * is invalid, the segment will contain a null string.
888 * @param lineIndex The line
889 */
890 public void getLineText(int lineIndex, Segment segment) {
891 int start = getLineStartOffset(lineIndex);
892
893 getText(start, getLineEndOffset(lineIndex) - start - 1, segment);
894 }
895
896 /**
897 * Returns the selection start offset.
898 */
899 public final int getSelectionStart() {
900 return selectionStart;
901 }
902
903 /**
904 * Returns the offset where the selection starts on the specified
905 * line.
906 */
907 public int getSelectionStart(int line) {
908 if (line == selectionStartLine)
909 return selectionStart;
910 else if (rectSelect) {
911 Element map = document.getDefaultRootElement();
912 int start = selectionStart
913 - map.getElement(selectionStartLine).getStartOffset();
914
915 Element lineElement = map.getElement(line);
916 int lineStart = lineElement.getStartOffset();
917 int lineEnd = lineElement.getEndOffset() - 1;
918
919 return Math.min(lineEnd, lineStart + start);
920 } else
921 return getLineStartOffset(line);
922 }
923
924 /**
925 * Returns the selection start line.
926 */
927 public final int getSelectionStartLine() {
928 return selectionStartLine;
929 }
930
931 /**
932 * Sets the selection start. The new selection will be the new
933 * selection start and the old selection end.
934 * @param selectionStart The selection start
935 * @see #select(int,int)
936 */
937 public final void setSelectionStart(int selectionStart) {
938 select(selectionStart, selectionEnd);
939 }
940
941 /**
942 * Returns the selection end offset.
943 */
944 public final int getSelectionEnd() {
945 return selectionEnd;
946 }
947
948 /**
949 * Returns the offset where the selection ends on the specified
950 * line.
951 */
952 public int getSelectionEnd(int line) {
953 if (line == selectionEndLine)
954 return selectionEnd;
955 else if (rectSelect) {
956 Element map = document.getDefaultRootElement();
957 int end = selectionEnd
958 - map.getElement(selectionEndLine).getStartOffset();
959
960 Element lineElement = map.getElement(line);
961 int lineStart = lineElement.getStartOffset();
962 int lineEnd = lineElement.getEndOffset() - 1;
963
964 return Math.min(lineEnd, lineStart + end);
965 } else
966 return getLineEndOffset(line) - 1;
967 }
968
969 /**
970 * Returns the selection end line.
971 */
972 public final int getSelectionEndLine() {
973 return selectionEndLine;
974 }
975
976 /**
977 * Sets the selection end. The new selection will be the old
978 * selection start and the bew selection end.
979 * @param selectionEnd The selection end
980 * @see #select(int,int)
981 */
982 public final void setSelectionEnd(int selectionEnd) {
983 select(selectionStart, selectionEnd);
984 }
985
986 /**
987 * Returns the caret position. This will either be the selection
988 * start or the selection end, depending on which direction the
989 * selection was made in.
990 */
991 public final int getCaretPosition() {
992 return (biasLeft ? selectionStart : selectionEnd);
993 }
994
995 /**
996 * Returns the caret line.
997 */
998 public final int getCaretLine() {
999 return (biasLeft ? selectionStartLine : selectionEndLine);
1000 }
1001
1002 /**
1003 * Returns the mark position. This will be the opposite selection
1004 * bound to the caret position.
1005 * @see #getCaretPosition()
1006 */
1007 public final int getMarkPosition() {
1008 return (biasLeft ? selectionEnd : selectionStart);
1009 }
1010
1011 /**
1012 * Returns the mark line.
1013 */
1014 public final int getMarkLine() {
1015 return (biasLeft ? selectionEndLine : selectionStartLine);
1016 }
1017
1018 /**
1019 * Sets the caret position. The new selection will consist of the
1020 * caret position only (hence no text will be selected)
1021 * @param caret The caret position
1022 * @see #select(int,int)
1023 */
1024 public final void setCaretPosition(int caret) {
1025 select(caret, caret);
1026 }
1027
1028 /**
1029 * Selects all text in the document.
1030 */
1031 public final void selectAll() {
1032 select(0, getDocumentLength());
1033 }
1034
1035 /**
1036 * Moves the mark to the caret position.
1037 */
1038 public final void selectNone() {
1039 select(getCaretPosition(), getCaretPosition());
1040 }
1041
1042 /**
1043 * Selects from the start offset to the end offset. This is the
1044 * general selection method used by all other selecting methods.
1045 * The caret position will be start if start < end, and end
1046 * if end > start.
1047 * @param start The start offset
1048 * @param end The end offset
1049 */
1050 public void select(int start, int end) {
1051 int newStart, newEnd;
1052 boolean newBias;
1053
1054 if (start <= end) {
1055 newStart = start;
1056 newEnd = end;
1057 newBias = false;
1058 } else {
1059 newStart = end;
1060 newEnd = start;
1061 newBias = true;
1062 }
1063
1064 if (newStart < 0 || newEnd > getDocumentLength()) {
1065 throw new IllegalArgumentException("Bounds out of range: " + newStart + "," + newEnd);
1066 }
1067
1068 // If the new position is the same as the old, we don't
1069 // do all this crap, however we still do the stuff at
1070 // the end (clearing magic position, scrolling)
1071 if (newStart != selectionStart || newEnd != selectionEnd
1072 || newBias != biasLeft) {
1073 int newStartLine = getLineOfOffset(newStart);
1074 int newEndLine = getLineOfOffset(newEnd);
1075
1076 if (painter.isBracketHighlightEnabled()) {
1077 if (bracketLine != -1)
1078 painter.invalidateLine(bracketLine);
1079 updateBracketHighlight(end);
1080 if (bracketLine != -1)
1081 painter.invalidateLine(bracketLine);
1082 }
1083
1084 painter.invalidateLineRange(selectionStartLine, selectionEndLine);
1085 painter.invalidateLineRange(newStartLine, newEndLine);
1086
1087 // repaint the gutter if the current line changes and current
1088 // line highlighting is enabled
1089 if ((newStartLine != selectionStartLine
1090 || newEndLine != selectionEndLine || newBias != biasLeft)
1091 && mGutter.isCurrentLineHighlightEnabled()) {
1092 mGutter.invalidateLine(biasLeft ? selectionStartLine : selectionEndLine);
1093 mGutter.invalidateLine(newBias ? newStartLine : newEndLine);
1094 }
1095
1096 document.addUndoableEdit(new CaretUndo(
1097 selectionStart, selectionEnd));
1098
1099 selectionStart = newStart;
1100 selectionEnd = newEnd;
1101 selectionStartLine = newStartLine;
1102 selectionEndLine = newEndLine;
1103 biasLeft = newBias;
1104
1105 fireCaretEvent();
1106 }
1107
1108 // When the user is typing, etc, we don't want the caret to blink
1109 blink = true;
1110 caretTimer.restart();
1111
1112 // Disable rectangle select if selection start = selection end
1113 if (selectionStart == selectionEnd)
1114 rectSelect = false;
1115
1116 // Clear the `magic' caret position used by up/down
1117 magicCaret = -1;
1118
1119 scrollToCaret();
1120 }
1121
1122 /**
1123 * Returns the selected text, or null if no selection is active.
1124 */
1125 public final String getSelectedText() {
1126 if (selectionStart == selectionEnd)
1127 return null;
1128
1129 if (rectSelect) {
1130 // Return each row of the selection on a new line
1131
1132 Element map = document.getDefaultRootElement();
1133
1134 int start = selectionStart
1135 - map.getElement(selectionStartLine).getStartOffset();
1136 int end = selectionEnd
1137 - map.getElement(selectionEndLine).getStartOffset();
1138
1139 // Certain rectangles satisfy this condition...
1140 if (end < start) {
1141 int tmp = end;
1142
1143 end = start;
1144 start = tmp;
1145 }
1146
1147 StringBuffer buf = new StringBuffer();
1148 Segment seg = new Segment();
1149
1150 for (int i = selectionStartLine; i <= selectionEndLine; i++) {
1151 Element lineElement = map.getElement(i);
1152 int lineStart = lineElement.getStartOffset();
1153 int lineEnd = lineElement.getEndOffset() - 1;
1154 int lineLen = lineEnd - lineStart;
1155
1156 lineStart = Math.min(lineStart + start, lineEnd);
1157 lineLen = Math.min(end - start, lineEnd - lineStart);
1158
1159 getText(lineStart, lineLen, seg);
1160 buf.append(seg.array, seg.offset, seg.count);
1161
1162 if (i != selectionEndLine)
1163 buf.append(lineSep);
1164 }
1165
1166 return buf.toString();
1167 } else {
1168 return getText(selectionStart, selectionEnd - selectionStart);
1169 }
1170 }
1171
1172 /**
1173 * Replaces the selection with the specified text.
1174 * @param selectedText The replacement text for the selection
1175 */
1176 public void setSelectedText(String selectedText) {
1177 if (indent) tab = " ";
1178 if (!editable) {
1179 throw new InternalError("Text component read only");
1180 }
1181
1182 document.beginCompoundEdit();
1183
1184 try {
1185 if (rectSelect) {
1186 Element map = document.getDefaultRootElement();
1187
1188 int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
1189 int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
1190
1191 // Certain rectangles satisfy this condition...
1192 if (end < start) {
1193 int tmp = end;
1194
1195 end = start;
1196 start = tmp;
1197 }
1198
1199 int lastNewline = 0;
1200 int currNewline = 0;
1201
1202 for (int i = selectionStartLine; i <= selectionEndLine; i++) {
1203 Element lineElement = map.getElement(i);
1204 int lineStart = lineElement.getStartOffset();
1205 int lineEnd = lineElement.getEndOffset() - 1;
1206 int rectStart = Math.min(lineEnd, lineStart + start);
1207
1208 document.remove(rectStart, Math.min(lineEnd - rectStart,
1209 end - start));
1210
1211 if (selectedText == null)
1212 continue;
1213
1214 currNewline = selectedText.indexOf(lineSep, lastNewline);
1215 if (currNewline == -1)
1216 currNewline = selectedText.length();
1217
1218 document.insertString(rectStart, tab + selectedText.substring(lastNewline, currNewline), null);
1219
1220 lastNewline = Math.min(selectedText.length(),
1221 currNewline + 1);
1222 }
1223
1224 if (selectedText != null && currNewline != selectedText.length()) {
1225 int offset = map.getElement(selectionEndLine).getEndOffset()
1226 - 1;
1227
1228 document.insertString(offset, lineSep, null);
1229 document.insertString(offset + 1, selectedText.substring(currNewline + 1), null);
1230 }
1231 } else {
1232 document.remove(selectionStart,
1233 selectionEnd - selectionStart);
1234 if (selectedText != null) {
1235 document.insertString(selectionStart,
1236 selectedText, null);
1237 }
1238 }
1239 } catch (BadLocationException bl) {
1240
1241 throw new InternalError("Cannot replace selection");
1242 }
1243 // No matter what happens, avoid leaving the document in a bad state
1244 finally {
1245 document.endCompoundEdit();
1246 }
1247
1248 setCaretPosition(selectionEnd);
1249 }
1250
1251 /**
1252 * Returns true if this text area is editable, false otherwise.
1253 */
1254 public final boolean isEditable() {
1255 return editable;
1256 }
1257
1258 /**
1259 * Sets if this component is editable.
1260 * @param editable True if this text area should be editable, false otherwise
1261 */
1262 public final void setEditable(boolean editable) {
1263 this.editable = editable;
1264 }
1265
1266 /**
1267 * Returns the right click popup menu.
1268 */
1269 public final JPopupMenu getRightClickPopup() {
1270 return popup;
1271 }
1272
1273 /**
1274 * Sets the right click popup menu.
1275 * @param popup The popup
1276 */
1277 public final void setRightClickPopup(JPopupMenu popup) {
1278 this.popup = popup;
1279 }
1280
1281 /**
1282 * Returns the `magic' caret position. This can be used to preserve
1283 * the column position when moving up and down lines.
1284 */
1285 public final int getMagicCaretPosition() {
1286 return magicCaret;
1287 }
1288
1289 /**
1290 * Sets the `magic' caret position. This can be used to preserve
1291 * the column position when moving up and down lines.
1292 * @param magicCaret The magic caret position
1293 */
1294 public final void setMagicCaretPosition(int magicCaret) {
1295 this.magicCaret = magicCaret;
1296 }
1297
1298 /**
1299 * Similar to <code>setSelectedText()</code>, but overstrikes the
1300 * appropriate number of characters if overwrite mode is enabled.
1301 * @param str The string
1302 * @see #setSelectedText(String)
1303 * @see #isOverwriteEnabled()
1304 */
1305 public void overwriteSetSelectedText(String str) {
1306 // Don't overstrike if there is a selection
1307 if (!overwrite || selectionStart != selectionEnd) {
1308 setSelectedText(str);
1309 return;
1310 }
1311
1312 // Don't overstrike if we are on the end of the line
1313 int caret = getCaretPosition();
1314 int caretLineEnd = getLineEndOffset(getCaretLine());
1315
1316 if (caretLineEnd - caret <= str.length()) {
1317 setSelectedText(str);
1318 return;
1319 }
1320
1321 document.beginCompoundEdit();
1322
1323 try {
1324 document.remove(caret, str.length());
1325 document.insertString(caret, str, null);
1326 } catch (BadLocationException bl) {
1327 bl.printStackTrace();
1328 } finally {
1329 document.endCompoundEdit();
1330 }
1331 }
1332
1333 /**
1334 * Returns true if overwrite mode is enabled, false otherwise.
1335 */
1336 public final boolean isOverwriteEnabled() {
1337 return overwrite;
1338 }
1339
1340 /**
1341 * Sets if overwrite mode should be enabled.
1342 * @param overwrite True if overwrite mode should be enabled, false otherwise.
1343 */
1344 public final void setOverwriteEnabled(boolean overwrite) {
1345 this.overwrite = overwrite;
1346 painter.invalidateSelectedLines();
1347 }
1348
1349 /**
1350 * Returns true if the selection is rectangular, false otherwise.
1351 */
1352 public final boolean isSelectionRectangular() {
1353 return rectSelect;
1354 }
1355
1356 /**
1357 * Sets if the selection should be rectangular.
1358 * @param overwrite True if the selection should be rectangular,
1359 * false otherwise.
1360 */
1361 public final void setSelectionRectangular(boolean rectSelect) {
1362 this.rectSelect = rectSelect;
1363 painter.invalidateSelectedLines();
1364 }
1365
1366 /**
1367 * Returns the position of the highlighted bracket (the bracket
1368 * matching the one before the caret)
1369 */
1370 public final int getBracketPosition() {
1371 return bracketPosition;
1372 }
1373
1374 /**
1375 * Returns the line of the highlighted bracket (the bracket
1376 * matching the one before the caret)
1377 */
1378 public final int getBracketLine() {
1379 return bracketLine;
1380 }
1381
1382 /**
1383 * Adds a caret change listener to this text area.
1384 * @param listener The listener
1385 */
1386 public final void addCaretListener(CaretListener listener) {
1387 listenerList.add(CaretListener.class, listener);
1388 }
1389
1390 /**
1391 * Removes a caret change listener from this text area.
1392 * @param listener The listener
1393 */
1394 public final void removeCaretListener(CaretListener listener) {
1395 listenerList.remove(CaretListener.class, listener);
1396 }
1397
1398 /**
1399 * Deletes the selected text from the text area and places it
1400 * into the clipboard.
1401 */
1402 public void cut() {
1403 if (editable) {
1404 copy();
1405 setSelectedText("");
1406 }
1407 }
1408
1409 /**
1410 * Places the selected text into the clipboard.
1411 */
1412 public void copy() {
1413 if (selectionStart != selectionEnd) {
1414 Clipboard clipboard = getToolkit().getSystemClipboard();
1415
1416 String selection = getSelectedText();
1417
1418 int repeatCount = inputHandler.getRepeatCount();
1419 StringBuffer buf = new StringBuffer();
1420
1421 for (int i = 0; i < repeatCount; i++)
1422 buf.append(selection);
1423
1424 clipboard.setContents(new StringSelection(buf.toString()), null);
1425 }
1426 }
1427
1428 /**
1429 * Inserts the clipboard contents into the text.
1430 */
1431 public void paste() {
1432 if (editable) {
1433 Clipboard clipboard = getToolkit().getSystemClipboard();
1434
1435 try {
1436 // The MacOS MRJ doesn't convert \r to \n, so do it here
1437 String selection = ((String) clipboard.getContents(this).getTransferData(
1438 DataFlavor.stringFlavor)).replace('\r', '\n');
1439
1440 int repeatCount = inputHandler.getRepeatCount();
1441 StringBuffer buf = new StringBuffer();
1442
1443 for (int i = 0; i < repeatCount; i++)
1444 buf.append(selection);
1445 selection = buf.toString();
1446 setSelectedText(selection);
1447 } catch (Exception e) {
1448 getToolkit().beep();
1449 GUIUtilities.showExceptionErrorMessage("Clipboard does not contain a string");
1450 }
1451 }
1452 }
1453
1454 /**
1455 * Undoes the most recent edit. Returns true if the undo was successful.
1456 */
1457 public boolean undo() {
1458 if (editable) return getDocument().undo();
1459 else return false;
1460 }
1461
1462 /**
1463 * Redoes the most recently undone edit. Returns true if the redo was successful.
1464 */
1465 public boolean redo() {
1466 if (editable) return getDocument().redo();
1467 else return false;
1468 }
1469
1470 /**
1471 * Indents the selected lines.
1472 */
1473 public void indent() {
1474
1475 if (selectionStart == selectionEnd)
1476 return;
1477
1478 Element map = document.getDefaultRootElement();
1479
1480 int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
1481 int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
1482
1483 // Certain rectangles satisfy this condition...
1484 if (end < start) {
1485 int tmp = end;
1486 end = start;
1487 start = tmp;
1488 }
1489
1490 StringBuffer buf = new StringBuffer();
1491 Segment seg = new Segment();
1492
1493 for (int i = selectionStartLine; i <= selectionEndLine; i++) {
1494 Element lineElement = map.getElement(i);
1495 int lineStart = lineElement.getStartOffset();
1496 int lineEnd = lineElement.getEndOffset() - 1;
1497 int lineLen = lineEnd - lineStart;
1498
1499 lineStart = Math.min(lineStart + start, lineEnd);
1500 lineLen = Math.max(end - start, lineEnd - lineStart);