001/* 002 * JEditTextArea.java - jEdit's text component 003 * Copyright (C) 1999 Slava Pestov 004 * Copyright (C) 2000-2010 The Gridarta Developers. 005 * 006 * You may use and modify this package for any purpose. Redistribution is 007 * permitted, in both source and binary form, provided that this notice 008 * remains intact in all source distributions of this package. 009 */ 010 011package net.sf.gridarta.textedit.textarea; 012 013import java.awt.AWTEvent; 014import java.awt.Adjustable; 015import java.awt.Font; 016import java.awt.FontMetrics; 017import java.awt.datatransfer.Clipboard; 018import java.awt.datatransfer.DataFlavor; 019import java.awt.datatransfer.StringSelection; 020import java.awt.datatransfer.UnsupportedFlavorException; 021import java.awt.event.ActionEvent; 022import java.awt.event.ActionListener; 023import java.awt.event.AdjustmentEvent; 024import java.awt.event.AdjustmentListener; 025import java.awt.event.ComponentAdapter; 026import java.awt.event.ComponentEvent; 027import java.awt.event.FocusEvent; 028import java.awt.event.FocusListener; 029import java.awt.event.InputEvent; 030import java.awt.event.KeyEvent; 031import java.awt.event.MouseAdapter; 032import java.awt.event.MouseEvent; 033import java.awt.event.MouseMotionListener; 034import java.awt.event.MouseWheelEvent; 035import java.io.IOException; 036import java.lang.reflect.Field; 037import java.lang.reflect.InvocationTargetException; 038import java.lang.reflect.Method; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Set; 042import javax.swing.JComponent; 043import javax.swing.JPopupMenu; 044import javax.swing.JScrollBar; 045import javax.swing.KeyStroke; 046import javax.swing.SwingUtilities; 047import javax.swing.Timer; 048import javax.swing.event.CaretEvent; 049import javax.swing.event.DocumentEvent; 050import javax.swing.event.DocumentListener; 051import javax.swing.text.BadLocationException; 052import javax.swing.text.Element; 053import javax.swing.text.Segment; 054import javax.swing.text.Utilities; 055import javax.swing.undo.AbstractUndoableEdit; 056import javax.swing.undo.CannotRedoException; 057import javax.swing.undo.CannotUndoException; 058import javax.swing.undo.UndoableEdit; 059import net.sf.gridarta.textedit.textarea.tokenmarker.TokenMarker; 060import org.apache.log4j.Category; 061import org.apache.log4j.Logger; 062import org.jetbrains.annotations.NotNull; 063import org.jetbrains.annotations.Nullable; 064 065/** 066 * jEdit's text area component. It is more suited for editing program source 067 * code than JEditorPane, because it drops the unnecessary features (images, 068 * variable-width lines, and so on) and adds a whole bunch of useful goodies 069 * such as: <ul> <li>More flexible key binding scheme <li>Supports macro 070 * recorders <li>Rectangular selection <li>Bracket highlighting <li>Syntax 071 * highlighting <li>Command repetition <li>Block caret can be enabled </ul> It 072 * is also faster and doesn't have as many problems. It can be used in other 073 * applications; the only other part of jEdit it depends on is the syntax 074 * package.<p> <p/> To use it in your app, treat it like any other component, 075 * for example: 076 * <pre>JEditTextArea ta = new JEditTextArea(); 077 * ta.setTokenMarker(new JavaTokenMarker()); 078 * ta.setText("public class Test {\n" 079 * + " public static void main(String[] args) {\n" 080 * + " System.err.println(\"Hello World\");\n" 081 * + " }\n" 082 * + "}");</pre> 083 * @author Slava Pestov 084 * @author <a href="mailto:andi.vogl@gmx.net">Andreas Vogl</a> 085 * @author Andreas Kirschbaum 086 */ 087public class JEditTextArea extends JComponent { 088 089 /** 090 * The Logger for printing log messages. 091 */ 092 private static final Category log = Logger.getLogger(JEditTextArea.class); 093 094 /** 095 * Serial Version UID. 096 */ 097 private static final long serialVersionUID = 1L; 098 099 /** 100 * The text contents in the last "unmodified" state. 101 */ 102 @NotNull 103 private String unmodifiedText = ""; 104 105 @Nullable 106 private static JEditTextArea focusedComponent; 107 108 @NotNull 109 private static final Timer caretTimer = new Timer(500, new CaretBlinker()); 110 111 static { 112 caretTimer.setInitialDelay(500); 113 caretTimer.start(); 114 } 115 116 @NotNull 117 private final TextAreaPainter painter; 118 119 @Nullable 120 private final JPopupMenu popup; 121 122 private final boolean caretBlinks; 123 124 private boolean caretVisible; 125 126 private boolean blink; 127 128 private final boolean editable; 129 130 private int firstLine; 131 132 private int visibleLines; 133 134 private final int electricScroll; 135 136 private int horizontalOffset; 137 138 @NotNull 139 private final JScrollBar vertical = new JScrollBar(Adjustable.VERTICAL); 140 141 @NotNull 142 private final JScrollBar horizontal = new JScrollBar(Adjustable.HORIZONTAL); 143 144 private boolean scrollBarsInitialized; 145 146 @NotNull 147 private final InputHandler inputHandler; 148 149 @Nullable 150 private SyntaxDocument document; 151 152 @NotNull 153 private final DocumentListener documentHandler; 154 155 @NotNull 156 private final Segment lineSegment; 157 158 private int selectionStart; 159 160 private int selectionStartLine; 161 162 private int selectionEnd; 163 164 private int selectionEndLine; 165 166 private boolean biasLeft; 167 168 private int bracketPosition; 169 170 private int bracketLine; 171 172 private int magicCaret; 173 174 private boolean overwrite; 175 176 private boolean rectangleSelect; 177 178 /** 179 * Creates a new JEditTextArea with the specified settings. 180 * @param defaults the default settings 181 */ 182 public JEditTextArea(@NotNull final TextAreaDefaults defaults) { 183 // Enable the necessary events 184 enableEvents(AWTEvent.KEY_EVENT_MASK); 185 186 // Initialize some misc. stuff 187 painter = new TextAreaPainter(this, defaults); 188 documentHandler = new DocumentHandler(); 189 lineSegment = new Segment(); 190 bracketLine = -1; 191 bracketPosition = -1; 192 blink = true; 193 194 // Initialize the GUI 195 setLayout(new ScrollLayout(this)); 196 add(ScrollLayout.CENTER, painter); 197 add(ScrollLayout.RIGHT, vertical); 198 add(ScrollLayout.BOTTOM, horizontal); 199 200 // Add some event listeners 201 vertical.addAdjustmentListener(new AdjustHandler()); 202 horizontal.addAdjustmentListener(new AdjustHandler()); 203 painter.addComponentListener(new ComponentHandler()); 204 final MouseHandler mouseHandler = new MouseHandler(); 205 painter.addMouseListener(mouseHandler); 206 painter.addMouseWheelListener(mouseHandler); 207 painter.addMouseMotionListener(new DragHandler()); 208 addFocusListener(new FocusHandler()); 209 210 // Load the defaults 211 inputHandler = defaults.getInputHandler(); 212 setDocument(defaults.getDocument()); 213 editable = defaults.getEditable(); 214 caretVisible = defaults.getCaretVisible(); 215 caretBlinks = defaults.getCaretBlinks(); 216 electricScroll = defaults.getElectricScroll(); 217 218 popup = defaults.getPopup(); 219 220 // free tab key from the focus traversal manager 221 freeTabKeyFromFocusTraversal(); 222 223 // We don't seem to get the initial focus event? 224 focusedComponent = this; 225 } 226 227 /** 228 * In JDKs above 1.4, the tab key is used for focus traversal. That means 229 * the tab key normally does not "work" inside the text area. But that would 230 * be a pity, because we need the tab key for indentation. <p/> So what this 231 * method does is setting the focus traversal to "tab + <control>", in 232 * order to "free" the tab key from focus traversal events. In JDKs above 233 * 1.4, this task can be accomplished simply by three lines of code: <code> 234 * Set forwardTraversalKeys = new HashSet(); forwardTraversalKeys.add(KeyStroke.getKeyStroke("control 235 * TAB")); setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, 236 * forwardTraversalKeys); </code> <p/> Now, what's the big deal there? The 237 * problem is that the class "KeyboardFocusManager" as well as the method 238 * "setFocusTraversalKeys" are undefined in JDKs 1.3.* and below. Hence, if 239 * above code was inserted as-is, this application would no longer compile 240 * with java 1.3. <p/> The solution to this problem is implemented here: The 241 * critical classes and methods are accessed through the reflection 242 * interface, which allows to execute them *if defined* but still compile 243 * *if undefined*. 244 */ 245 private void freeTabKeyFromFocusTraversal() { 246 try { 247 // preparing the key set first, this should be harmless 248 final Set<KeyStroke> forwardTraversalKeys = new HashSet<KeyStroke>(); 249 forwardTraversalKeys.add(KeyStroke.getKeyStroke("control TAB")); 250 251 // here we try to access java.awt.KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS 252 final Field field = Class.forName("java.awt.KeyboardFocusManager").getField("FORWARD_TRAVERSAL_KEYS"); 253 final Integer value = field.getInt(field); // store the value of this field 254 255 for (final Method method : getClass().getMethods()) { 256 // here we try to find the method "setFocusTraversalKeys", and execute it if found 257 if (method.getName().equalsIgnoreCase("setFocusTraversalKeys")) { 258 method.invoke(this, value, forwardTraversalKeys); 259 // System.err.println("freeTabKeyFromFocusTraversal() succeeded!"); 260 } 261 } 262 } catch (final ClassNotFoundException ignored) { 263 // ignore 264 } catch (final IllegalAccessException ignored) { 265 // ignore 266 } catch (final InvocationTargetException ignored) { 267 // ignore 268 } catch (final NoSuchFieldException ignored) { 269 // ignore 270 } 271 } 272 273 /** 274 * Set the TextArea font 275 * @param font font 276 */ 277 @Override 278 public void setFont(@NotNull final Font font) { 279 painter.setFont(font); 280 } 281 282 /** 283 * Returns the object responsible for painting this text area. 284 */ 285 @NotNull 286 public TextAreaPainter getPainter() { 287 return painter; 288 } 289 290 /** 291 * Returns the input handler. 292 */ 293 @NotNull 294 public InputHandler getInputHandler() { 295 return inputHandler; 296 } 297 298 /** 299 * Returns true if the caret is visible, false otherwise. 300 */ 301 public boolean isCaretVisible() { 302 return (!caretBlinks || blink) && caretVisible; 303 } 304 305 /** 306 * Sets if the caret should be visible. 307 * @param caretVisible true if the caret should be visible, false otherwise 308 */ 309 public void setCaretVisible(final boolean caretVisible) { 310 this.caretVisible = caretVisible; 311 blink = true; 312 313 painter.invalidateSelectedLines(); 314 } 315 316 /** 317 * Blinks the caret. 318 */ 319 public void blinkCaret() { 320 if (caretBlinks) { 321 blink = !blink; 322 painter.invalidateSelectedLines(); 323 } else { 324 blink = true; 325 } 326 } 327 328 /** 329 * Returns the number of lines from the top and button of the text area that 330 * are always visible. 331 */ 332 public int getElectricScroll() { 333 return electricScroll; 334 } 335 336 /** 337 * Updates the state of the scroll bars. This should be called if the number 338 * of lines in the document changes, or when the size of the text are 339 * changes. 340 */ 341 public void updateScrollBars() { 342 if (visibleLines != 0) { 343 vertical.setValues(firstLine, visibleLines, 0, getLineCount()); 344 //vertical.setUnitIncrement(2); 345 vertical.setUnitIncrement(1); // scroll one line per click 346 vertical.setBlockIncrement(visibleLines); 347 } 348 349 final int width = painter.getWidth(); 350 if (width != 0) { 351 horizontal.setValues(-horizontalOffset, width, 0, width * 5); 352 //horizontal.setUnitIncrement(painter.getFontMetrics().charWidth('w')); 353 horizontal.setUnitIncrement(painter.getDefaultCharWidth()); 354 horizontal.setBlockIncrement(width / 2); 355 } 356 } 357 358 /** 359 * Returns the line displayed at the text area's origin. 360 */ 361 public int getFirstLine() { 362 return firstLine; 363 } 364 365 /** 366 * Sets the line displayed at the text area's origin without updating the 367 * scroll bars. 368 */ 369 public void setFirstLine(final int firstLine) { 370 if (firstLine == this.firstLine) { 371 return; 372 } 373 374 this.firstLine = firstLine; 375 if (firstLine != vertical.getValue()) { 376 updateScrollBars(); 377 } 378 painter.repaint(); 379 } 380 381 /** 382 * Returns the number of lines visible in this text area. 383 */ 384 public int getVisibleLines() { 385 return visibleLines; 386 } 387 388 /** 389 * Recalculates the number of visible lines. This should not be called 390 * directly. 391 */ 392 public void recalculateVisibleLines() { 393 if (painter == null) { 394 return; 395 } 396 397 final int height = painter.getHeight(); 398 399 // get line height 400 final int lineHeight; 401 if (painter.getFontMetrics() == null) { 402 lineHeight = painter.getDefaultLineHeight(); // default height might be wrong, take it only when needed 403 } else { 404 lineHeight = painter.getFontMetrics().getHeight(); 405 } 406 407 visibleLines = height / lineHeight; 408 updateScrollBars(); 409 } 410 411 /** 412 * Returns the horizontal offset of drawn lines. 413 */ 414 public int getHorizontalOffset() { 415 return horizontalOffset; 416 } 417 418 /** 419 * Sets the horizontal offset of drawn lines. This can be used to implement 420 * horizontal scrolling. 421 * @param horizontalOffset offset The new horizontal offset 422 */ 423 public void setHorizontalOffset(final int horizontalOffset) { 424 if (horizontalOffset == this.horizontalOffset) { 425 return; 426 } 427 428 this.horizontalOffset = horizontalOffset; 429 if (horizontalOffset != horizontal.getValue()) { 430 updateScrollBars(); 431 } 432 433 painter.repaint(); 434 } 435 436 /** 437 * A fast way of changing both the first line and horizontal offset. 438 * @param firstLine the new first line 439 * @param horizontalOffset the new horizontal offset 440 */ 441 public void setOrigin(final int firstLine, final int horizontalOffset) { 442 boolean changed = false; 443 444 if (horizontalOffset != this.horizontalOffset) { 445 this.horizontalOffset = horizontalOffset; 446 changed = true; 447 } 448 449 if (firstLine != this.firstLine) { 450 this.firstLine = firstLine; 451 changed = true; 452 } 453 454 if (changed) { 455 updateScrollBars(); 456 painter.repaint(); 457 } 458 } 459 460 /** 461 * Ensures that the caret is visible by scrolling the text area if 462 * necessary. 463 */ 464 public void scrollToCaret() { 465 final int line = getCaretLine(); 466 final int lineStart = getLineStartOffset(line); 467 final int offset = Math.max(0, Math.min(getLineLength(line) - 1, getCaretPosition() - lineStart)); 468 469 scrollTo(line, offset); 470 } 471 472 /** 473 * Sets the focus to this TextArea, so this component is instantly 474 * registered for key press events. The graphics context must be fully 475 * initialized before calling this method. 476 */ 477 public void setEditingFocus() { 478 try { 479 requestFocus(); 480 setCaretVisible(true); 481 focusedComponent = this; 482 setCaretPosition(0); // set caret to 0, 0 coordinates 483 } catch (final NullPointerException e) { 484 log.error("Null Pointer Exception in JEditTextArea.setEditingFocus()"); 485 } 486 } 487 488 /** 489 * Ensures that the specified line and offset is visible by scrolling the 490 * text area if necessary. 491 * @param line the line to scroll to 492 * @param offset the offset in the line to scroll to 493 */ 494 public void scrollTo(final int line, final int offset) { 495 // visibleLines == 0 before the component is realized 496 // we can't do any proper scrolling then, so we have 497 // this hack... 498 if (visibleLines == 0) { 499 setFirstLine(Math.max(0, line - electricScroll)); 500 return; 501 } 502 503 int newFirstLine = firstLine; 504 int newHorizontalOffset = horizontalOffset; 505 506 if (line < firstLine + electricScroll) { 507 newFirstLine = Math.max(0, line - electricScroll); 508 } else if (line + electricScroll >= firstLine + visibleLines) { 509 newFirstLine = (line - visibleLines) + electricScroll + 1; 510 if (newFirstLine + visibleLines >= getLineCount()) { 511 newFirstLine = getLineCount() - visibleLines; 512 } 513 if (newFirstLine < 0) { 514 newFirstLine = 0; 515 } 516 } 517 518 final int x = offsetToX2(line, offset); 519 final int width = painter.getFontMetrics().charWidth('w'); 520 521 if (x < 0) { 522 newHorizontalOffset = Math.min(0, horizontalOffset - x + width + 5); 523 } else if (x + width >= painter.getWidth()) { 524 newHorizontalOffset = horizontalOffset + (painter.getWidth() - x) - width - 5; 525 } 526 527 setOrigin(newFirstLine, newHorizontalOffset); 528 } 529 530 /** 531 * Converts a line index to a y co-ordinate. 532 * @param line the line 533 */ 534 public int lineToY(final int line) { 535 final FontMetrics fm = painter.getFontMetrics(); 536 return (line - firstLine) * fm.getHeight() - (fm.getLeading() + fm.getMaxDescent()); 537 } 538 539 /** 540 * Converts a y co-ordinate to a line index. 541 * @param y the y co-ordinate 542 */ 543 public int yToLine(final int y) { 544 final FontMetrics fm = painter.getFontMetrics(); 545 final int height = fm.getHeight(); 546 return Math.max(0, Math.min(getLineCount() - 1, y / height + firstLine)); 547 } 548 549 /** 550 * Converts an offset in a line into an x co-ordinate. This is a slow 551 * version that can be used any time. 552 * @param line the line 553 * @param offset the offset, from the start of the line 554 */ 555 public int offsetToX(final int line, final int offset) { 556 // don't use cached tokens 557 painter.setCurrentLineTokens(null); 558 return offsetToX2(line, offset); 559 } 560 561 /** 562 * Converts an offset in a line into an x co-ordinate. This is a fast 563 * version that should only be used if no changes were made to the text 564 * since the last repaint. 565 * @param line the line 566 * @param offset the offset, from the start of the line 567 */ 568 public int offsetToX2(final int line, final int offset) { 569 final TokenMarker tokenMarker = getTokenMarker(); 570 571 /* Use painter's cached info for speed */ 572 FontMetrics fm = painter.getFontMetrics(); 573 574 getLineText(line, lineSegment); 575 576 final int segmentOffset = lineSegment.offset; 577 int x = horizontalOffset; 578 579 /* If syntax coloring is disabled, do simple translation */ 580 if (tokenMarker == null) { 581 lineSegment.count = offset; 582 return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0); 583 } else { 584 /* If syntax coloring is enabled, we have to do this because 585 * tokens can vary in width */ 586 final List<Token> tokens; 587 if (painter.getCurrentLineIndex() == line && painter.getCurrentLineTokens() != null) { 588 tokens = painter.getCurrentLineTokens(); 589 } else { 590 painter.setCurrentLineIndex(line); 591 tokens = tokenMarker.markTokens(lineSegment, line); 592 painter.setCurrentLineTokens(tokens); 593 } 594 595 final Font defaultFont = painter.getFont(); 596 final SyntaxStyles styles = painter.getStyles(); 597 598 for (final Token token : tokens) { 599 final byte id = token.getId(); 600 if (id == Token.NULL) { 601 fm = painter.getFontMetrics(); 602 } else { 603 fm = styles.getStyle(id).getFontMetrics(defaultFont, painter.getGraphics()); 604 } 605 606 final int length = token.getLength(); 607 608 if (offset + segmentOffset < lineSegment.offset + length) { 609 lineSegment.count = offset - (lineSegment.offset - segmentOffset); 610 return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0); 611 } 612 lineSegment.count = length; 613 x += Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0); 614 lineSegment.offset += length; 615 } 616 return x; 617 } 618 } 619 620 /** 621 * Converts an x co-ordinate to an offset within a line. 622 * @param line the line 623 * @param x the x co-ordinate 624 */ 625 public int xToOffset(final int line, final int x) { 626 final TokenMarker tokenMarker = getTokenMarker(); 627 628 /* Use painter's cached info for speed */ 629 FontMetrics fm = painter.getFontMetrics(); 630 631 getLineText(line, lineSegment); 632 633 final char[] segmentArray = lineSegment.array; 634 final int segmentOffset = lineSegment.offset; 635 final int segmentCount = lineSegment.count; 636 637 int width = horizontalOffset; 638 639 if (tokenMarker == null) { 640 for (int i = 0; i < segmentCount; i++) { 641 final char c = segmentArray[i + segmentOffset]; 642 final int charWidth; 643 if (c == '\t') { 644 charWidth = (int) painter.nextTabStop((float) width, i) - width; 645 } else { 646 charWidth = fm.charWidth(c); 647 } 648 649 if (painter.isBlockCaretEnabled()) { 650 if (x - charWidth <= width) { 651 return i; 652 } 653 } else { 654 if (x - charWidth / 2 <= width) { 655 return i; 656 } 657 } 658 659 width += charWidth; 660 } 661 662 return segmentCount; 663 } else { 664 final List<Token> tokens; 665 if (painter.getCurrentLineIndex() == line && painter.getCurrentLineTokens() != null) { 666 tokens = painter.getCurrentLineTokens(); 667 } else { 668 painter.setCurrentLineIndex(line); 669 tokens = tokenMarker.markTokens(lineSegment, line); 670 painter.setCurrentLineTokens(tokens); 671 } 672 673 int offset = 0; 674 final Font defaultFont = painter.getFont(); 675 final SyntaxStyles styles = painter.getStyles(); 676 677 for (final Token token : tokens) { 678 final byte id = token.getId(); 679 if (id == Token.NULL) { 680 fm = painter.getFontMetrics(); 681 } else { 682 fm = styles.getStyle(id).getFontMetrics(defaultFont, painter.getGraphics()); 683 } 684 685 final int length = token.getLength(); 686 687 for (int i = 0; i < length; i++) { 688 final char c = segmentArray[segmentOffset + offset + i]; 689 final int charWidth; 690 if (c == '\t') { 691 charWidth = (int) painter.nextTabStop((float) width, offset + i) - width; 692 } else { 693 charWidth = fm.charWidth(c); 694 } 695 696 if (painter.isBlockCaretEnabled()) { 697 if (x - charWidth <= width) { 698 return offset + i; 699 } 700 } else { 701 if (x - charWidth / 2 <= width) { 702 return offset + i; 703 } 704 } 705 706 width += charWidth; 707 } 708 709 offset += length; 710 } 711 return offset; 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(final int x, final int y) { 721 final int line = yToLine(y); 722 final int start = getLineStartOffset(line); 723 return start + xToOffset(line, x); 724 } 725 726 /** 727 * Returns the document this text area is editing. 728 */ 729 @NotNull 730 @SuppressWarnings("NullableProblems") 731 public SyntaxDocument getDocument() { 732 if (document == null) { 733 throw new IllegalStateException(); 734 } 735 return document; 736 } 737 738 /** 739 * Sets the document this text area is editing. 740 * @param document the document 741 */ 742 public final void setDocument(@Nullable final SyntaxDocument document) { 743 if (this.document == document) { 744 return; 745 } 746 747 if (this.document != null) { 748 this.document.removeDocumentListener(documentHandler); 749 } 750 751 this.document = document; 752 753 document.addDocumentListener(documentHandler); 754 755 select(0, 0); 756 updateScrollBars(); 757 painter.repaint(); 758 } 759 760 /** 761 * Returns the document's token marker. Equivalent to calling 762 * <code>getDocument().getTokenMarker()</code>. 763 */ 764 @Nullable 765 public TokenMarker getTokenMarker() { 766 return document.getTokenMarker(); 767 } 768 769 /** 770 * Returns the length of the document. Equivalent to calling 771 * <code>getDocument().getLength()</code>. 772 */ 773 public int getDocumentLength() { 774 return document.getLength(); 775 } 776 777 /** 778 * Returns the number of lines in the document. 779 */ 780 public int getLineCount() { 781 return document.getDefaultRootElement().getElementCount(); 782 } 783 784 /** 785 * Returns the line containing the specified offset. 786 * @param offset the offset 787 */ 788 public int getLineOfOffset(final int offset) { 789 return document.getDefaultRootElement().getElementIndex(offset); 790 } 791 792 /** 793 * Returns the start offset of the specified line. 794 * @param line the line 795 * @return the start offset of the specified line, or -1 if the line is 796 * invalid 797 */ 798 public int getLineStartOffset(final int line) { 799 final Element lineElement = document.getDefaultRootElement().getElement(line); 800 if (lineElement == null) { 801 return -1; 802 } else { 803 return lineElement.getStartOffset(); 804 } 805 } 806 807 /** 808 * Returns the end offset of the specified line. 809 * @param line the line 810 * @return the end offset of the specified line, or -1 if the line is 811 * invalid 812 */ 813 public int getLineEndOffset(final int line) { 814 final Element lineElement = document.getDefaultRootElement().getElement(line); 815 if (lineElement == null) { 816 return -1; 817 } else { 818 return lineElement.getEndOffset(); 819 } 820 } 821 822 /** 823 * Returns the length of the specified line. 824 * @param line the line 825 */ 826 public int getLineLength(final int line) { 827 final Element lineElement = document.getDefaultRootElement().getElement(line); 828 if (lineElement == null) { 829 return -1; 830 } else { 831 return lineElement.getEndOffset() - lineElement.getStartOffset() - 1; 832 } 833 } 834 835 /** 836 * Returns the entire text of this text area. 837 */ 838 @NotNull 839 public String getText() { 840 try { 841 return document.getText(0, document.getLength()); 842 } catch (final BadLocationException bl) { 843 bl.printStackTrace(); 844 return ""; 845 } 846 } 847 848 /** 849 * Sets the entire text of this text area. 850 */ 851 public void setText(@NotNull final String text) { 852 try { 853 SyntaxDocument.beginCompoundEdit(); 854 document.remove(0, document.getLength()); 855 document.insertString(0, text, null); 856 } catch (final BadLocationException bl) { 857 bl.printStackTrace(); 858 } finally { 859 SyntaxDocument.endCompoundEdit(); 860 } 861 } 862 863 /** 864 * Returns the specified substring of the document. 865 * @param start the start offset 866 * @param len the length of the substring 867 * @return the substring, or null if the offsets are invalid 868 */ 869 @Nullable 870 public String getText(final int start, final int len) { 871 try { 872 return document.getText(start, len); 873 } catch (final BadLocationException bl) { 874 bl.printStackTrace(); 875 return null; 876 } 877 } 878 879 /** 880 * Copies the specified substring of the document into a segment. If the 881 * offsets are invalid, the segment will contain a null string. 882 * @param start the start offset 883 * @param len the length of the substring 884 * @param segment the segment 885 */ 886 public void getText(final int start, final int len, @NotNull final Segment segment) { 887 try { 888 document.getText(start, len, segment); 889 } catch (final BadLocationException bl) { 890 bl.printStackTrace(); 891 segment.offset = 0; 892 segment.count = 0; 893 } 894 } 895 896 /** 897 * Returns the text on the specified line. 898 * @param lineIndex the line 899 * @return the text, or null if the line is invalid 900 */ 901 @NotNull 902 public CharSequence getLineText(final int lineIndex) { 903 final int start = getLineStartOffset(lineIndex); 904 return getText(start, getLineEndOffset(lineIndex) - start - 1); 905 } 906 907 /** 908 * Copies the text on the specified line into a segment. If the line is 909 * invalid, the segment will contain a null string. 910 * @param lineIndex the line 911 */ 912 public void getLineText(final int lineIndex, @NotNull final Segment segment) { 913 final int start = getLineStartOffset(lineIndex); 914 getText(start, getLineEndOffset(lineIndex) - start - 1, segment); 915 } 916 917 /** 918 * Returns the selection start offset. 919 */ 920 public int getSelectionStart() { 921 return selectionStart; 922 } 923 924 /** 925 * Returns the selection start line. 926 */ 927 public int getSelectionStartLine() { 928 return selectionStartLine; 929 } 930 931 /** 932 * Returns the selection end offset. 933 */ 934 public int getSelectionEnd() { 935 return selectionEnd; 936 } 937 938 /** 939 * Returns the selection end line. 940 */ 941 public int getSelectionEndLine() { 942 return selectionEndLine; 943 } 944 945 /** 946 * Returns the caret position. This will either be the selection start or 947 * the selection end, depending on which direction the selection was made 948 * in. 949 */ 950 public int getCaretPosition() { 951 return biasLeft ? selectionStart : selectionEnd; 952 } 953 954 /** 955 * Returns the caret line. 956 */ 957 public int getCaretLine() { 958 return biasLeft ? selectionStartLine : selectionEndLine; 959 } 960 961 /** 962 * Returns the mark position. This will be the opposite selection bound to 963 * the caret position. 964 * @see #getCaretPosition() 965 */ 966 public int getMarkPosition() { 967 return biasLeft ? selectionEnd : selectionStart; 968 } 969 970 /** 971 * Sets the caret position. The new selection will consist of the caret 972 * position only (hence no text will be selected). 973 * @param caret the caret position 974 * @see #select(int, int) 975 */ 976 public void setCaretPosition(final int caret) { 977 select(caret, caret); 978 } 979 980 /** 981 * Selects all text in the document. 982 */ 983 public void selectAll() { 984 select(0, getDocumentLength()); 985 } 986 987 /** 988 * Selects from the start offset to the end offset. This is the general 989 * selection method used by all other selecting methods. The caret position 990 * will be start if start < end, and end if end > start. 991 * @param start the start offset 992 * @param end the end offset 993 */ 994 public void select(final int start, final int end) { 995 final int newStart; 996 final int newEnd; 997 final boolean newBias; 998 if (start <= end) { 999 newStart = start; 1000 newEnd = end; 1001 newBias = false; 1002 } else { 1003 newStart = end; 1004 newEnd = start; 1005 newBias = true; 1006 } 1007 1008 if (newStart < 0 || newEnd > getDocumentLength()) { 1009 throw new IllegalArgumentException("Bounds out of range: " + newStart + ", " + newEnd); 1010 } 1011 1012 // If the new position is the same as the old, we don't 1013 // do all this crap, however we still do the stuff at 1014 // the end (clearing magic position, scrolling) 1015 if (newStart != selectionStart || newEnd != selectionEnd || newBias != biasLeft) { 1016 final int newStartLine = getLineOfOffset(newStart); 1017 final int newEndLine = getLineOfOffset(newEnd); 1018 1019 if (painter.isBracketHighlightEnabled()) { 1020 if (bracketLine != -1) { 1021 painter.invalidateLine(bracketLine); 1022 } 1023 1024 updateBracketHighlight(end); 1025 if (bracketLine != -1) { 1026 painter.invalidateLine(bracketLine); 1027 } 1028 } 1029 1030 painter.invalidateLineRange(selectionStartLine, selectionEndLine); 1031 painter.invalidateLineRange(newStartLine, newEndLine); 1032 1033 SyntaxDocument.addUndoableEdit(new CaretUndo(selectionStart, selectionEnd)); 1034 1035 selectionStart = newStart; 1036 selectionEnd = newEnd; 1037 selectionStartLine = newStartLine; 1038 selectionEndLine = newEndLine; 1039 biasLeft = newBias; 1040 } 1041 1042 // When the user is typing, etc, we don't want the caret 1043 // to blink 1044 blink = true; 1045 caretTimer.restart(); 1046 1047 // Disable rectangle select if selection start = selection end 1048 if (selectionStart == selectionEnd) { 1049 rectangleSelect = false; 1050 } 1051 1052 // Clear the `magic' caret position used by up/down 1053 magicCaret = -1; 1054 1055 scrollToCaret(); 1056 } 1057 1058 /** 1059 * Returns the selected text, or null if no selection is active. 1060 */ 1061 @Nullable 1062 public String getSelectedText() { 1063 if (selectionStart == selectionEnd) { 1064 return null; 1065 } 1066 1067 if (rectangleSelect) { 1068 // Return each row of the selection on a new line 1069 1070 final Element map = document.getDefaultRootElement(); 1071 1072 int start = selectionStart - map.getElement(selectionStartLine).getStartOffset(); 1073 int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset(); 1074 1075 // Certain rectangles satisfy this condition... 1076 if (end < start) { 1077 final int tmp = end; 1078 end = start; 1079 start = tmp; 1080 } 1081 1082 final StringBuilder buf = new StringBuilder(); 1083 final Segment seg = new Segment(); 1084 1085 for (int i = selectionStartLine; i <= selectionEndLine; i++) { 1086 final Element lineElement = map.getElement(i); 1087 int lineStart = lineElement.getStartOffset(); 1088 final int lineEnd = lineElement.getEndOffset() - 1; 1089 1090 lineStart = Math.min(lineStart + start, lineEnd); 1091 final int lineLen = Math.min(end - start, lineEnd - lineStart); 1092 1093 getText(lineStart, lineLen, seg); 1094 buf.append(seg.array, seg.offset, seg.count); 1095 1096 if (i != selectionEndLine) { 1097 buf.append('\n'); 1098 } 1099 } 1100 1101 return buf.toString(); 1102 } else { 1103 return getText(selectionStart, selectionEnd - selectionStart); 1104 } 1105 } 1106 1107 /** 1108 * Replaces the selection with the specified text. 1109 * @param selectedText the replacement text for the selection 1110 */ 1111 public void setSelectedText(@NotNull final String selectedText) { 1112 if (!editable) { 1113 throw new InternalError("Text component read only"); 1114 } 1115 1116 SyntaxDocument.beginCompoundEdit(); 1117 1118 try { 1119 if (rectangleSelect) { 1120 final Element map = document.getDefaultRootElement(); 1121 1122 int start = selectionStart - map.getElement(selectionStartLine).getStartOffset(); 1123 int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset(); 1124 1125 // Certain rectangles satisfy this condition... 1126 if (end < start) { 1127 final int tmp = end; 1128 end = start; 1129 start = tmp; 1130 } 1131 1132 int lastNewline = 0; 1133 int currNewline = 0; 1134 1135 for (int i = selectionStartLine; i <= selectionEndLine; i++) { 1136 final Element lineElement = map.getElement(i); 1137 final int lineStart = lineElement.getStartOffset(); 1138 final int lineEnd = lineElement.getEndOffset() - 1; 1139 final int rectangleStart = Math.min(lineEnd, lineStart + start); 1140 1141 document.remove(rectangleStart, Math.min(lineEnd - rectangleStart, end - start)); 1142 1143 if (selectedText == null) { 1144 continue; 1145 } 1146 1147 currNewline = selectedText.indexOf('\n', lastNewline); 1148 if (currNewline == -1) { 1149 currNewline = selectedText.length(); 1150 } 1151 1152 document.insertString(rectangleStart, selectedText.substring(lastNewline, currNewline), null); 1153 1154 lastNewline = Math.min(selectedText.length(), currNewline + 1); 1155 } 1156 1157 if (selectedText != null && currNewline != selectedText.length()) { 1158 final int offset = map.getElement(selectionEndLine).getEndOffset() - 1; 1159 document.insertString(offset, "\n", null); 1160 document.insertString(offset + 1, selectedText.substring(currNewline + 1), null); 1161 } 1162 } else { 1163 document.remove(selectionStart, selectionEnd - selectionStart); 1164 if (selectedText != null) { 1165 document.insertString(selectionStart, selectedText, null); 1166 } 1167 } 1168 } catch (final BadLocationException bl) { 1169 bl.printStackTrace(); 1170 throw new InternalError("Cannot replace selection"); 1171 } finally { 1172 // No matter what happens... stops us from leaving document 1173 // in a bad state 1174 SyntaxDocument.endCompoundEdit(); 1175 } 1176 1177 setCaretPosition(selectionEnd); 1178 } 1179 1180 /** 1181 * Returns true if this text area is editable, false otherwise. 1182 */ 1183 public boolean isEditable() { 1184 return editable; 1185 } 1186 1187 /** 1188 * Returns the `magic' caret position. This can be used to preserve the 1189 * column position when moving up and down lines. 1190 */ 1191 public int getMagicCaretPosition() { 1192 return magicCaret; 1193 } 1194 1195 /** 1196 * Sets the `magic' caret position. This can be used to preserve the column 1197 * position when moving up and down lines. 1198 * @param magicCaret the magic caret position 1199 */ 1200 public void setMagicCaretPosition(final int magicCaret) { 1201 this.magicCaret = magicCaret; 1202 } 1203 1204 /** 1205 * Similar to <code>setSelectedText()</code>, but overstrikes the 1206 * appropriate number of characters if overwrite mode is enabled. 1207 * @param str the string 1208 * @see #setSelectedText(String) 1209 * @see #isOverwriteEnabled() 1210 */ 1211 public void overwriteSetSelectedText(@NotNull final String str) { 1212 // Don't overstrike if there is a selection 1213 if (!overwrite || selectionStart != selectionEnd) { 1214 setSelectedText(str); 1215 return; 1216 } 1217 1218 // Don't overstrike if we're on the end of 1219 // the line 1220 final int caret = getCaretPosition(); 1221 final int caretLineEnd = getLineEndOffset(getCaretLine()); 1222 if (caretLineEnd - caret <= str.length()) { 1223 setSelectedText(str); 1224 return; 1225 } 1226 1227 SyntaxDocument.beginCompoundEdit(); 1228 1229 try { 1230 document.remove(caret, str.length()); 1231 document.insertString(caret, str, null); 1232 } catch (final BadLocationException bl) { 1233 bl.printStackTrace(); 1234 } finally { 1235 SyntaxDocument.endCompoundEdit(); 1236 } 1237 } 1238 1239 /** 1240 * Returns true if overwrite mode is enabled, false otherwise. 1241 */ 1242 public boolean isOverwriteEnabled() { 1243 return overwrite; 1244 } 1245 1246 /** 1247 * Sets if overwrite mode should be enabled. 1248 * @param overwrite true if overwrite mode should be enabled, false 1249 * otherwise 1250 */ 1251 public void setOverwriteEnabled(final boolean overwrite) { 1252 this.overwrite = overwrite; 1253 painter.invalidateSelectedLines(); 1254 } 1255 1256 /** 1257 * Returns true if the selection is rectangular, false otherwise. 1258 */ 1259 public boolean isSelectionRectangular() { 1260 return rectangleSelect; 1261 } 1262 1263 /** 1264 * Sets if the selection should be rectangular. 1265 * @param rectangleSelect true if the selection should be rectangular, false 1266 * otherwise 1267 */ 1268 public void setSelectionRectangular(final boolean rectangleSelect) { 1269 this.rectangleSelect = rectangleSelect; 1270 painter.invalidateSelectedLines(); 1271 } 1272 1273 /** 1274 * Returns the position of the highlighted bracket (the bracket matching the 1275 * one before the caret). 1276 */ 1277 public int getBracketPosition() { 1278 return bracketPosition; 1279 } 1280 1281 /** 1282 * Returns the line of the highlighted bracket (the bracket matching the one 1283 * before the caret). 1284 */ 1285 public int getBracketLine() { 1286 return bracketLine; 1287 } 1288 1289 /** 1290 * Deletes the selected text from the text area and places it into the 1291 * clipboard. 1292 */ 1293 public void cut() { 1294 if (editable) { 1295 copy(); 1296 setSelectedText(""); 1297 } 1298 } 1299 1300 /** 1301 * Places the selected text into the clipboard. 1302 */ 1303 public void copy() { 1304 if (selectionStart != selectionEnd) { 1305 final Clipboard clipboard = getToolkit().getSystemClipboard(); 1306 1307 final String selection = getSelectedText(); 1308 1309 final int repeatCount = inputHandler.getRepeatCount(); 1310 final StringBuilder buf = new StringBuilder(); 1311 for (int i = 0; i < repeatCount; i++) { 1312 buf.append(selection); 1313 } 1314 1315 clipboard.setContents(new StringSelection(buf.toString()), null); 1316 } 1317 } 1318 1319 /** 1320 * Inserts the clipboard contents into the text. 1321 */ 1322 public void paste() { 1323 if (editable) { 1324 final Clipboard clipboard = getToolkit().getSystemClipboard(); 1325 try { 1326 // The MacOS MRJ doesn't convert \r to \n, 1327 // so do it here 1328 final String selection = ((String) clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor)).replace('\r', '\n'); 1329 1330 final int repeatCount = inputHandler.getRepeatCount(); 1331 final StringBuilder buf = new StringBuilder(); 1332 for (int i = 0; i < repeatCount; i++) { 1333 buf.append(selection); 1334 } 1335 setSelectedText(buf.toString()); 1336 } catch (final IOException e) { 1337 getToolkit().beep(); 1338 log.error("Clipboard does not contain a string"); 1339 } catch (final UnsupportedFlavorException e) { 1340 getToolkit().beep(); 1341 log.error("Clipboard does not contain a string"); 1342 } 1343 } 1344 } 1345 1346 /** 1347 * Called by the AWT when this component is removed from it's parent. This 1348 * stops clears the currently focused component. 1349 */ 1350 @Override 1351 public void removeNotify() { 1352 super.removeNotify(); 1353 if (focusedComponent == this) { 1354 focusedComponent = null; 1355 } 1356 } 1357 1358 /** 1359 * Forwards key events directly to the input handler. This is slightly 1360 * faster than using a KeyListener because some Swing overhead is avoided. 1361 */ 1362 @Override 1363 public void processKeyEvent(@NotNull final KeyEvent e) { 1364 if (inputHandler == null) { 1365 return; 1366 } 1367 1368 switch (e.getID()) { 1369 case KeyEvent.KEY_TYPED: 1370 inputHandler.keyTyped(e); 1371 break; 1372 1373 case KeyEvent.KEY_PRESSED: 1374 inputHandler.keyPressed(e); 1375 break; 1376 1377 case KeyEvent.KEY_RELEASED: 1378 inputHandler.keyReleased(e); 1379 break; 1380 } 1381 1382 super.processKeyEvent(e); 1383 } 1384 1385 void updateBracketHighlight(final int newCaretPosition) { 1386 if (newCaretPosition == 0) { 1387 bracketPosition = -1; 1388 bracketLine = -1; 1389 return; 1390 } 1391 1392 try { 1393 final int offset = TextUtilities.findMatchingBracket(document, newCaretPosition - 1); 1394 if (offset != -1) { 1395 bracketLine = getLineOfOffset(offset); 1396 bracketPosition = offset - getLineStartOffset(bracketLine); 1397 return; 1398 } 1399 } catch (final BadLocationException bl) { 1400 bl.printStackTrace(); 1401 } 1402 1403 bracketLine = -1; 1404 bracketPosition = -1; 1405 } 1406 1407 void documentChanged(@NotNull final DocumentEvent evt) { 1408 final DocumentEvent.ElementChange ch = evt.getChange(document.getDefaultRootElement()); 1409 1410 final int count; 1411 if (ch == null) { 1412 count = 0; 1413 } else { 1414 count = ch.getChildrenAdded().length - ch.getChildrenRemoved().length; 1415 } 1416 1417 final int line = getLineOfOffset(evt.getOffset()); 1418 if (count == 0) { 1419 painter.invalidateLine(line); 1420 } else if (line < firstLine) { 1421 // do magic stuff 1422 setFirstLine(firstLine + count); 1423 // end of magic stuff 1424 } else { 1425 painter.invalidateLineRange(line, firstLine + visibleLines); 1426 updateScrollBars(); 1427 } 1428 } 1429 1430 /** 1431 * Return whether the text content has been modified from the "unmodified" 1432 * state. 1433 * @return <code>true</code> if the text content has been modified, or 1434 * <code>false</code> if it is unmodified 1435 */ 1436 public boolean isModified() { 1437 return !unmodifiedText.equals(getText()); 1438 } 1439 1440 /** 1441 * Reset the "modified" state. 1442 */ 1443 public void resetModified() { 1444 unmodifiedText = getText(); 1445 } 1446 1447 private static class CaretBlinker implements ActionListener { 1448 1449 @Override 1450 public void actionPerformed(@NotNull final ActionEvent e) { 1451 if (focusedComponent != null && focusedComponent.hasFocus()) { 1452 focusedComponent.blinkCaret(); 1453 } 1454 } 1455 1456 } 1457 1458 private class MutableCaretEvent extends CaretEvent { 1459 1460 /** 1461 * Serial Version UID. 1462 */ 1463 private static final long serialVersionUID = 1L; 1464 1465 MutableCaretEvent() { 1466 super(JEditTextArea.this); 1467 } 1468 1469 @Override 1470 public int getDot() { 1471 return getCaretPosition(); 1472 } 1473 1474 @Override 1475 public int getMark() { 1476 return getMarkPosition(); 1477 } 1478 1479 } 1480 1481 private class AdjustHandler implements AdjustmentListener { 1482 1483 @Override 1484 public void adjustmentValueChanged(@NotNull final AdjustmentEvent e) { 1485 if (!scrollBarsInitialized) { 1486 return; 1487 } 1488 1489 // If this is not done, mousePressed events accumulate 1490 // and the result is that scrolling doesn't stop after 1491 // the mouse is released 1492 SwingUtilities.invokeLater(new Runnable() { 1493 1494 @Override 1495 public void run() { 1496 if (e.getAdjustable() == vertical) { 1497 setFirstLine(vertical.getValue()); 1498 } else { 1499 setHorizontalOffset(-horizontal.getValue()); 1500 } 1501 } 1502 1503 }); 1504 } 1505 1506 } 1507 1508 private class ComponentHandler extends ComponentAdapter { 1509 1510 @Override 1511 public void componentResized(@NotNull final ComponentEvent e) { 1512 recalculateVisibleLines(); 1513 scrollBarsInitialized = true; 1514 } 1515 1516 } 1517 1518 private class DocumentHandler implements DocumentListener { 1519 1520 @Override 1521 public void insertUpdate(@NotNull final DocumentEvent e) { 1522 documentChanged(e); 1523 1524 final int offset = e.getOffset(); 1525 final int length = e.getLength(); 1526 1527 final int newStart; 1528 if (selectionStart > offset || (selectionStart == selectionEnd && selectionStart == offset)) { 1529 newStart = selectionStart + length; 1530 } else { 1531 newStart = selectionStart; 1532 } 1533 1534 final int newEnd; 1535 if (selectionEnd >= offset) { 1536 newEnd = selectionEnd + length; 1537 } else { 1538 newEnd = selectionEnd; 1539 } 1540 1541 select(newStart, newEnd); 1542 } 1543 1544 @Override 1545 public void removeUpdate(@NotNull final DocumentEvent e) { 1546 documentChanged(e); 1547 1548 final int offset = e.getOffset(); 1549 final int length = e.getLength(); 1550 1551 final int newStart; 1552 if (selectionStart > offset) { 1553 if (selectionStart > offset + length) { 1554 newStart = selectionStart - length; 1555 } else { 1556 newStart = offset; 1557 } 1558 } else { 1559 newStart = selectionStart; 1560 } 1561 1562 final int newEnd; 1563 if (selectionEnd > offset) { 1564 if (selectionEnd > offset + length) { 1565 newEnd = selectionEnd - length; 1566 } else { 1567 newEnd = offset; 1568 } 1569 } else { 1570 newEnd = selectionEnd; 1571 } 1572 1573 select(newStart, newEnd); 1574 } 1575 1576 @Override 1577 public void changedUpdate(@NotNull final DocumentEvent e) { 1578 } 1579 1580 } 1581 1582 private class DragHandler implements MouseMotionListener { 1583 1584 @Override 1585 public void mouseDragged(@NotNull final MouseEvent e) { 1586 if (popup != null && popup.isVisible()) { 1587 return; 1588 } 1589 1590 setSelectionRectangular((e.getModifiers() & InputEvent.CTRL_MASK) != 0); 1591 select(getMarkPosition(), xyToOffset(e.getX(), e.getY())); 1592 } 1593 1594 @Override 1595 public void mouseMoved(@NotNull final MouseEvent e) { 1596 } 1597 1598 } 1599 1600 private class FocusHandler implements FocusListener { 1601 1602 @Override 1603 public void focusGained(@NotNull final FocusEvent e) { 1604 setCaretVisible(true); 1605 focusedComponent = JEditTextArea.this; 1606 } 1607 1608 @Override 1609 public void focusLost(@NotNull final FocusEvent e) { 1610 setCaretVisible(false); 1611 focusedComponent = null; 1612 } 1613 1614 } 1615 1616 /** 1617 * @noinspection RefusedBequest 1618 */ 1619 private class MouseHandler extends MouseAdapter { 1620 1621 @Override 1622 public void mousePressed(@NotNull final MouseEvent e) { 1623 requestFocus(); 1624 1625 // Focus events not fired sometimes? 1626 setCaretVisible(true); 1627 focusedComponent = JEditTextArea.this; 1628 1629 if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0 && popup != null) { 1630 popup.show(painter, e.getX(), e.getY()); 1631 return; 1632 } 1633 1634 final int line = yToLine(e.getY()); 1635 final int offset = xToOffset(line, e.getX()); 1636 final int dot = getLineStartOffset(line) + offset; 1637 1638 switch (e.getClickCount()) { 1639 case 1: 1640 doSingleClick(e, dot); 1641 break; 1642 case 2: 1643 doDoubleClick(line, offset, dot); 1644 break; 1645 case 3: 1646 doTripleClick(line); 1647 break; 1648 } 1649 } 1650 1651 @Override 1652 public void mouseWheelMoved(@NotNull final MouseWheelEvent e) { 1653 final int diff; 1654 if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) { 1655 diff = e.getUnitsToScroll() * vertical.getUnitIncrement(1); 1656 } else { 1657 diff = e.getWheelRotation() * vertical.getBlockIncrement(); 1658 } 1659 vertical.setValue(vertical.getValue() + diff); 1660 } 1661 1662 private void doSingleClick(@NotNull final InputEvent evt, final int dot) { 1663 if ((evt.getModifiers() & InputEvent.SHIFT_MASK) == 0) { 1664 setCaretPosition(dot); 1665 } else { 1666 rectangleSelect = (evt.getModifiers() & InputEvent.CTRL_MASK) != 0; 1667 select(getMarkPosition(), dot); 1668 } 1669 } 1670 1671 private void doDoubleClick(final int line, final int offset, final int dot) { 1672 // Ignore empty lines 1673 if (getLineLength(line) == 0) { 1674 return; 1675 } 1676 1677 try { 1678 int bracket = TextUtilities.findMatchingBracket(document, Math.max(0, dot - 1)); 1679 if (bracket != -1) { 1680 int mark = getMarkPosition(); 1681 // Hack 1682 if (bracket > mark) { 1683 bracket++; 1684 mark--; 1685 } 1686 select(mark, bracket); 1687 return; 1688 } 1689 } catch (final BadLocationException bl) { 1690 bl.printStackTrace(); 1691 } 1692 1693 // Ok, it's not a bracket... select the word 1694 final CharSequence lineText = getLineText(line); 1695 char ch = lineText.charAt(Math.max(0, offset - 1)); 1696 1697 String noWordSep = (String) document.getProperty("noWordSep"); 1698 if (noWordSep == null) { 1699 noWordSep = ""; 1700 } 1701 1702 // If the user clicked on a non-letter char, 1703 // we select the surrounding non-letters 1704 final boolean selectNoLetter = !Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1; 1705 1706 int wordStart = 0; 1707 1708 for (int i = offset - 1; i >= 0; i--) { 1709 ch = lineText.charAt(i); 1710 if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) { 1711 wordStart = i + 1; 1712 break; 1713 } 1714 } 1715 1716 int wordEnd = lineText.length(); 1717 for (int i = offset; i < lineText.length(); i++) { 1718 ch = lineText.charAt(i); 1719 if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) { 1720 wordEnd = i; 1721 break; 1722 } 1723 } 1724 1725 final int lineStart = getLineStartOffset(line); 1726 select(lineStart + wordStart, lineStart + wordEnd); 1727 1728 /* 1729 String lineText = getLineText(line); 1730 String noWordSep = (String)document.getProperty("noWordSep"); 1731 int wordStart = TextUtilities.findWordStart(lineText, offset, noWordSep); 1732 int wordEnd = TextUtilities.findWordEnd(lineText, offset, noWordSep); 1733 1734 int lineStart = getLineStartOffset(line); 1735 select(lineStart + wordStart, lineStart + wordEnd); 1736 */ 1737 } 1738 1739 private void doTripleClick(final int line) { 1740 select(getLineStartOffset(line), getLineEndOffset(line) - 1); 1741 } 1742 1743 } 1744 1745 private class CaretUndo extends AbstractUndoableEdit { 1746 1747 /** 1748 * Serial Version UID. 1749 */ 1750 private static final long serialVersionUID = 1L; 1751 1752 private int start; 1753 1754 private int end; 1755 1756 private CaretUndo(final int start, final int end) { 1757 this.start = start; 1758 this.end = end; 1759 } 1760 1761 @Override 1762 public boolean isSignificant() { 1763 return false; 1764 } 1765 1766 @NotNull 1767 @Override 1768 public String getPresentationName() { 1769 return "caret move"; 1770 } 1771 1772 @Override 1773 public void undo() throws CannotUndoException { 1774 super.undo(); 1775 1776 select(start, end); 1777 } 1778 1779 @Override 1780 public void redo() throws CannotRedoException { 1781 super.redo(); 1782 1783 select(start, end); 1784 } 1785 1786 @Override 1787 public boolean addEdit(@NotNull final UndoableEdit anEdit) { 1788 if (anEdit instanceof CaretUndo) { 1789 final CaretUndo caretUndo = (CaretUndo) anEdit; 1790 start = caretUndo.start; 1791 end = caretUndo.end; 1792 caretUndo.die(); 1793 1794 return true; 1795 } else { 1796 return false; 1797 } 1798 } 1799 1800 } 1801 1802}