/* * TextAreaMouseHandler.java - standalone mouse handler for textarea * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 2006 Matthieu Casanova * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.textarea; import org.gjt.sp.jedit.OperatingSystem; import org.gjt.sp.jedit.TextUtilities; import org.gjt.sp.util.StandardUtilities; import javax.swing.event.MouseInputAdapter; import java.awt.event.MouseEvent; import java.awt.event.InputEvent; import java.awt.*; /** Standalone TextArea MouseHandler. * * @author Matthieu Casanova * @version $Id: TextAreaMouseHandler.java 22883 2013-03-23 17:58:56Z thomasmey $ */ public class TextAreaMouseHandler extends MouseInputAdapter { //{{{ MouseHandler constructor TextAreaMouseHandler(TextArea textArea) { this.textArea = textArea; } //}}} //{{{ mousePressed() method @Override public void mousePressed(MouseEvent evt) { showCursor(); control = (OperatingSystem.isMacOS() && evt.isMetaDown()) || (!OperatingSystem.isMacOS() && evt.isControlDown()); ctrlForRectangularSelection = true; // so that Home <mouse click> Home is not the same // as pressing Home twice in a row textArea.getInputHandler().resetLastActionCount(); quickCopyDrag = (textArea.isQuickCopyEnabled() && isMiddleButton(evt.getModifiers())); if(!quickCopyDrag) { textArea.requestFocus(); TextArea.focusedComponent = textArea; } if(textArea.getBuffer().isLoading()) return; int x = evt.getX(); int y = evt.getY(); dragStart = textArea.xyToOffset(x,y, !(textArea.getPainter().isBlockCaretEnabled() || textArea.isOverwriteEnabled())); dragStartLine = textArea.getLineOfOffset(dragStart); dragStartOffset = dragStart - textArea.getLineStartOffset( dragStartLine); if(isPopupTrigger(evt) && textArea.isRightClickPopupEnabled()) { textArea.handlePopupTrigger(evt); return; } dragged = false; textArea.blink = true; textArea.invalidateLine(textArea.getCaretLine()); clickCount = evt.getClickCount(); if(textArea.isDragEnabled() && textArea.selectionManager.insideSelection(x,y) && clickCount == 1 && !evt.isShiftDown()) { maybeDragAndDrop = true; textArea.moveCaretPosition(dragStart,false); return; } maybeDragAndDrop = false; if(quickCopyDrag) { // ignore double clicks of middle button doSingleClick(evt); } else { switch(clickCount) { case 1: doSingleClick(evt); break; case 2: doDoubleClick(); break; default: //case 3: doTripleClick(); break; } } } //}}} //{{{ doSingleClick() method protected void doSingleClick(MouseEvent evt) { int x = evt.getX(); int extraEndVirt = 0; if(textArea.chunkCache.getLineInfo( textArea.getLastScreenLine()).lastSubregion) { int dragStart = textArea.xyToOffset(x,evt.getY(), !textArea.getPainter().isBlockCaretEnabled() && !textArea.isOverwriteEnabled()); int screenLine = textArea.getScreenLineOfOffset(dragStart); ChunkCache.LineInfo lineInfo = textArea.chunkCache.getLineInfo(screenLine); int offset = textArea.getScreenLineEndOffset(screenLine); if ((1 != offset - dragStart) || (lineInfo.lastSubregion)) { offset--; } float dragStartLineWidth = textArea.offsetToXY(offset).x; if(x > dragStartLineWidth) { extraEndVirt = (int)( (x - dragStartLineWidth) / textArea.charWidth); if(!textArea.getPainter().isBlockCaretEnabled() && !textArea.isOverwriteEnabled() && (x - textArea.getHorizontalOffset()) % textArea.charWidth > textArea.charWidth / 2) { extraEndVirt++; } } } if(((control && ctrlForRectangularSelection) || textArea.isRectangularSelectionEnabled()) && textArea.isEditable()) { int screenLine = (evt.getY() / textArea.getPainter().getLineHeight()); if(screenLine > textArea.getLastScreenLine()) screenLine = textArea.getLastScreenLine(); ChunkCache.LineInfo info = textArea.chunkCache.getLineInfo(screenLine); if(info.lastSubregion && extraEndVirt != 0) { // control-click in virtual space inserts // whitespace and moves caret String whitespace = StandardUtilities .createWhiteSpace(extraEndVirt,0); textArea.getBuffer().insert(dragStart,whitespace); dragStart += whitespace.length(); } } if(evt.isShiftDown()) { textArea.resizeSelection( getSelectionPivotCaret(),dragStart,extraEndVirt, textArea.isRectangularSelectionEnabled() || (control && ctrlForRectangularSelection)); if(!quickCopyDrag) textArea.moveCaretPosition(dragStart,false); // so that shift-click-drag works dragStartLine = getSelectionPivotLine(); dragStart = getSelectionPivotCaret(); dragStartOffset = dragStart - textArea.getLineStartOffset(dragStartLine); // so that quick copy works dragged = true; return; } if(!quickCopyDrag) { Point p = textArea.offsetToXY(dragStart); // defer scrolling until mouserelease if result is off-screen textArea.moveCaretPosition(dragStart, (p.x < 0) ? TextArea.NO_SCROLL : TextArea.NORMAL_SCROLL); } if(!(textArea.isMultipleSelectionEnabled() || quickCopyDrag)) textArea.selectNone(); } //}}} //{{{ doDoubleClick() method protected void doDoubleClick() { // Ignore empty lines if(textArea.getLineLength(dragStartLine) == 0) return; String lineText = textArea.getLineText(dragStartLine); String noWordSep = textArea.getBuffer() .getStringProperty("noWordSep"); if(dragStartOffset == textArea.getLineLength(dragStartLine)) dragStartOffset--; boolean joinNonWordChars = textArea.getJoinNonWordChars(); int wordStart = TextUtilities.findWordStart(lineText,dragStartOffset, noWordSep,joinNonWordChars,false,false); int wordEnd = TextUtilities.findWordEnd(lineText, dragStartOffset+1,noWordSep, joinNonWordChars,false,false); int lineStart = textArea.getLineStartOffset(dragStartLine); Selection sel = new Selection.Range( lineStart + wordStart, lineStart + wordEnd); if(textArea.isMultipleSelectionEnabled()) textArea.addToSelection(sel); else textArea.setSelection(sel); if(quickCopyDrag) quickCopyDrag = false; textArea.moveCaretPosition(lineStart + wordEnd,false); dragged = true; } //}}} //{{{ doTripleClick() method protected void doTripleClick() { int newCaret = textArea.getLineEndOffset(dragStartLine); if(dragStartLine == textArea.getLineCount() - 1) newCaret--; Selection sel = new Selection.Range( textArea.getLineStartOffset(dragStartLine), newCaret); if(textArea.isMultipleSelectionEnabled()) textArea.addToSelection(sel); else textArea.setSelection(sel); if(quickCopyDrag) quickCopyDrag = false; textArea.moveCaretPosition(newCaret,false); dragged = true; } //}}} //{{{ mouseMoved() method @Override public void mouseMoved(MouseEvent evt) { showCursor(); } //}}} //{{{ mouseDragged() method @Override public void mouseDragged(MouseEvent evt) { if (isPopupTrigger(evt)) return; if(maybeDragAndDrop) { textArea.startDragAndDrop(evt,control); return; } if(textArea.getBuffer().isLoading()) return; TextAreaPainter painter = textArea.getPainter(); if(evt.getY() < 0) { int delta = Math.min(-1,evt.getY() / painter.getLineHeight()); textArea.setFirstLine(textArea.getFirstLine() + delta); } else if(evt.getY() >= painter.getHeight()) { int delta = Math.max(1,(evt.getY() - painter.getHeight()) / painter.getLineHeight()); if(textArea.lastLinePartial) delta--; textArea.setFirstLine(textArea.getFirstLine() + delta); } switch(clickCount) { case 1: doSingleDrag(evt); break; case 2: doDoubleDrag(evt); break; default: //case 3: doTripleDrag(evt); break; } } //}}} //{{{ doSingleDrag() method private void doSingleDrag(MouseEvent evt) { dragged = true; TextAreaPainter painter = textArea.getPainter(); int x = evt.getX(); int y = evt.getY(); if(y < 0) y = 0; else if(y >= painter.getHeight()) y = painter.getHeight() - 1; int dot = textArea.xyToOffset(x,y, (!painter.isBlockCaretEnabled() && !textArea.isOverwriteEnabled()) || quickCopyDrag); int dotLine = textArea.getLineOfOffset(dot); int extraEndVirt = 0; if(textArea.chunkCache.getLineInfo( textArea.getLastScreenLine()) .lastSubregion) { int screenLine = textArea.getScreenLineOfOffset(dot); ChunkCache.LineInfo lineInfo = textArea.chunkCache.getLineInfo(screenLine); int offset = textArea.getScreenLineEndOffset(screenLine); if ((1 != offset - dot) || (lineInfo.lastSubregion)) { offset--; } float dotLineWidth = textArea.offsetToXY(offset).x; if(x > dotLineWidth) { extraEndVirt = (int)((x - dotLineWidth) / textArea.charWidth); if(!painter.isBlockCaretEnabled() && !textArea.isOverwriteEnabled() && (x - textArea.getHorizontalOffset()) % textArea.charWidth > textArea.charWidth / 2) extraEndVirt++; } } textArea.resizeSelection(dragStart,dot,extraEndVirt, textArea.isRectangularSelectionEnabled() || (control && ctrlForRectangularSelection)); if(quickCopyDrag) { // just scroll to the dragged location textArea.scrollTo(dotLine,dot - textArea.getLineStartOffset(dotLine),false); } else { Point p = textArea.offsetToXY(dot); if(dot != textArea.getCaretPosition()) { // defer scroll to mouserelease if result is offscreen left without dragging that direction textArea.moveCaretPosition(dot, (p.x < 0 && x > 1) ? TextArea.NO_SCROLL : TextArea.NORMAL_SCROLL); } else if(p.x < 0 && x < 1) { // caret already offscreen left, user now attempting to drag left textArea.scrollToCaret(false); } if(textArea.isRectangularSelectionEnabled() && extraEndVirt != 0) { textArea.scrollTo(dotLine,dot - textArea.getLineStartOffset(dotLine) + extraEndVirt,false); } } } //}}} //{{{ doDoubleDrag() method private void doDoubleDrag(MouseEvent evt) { int markLineStart = textArea.getLineStartOffset(dragStartLine); int markLineLength = textArea.getLineLength(dragStartLine); int mark = dragStartOffset; TextAreaPainter painter = textArea.getPainter(); int pos = textArea.xyToOffset(evt.getX(), Math.max(0,Math.min(painter.getHeight(),evt.getY())), !(painter.isBlockCaretEnabled() || textArea.isOverwriteEnabled())); int line = textArea.getLineOfOffset(pos); int lineStart = textArea.getLineStartOffset(line); int lineLength = textArea.getLineLength(line); int offset = pos - lineStart; String lineText = textArea.getLineText(line); String markLineText = textArea.getLineText(dragStartLine); String noWordSep = textArea.getBuffer() .getStringProperty("noWordSep"); boolean joinNonWordChars = textArea.getJoinNonWordChars(); if(markLineStart + dragStartOffset > lineStart + offset) { if(offset != 0 && offset != lineLength) { offset = TextUtilities.findWordStart( lineText,offset,noWordSep, joinNonWordChars); } if(markLineLength != 0) { mark = TextUtilities.findWordEnd( markLineText,mark,noWordSep, joinNonWordChars); } } else { if(offset != 0 && lineLength != 0) { offset = TextUtilities.findWordEnd( lineText,offset,noWordSep, joinNonWordChars); } if(mark != 0 && mark != markLineLength) { mark = TextUtilities.findWordStart( markLineText,mark,noWordSep, joinNonWordChars); } } if(lineStart + offset == textArea.getCaretPosition()) return; textArea.resizeSelection(markLineStart + mark, lineStart + offset,0,false); textArea.moveCaretPosition(lineStart + offset,false); dragged = true; } //}}} //{{{ doTripleDrag() method private void doTripleDrag(MouseEvent evt) { TextAreaPainter painter = textArea.getPainter(); int offset = textArea.xyToOffset(evt.getX(), Math.max(0,Math.min(painter.getHeight(),evt.getY())), false); int mouseLine = textArea.getLineOfOffset(offset); int mark; int mouse; if(dragStartLine > mouseLine) { mark = textArea.getLineEndOffset(dragStartLine) - 1; if(offset == textArea.getLineEndOffset(mouseLine) - 1) mouse = offset; else mouse = textArea.getLineStartOffset(mouseLine); } else { mark = textArea.getLineStartOffset(dragStartLine); if(offset == textArea.getLineStartOffset(mouseLine)) mouse = offset; else if(offset == textArea.getLineEndOffset(mouseLine) - 1 && mouseLine != textArea.getLineCount() - 1) mouse = textArea.getLineEndOffset(mouseLine); else mouse = textArea.getLineEndOffset(mouseLine) - 1; } mouse = Math.min(textArea.getBuffer().getLength(),mouse); if(mouse == textArea.getCaretPosition()) return; textArea.resizeSelection(mark,mouse,0,false); textArea.moveCaretPosition(mouse,false); dragged = true; } //}}} //{{{ mouseReleased() method @Override public void mouseReleased(MouseEvent evt) { if(!dragged && textArea.isQuickCopyEnabled() && isMiddleButton(evt.getModifiers())) { textArea.requestFocus(); TextArea.focusedComponent = textArea; textArea.setCaretPosition(dragStart,false); } else if(maybeDragAndDrop && !textArea.isMultipleSelectionEnabled()) { textArea.selectNone(); } dragged = false; } //}}} //{{{ isPopupTrigger() method /** * Returns if the specified event is the popup trigger event. * This implements precisely defined behavior, as opposed to * MouseEvent.isPopupTrigger(). * @param evt The event * @since jEdit 4.3pre7 */ public static boolean isPopupTrigger(MouseEvent evt) { return isRightButton(evt.getModifiers()); } //}}} //{{{ isMiddleButton() method /** * @param modifiers The modifiers flag from a mouse event * @return true if the modifier match the middle button * @since jEdit 4.3pre7 */ public static boolean isMiddleButton(int modifiers) { if (OperatingSystem.isMacOS()) { if((modifiers & InputEvent.BUTTON1_MASK) != 0) return (modifiers & InputEvent.ALT_MASK) != 0; else return (modifiers & InputEvent.BUTTON2_MASK) != 0; } else return (modifiers & InputEvent.BUTTON2_MASK) != 0; } //}}} //{{{ isRightButton() method /** * @param modifiers The modifiers flag from a mouse event * @return true if the modifier match the right button * @since jEdit 4.3pre7 */ public static boolean isRightButton(int modifiers) { if (OperatingSystem.isMacOS()) { if((modifiers & InputEvent.BUTTON1_MASK) != 0) return (modifiers & InputEvent.CTRL_MASK) != 0; else return (modifiers & InputEvent.BUTTON3_MASK) != 0; } else return (modifiers & InputEvent.BUTTON3_MASK) != 0; } //}}} //{{{ Private methods //{{{ getSelectionPivotCaret() method /* * Dynamically get the "pivot" point associated with a current * selection. See inline comments for details. */ private int getSelectionPivotCaret() { int caret = textArea.caret; Selection s = textArea.getSelectionAtOffset(textArea.caret); if (s == null) return caret; // The mental model: an existing selection, and then a shift+click // somewhere else. What happens to the selection? Because a selection // exists, we need a "pivot" point. If the caret is at the start of a // selection, the end of the selection pivot point. So, a click above // the start of the caret will enlarge the selection, and a click below // the end will reverse the selection around the pivot point: the text // before the pivot will no longer be selected, and the text after it // and up to the click will be newly selected. Vice versa holds true // when the caret is at the end of the selection. If the caret is // somewhere else, just give up, and let the user fix it. caret = ( caret == s.start ? s.end : caret == s.end ? s.start : caret ); return caret; } //}}} //{{{ getSelectionPivotLine() method /* * See getSelectionPivotCaret for an explanation of this function */ private int getSelectionPivotLine() { int c = textArea.caret; int cl = textArea.caretLine; if(textArea.getSelectionCount() != 1) return cl; Selection s = textArea.getSelection(0); cl = ( c == s.start ? s.endLine : c == s.end ? s.startLine : cl ); return cl; } //}}} //}}} //{{{ Private members protected final TextArea textArea; protected int dragStartLine; protected int dragStartOffset; protected int dragStart; protected int clickCount; protected boolean dragged; protected boolean quickCopyDrag; protected boolean control; protected boolean ctrlForRectangularSelection; /* with drag and drop on, a mouse down in a selection does not immediately deselect */ protected boolean maybeDragAndDrop; //{{{ showCursor() method protected void showCursor() { textArea.getPainter().showCursor(); } //}}} //}}} }