blob: 9e26c3ed181bf50a53778078c9abafaa4659e77b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk.skin;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.wtk.ApplicationContext;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Cursor;
import org.apache.pivot.wtk.Dimensions;
import org.apache.pivot.wtk.GraphicsUtilities;
import org.apache.pivot.wtk.Insets;
import org.apache.pivot.wtk.Keyboard;
import org.apache.pivot.wtk.Mouse;
import org.apache.pivot.wtk.Platform;
import org.apache.pivot.wtk.TextArea;
import org.apache.pivot.wtk.TextAreaContentListener;
import org.apache.pivot.wtk.TextAreaListener;
import org.apache.pivot.wtk.TextAreaSelectionListener;
import org.apache.pivot.wtk.Theme;
/**
* Text area skin.
*/
public class TextAreaSkin extends ComponentSkin implements TextArea.Skin, TextAreaListener,
TextAreaContentListener, TextAreaSelectionListener {
private class BlinkCaretCallback implements Runnable {
@Override
public void run() {
caretOn = !caretOn;
if (selection == null) {
TextArea textArea = (TextArea) getComponent();
textArea.repaint(caret.x, caret.y, caret.width, caret.height);
}
}
}
private class ScrollSelectionCallback implements Runnable {
@Override
public void run() {
TextArea textArea = (TextArea) getComponent();
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
int selectionEnd = selectionStart + selectionLength - 1;
switch (scrollDirection) {
case UP: {
// Get previous offset
int index = getNextInsertionPoint(mouseX, selectionStart, scrollDirection);
if (index != -1) {
textArea.setSelection(index, selectionEnd - index + 1);
scrollCharacterToVisible(index + 1);
}
break;
}
case DOWN: {
// Get next offset
int index = getNextInsertionPoint(mouseX, selectionEnd, scrollDirection);
if (index != -1) {
// If the next character is a paragraph terminator,
// increment
// the selection
if (index < textArea.getCharacterCount()
&& textArea.getCharacterAt(index) == '\n') {
index++;
}
textArea.setSelection(selectionStart, index - selectionStart);
scrollCharacterToVisible(index - 1);
}
break;
}
default: {
break;
}
}
}
}
private int caretX = 0;
private Rectangle caret = new Rectangle();
private Area selection = null;
private boolean caretOn = false;
private int anchor = -1;
private TextArea.ScrollDirection scrollDirection = null;
private int mouseX = -1;
private BlinkCaretCallback blinkCaretCallback = new BlinkCaretCallback();
private ApplicationContext.ScheduledCallback scheduledBlinkCaretCallback = null;
private ScrollSelectionCallback scrollSelectionCallback = new ScrollSelectionCallback();
private ApplicationContext.ScheduledCallback scheduledScrollSelectionCallback = null;
private Font font;
private Color color;
private Color backgroundColor;
private Color inactiveColor;
private Color selectionColor;
private Color selectionBackgroundColor;
private Color inactiveSelectionColor;
private Color inactiveSelectionBackgroundColor;
private Insets margin;
private boolean wrapText;
private int tabWidth;
private int lineWidth;
private boolean acceptsEnter = true;
private boolean acceptsTab = false;
private Dimensions averageCharacterSize;
private ArrayList<TextAreaSkinParagraphView> paragraphViews = new ArrayList<>();
private static final int SCROLL_RATE = 30;
public TextAreaSkin() {
Theme theme = Theme.getTheme();
font = theme.getFont();
color = defaultForegroundColor();
selectionBackgroundColor = defaultForegroundColor();
inactiveSelectionBackgroundColor = defaultForegroundColor();
if (!themeIsDark()) {
selectionColor = Color.LIGHT_GRAY;
inactiveSelectionColor = Color.LIGHT_GRAY;
} else {
selectionColor = Color.DARK_GRAY;
inactiveSelectionColor = Color.DARK_GRAY;
}
backgroundColor = null;
inactiveColor = Color.GRAY;
margin = new Insets(4);
wrapText = true;
tabWidth = 4;
}
@Override
public void install(Component component) {
super.install(component);
TextArea textArea = (TextArea) component;
textArea.getTextAreaListeners().add(this);
textArea.getTextAreaContentListeners().add(this);
textArea.getTextAreaSelectionListeners().add(this);
textArea.setCursor(Cursor.TEXT);
}
@Override
public int getPreferredWidth(int height) {
int preferredWidth = 0;
if (lineWidth <= 0) {
for (TextAreaSkinParagraphView paragraphView : paragraphViews) {
paragraphView.setBreakWidth(Integer.MAX_VALUE);
preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
}
} else {
preferredWidth = averageCharacterSize.width * lineWidth;
}
preferredWidth += margin.left + margin.right;
return preferredWidth;
}
@Override
public int getPreferredHeight(int width) {
int preferredHeight = 0;
// Include margin in constraint
int breakWidth = (wrapText && width != -1) ? Math.max(width - (margin.left + margin.right),
0) : Integer.MAX_VALUE;
for (TextAreaSkinParagraphView paragraphView : paragraphViews) {
paragraphView.setBreakWidth(breakWidth);
preferredHeight += paragraphView.getHeight();
}
preferredHeight += margin.top + margin.bottom;
return preferredHeight;
}
@Override
public Dimensions getPreferredSize() {
int preferredWidth = 0;
int preferredHeight = 0;
for (TextAreaSkinParagraphView paragraphView : paragraphViews) {
paragraphView.setBreakWidth(Integer.MAX_VALUE);
preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
preferredHeight += paragraphView.getHeight();
}
preferredWidth += margin.left + margin.right;
preferredHeight += margin.top + margin.bottom;
return new Dimensions(preferredWidth, preferredHeight);
}
@SuppressWarnings("unused")
@Override
public void layout() {
TextArea textArea = (TextArea) getComponent();
int width = getWidth();
int breakWidth = (wrapText) ? Math.max(width - (margin.left + margin.right), 0)
: Integer.MAX_VALUE;
int y = margin.top;
int lastY = 0;
int lastHeight = 0;
int rowOffset = 0;
int index = 0;
for (TextAreaSkinParagraphView paragraphView : paragraphViews) {
paragraphView.setBreakWidth(breakWidth);
paragraphView.setX(margin.left);
paragraphView.setY(y);
lastY = y;
y += paragraphView.getHeight();
lastHeight = paragraphView.getHeight();
paragraphView.setRowOffset(rowOffset);
rowOffset += paragraphView.getRowCount();
index++;
}
updateSelection();
caretX = caret.x;
if (textArea.isFocused()) {
scrollCharacterToVisible(textArea.getSelectionStart());
showCaret(textArea.getSelectionLength() == 0);
} else {
showCaret(false);
}
}
@Override
public int getBaseline(int width, int height) {
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
LineMetrics lm = font.getLineMetrics("", fontRenderContext);
return Math.round(margin.top + lm.getAscent());
}
@Override
public void paint(Graphics2D graphics) {
TextArea textArea = (TextArea) getComponent();
int width = getWidth();
int height = getHeight();
// Draw the background
if (backgroundColor != null) {
graphics.setPaint(backgroundColor);
graphics.fillRect(0, 0, width, height);
}
// Draw the caret/selection
if (selection == null) {
if (caretOn && textArea.isFocused()) {
graphics.setColor(textArea.isEditable() ? color : inactiveColor);
graphics.fill(caret);
}
} else {
graphics.setColor(textArea.isFocused() && textArea.isEditable() ? selectionBackgroundColor
: inactiveSelectionBackgroundColor);
graphics.fill(selection);
}
// Draw the text
graphics.setFont(font);
graphics.translate(0, margin.top);
int breakWidth = (wrapText) ? Math.max(width - (margin.left + margin.right), 0)
: Integer.MAX_VALUE;
for (int i = 0, n = paragraphViews.getLength(); i < n; i++) {
TextAreaSkinParagraphView paragraphView = paragraphViews.get(i);
paragraphView.setBreakWidth(breakWidth);
paragraphView.validate();
int x = paragraphView.getX();
graphics.translate(x, 0);
paragraphView.paint(graphics);
graphics.translate(-x, 0);
graphics.translate(0, paragraphView.getHeight());
}
}
@Override
public boolean isOpaque() {
return (backgroundColor != null && backgroundColor.getTransparency() == Transparency.OPAQUE);
}
@Override
public int getInsertionPoint(int x, int y) {
int index = -1;
if (paragraphViews.getLength() > 0) {
TextAreaSkinParagraphView lastParagraphView = paragraphViews.get(paragraphViews.getLength() - 1);
if (y > lastParagraphView.getY() + lastParagraphView.getHeight()) {
// Select the character at x in the last row
TextAreaSkinParagraphView paragraphView = paragraphViews.get(paragraphViews.getLength() - 1);
index = paragraphView.getNextInsertionPoint(x, -1, TextArea.ScrollDirection.UP)
+ paragraphView.getParagraph().getOffset();
} else if (y < margin.top) {
// Select the character at x in the first row
TextAreaSkinParagraphView paragraphView = paragraphViews.get(0);
index = paragraphView.getNextInsertionPoint(x, -1, TextArea.ScrollDirection.DOWN);
} else {
// Select the character at x in the row at y
for (int i = 0, n = paragraphViews.getLength(); i < n; i++) {
TextAreaSkinParagraphView paragraphView = paragraphViews.get(i);
int paragraphViewY = paragraphView.getY();
if (y >= paragraphViewY && y < paragraphViewY + paragraphView.getHeight()) {
index = paragraphView.getInsertionPoint(x - paragraphView.getX(), y
- paragraphViewY)
+ paragraphView.getParagraph().getOffset();
break;
}
}
}
}
return index;
}
@Override
public int getNextInsertionPoint(int x, int from, TextArea.ScrollDirection direction) {
int index = -1;
if (paragraphViews.getLength() > 0) {
if (from == -1) {
int i = (direction == TextArea.ScrollDirection.DOWN) ? 0
: paragraphViews.getLength() - 1;
TextAreaSkinParagraphView paragraphView = paragraphViews.get(i);
index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(), -1, direction);
if (index != -1) {
index += paragraphView.getParagraph().getOffset();
}
} else {
TextArea textArea = (TextArea) getComponent();
int i = textArea.getParagraphAt(from);
TextAreaSkinParagraphView paragraphView = paragraphViews.get(i);
index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(), from
- paragraphView.getParagraph().getOffset(), direction);
if (index == -1) {
// Move to the next or previous paragraph view
if (direction == TextArea.ScrollDirection.DOWN) {
paragraphView = (i < paragraphViews.getLength() - 1) ? paragraphViews.get(i + 1)
: null;
} else {
paragraphView = (i > 0) ? paragraphViews.get(i - 1) : null;
}
if (paragraphView != null) {
index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(), -1,
direction);
}
}
if (index != -1) {
index += (paragraphView != null) ? paragraphView.getParagraph().getOffset() : 0;
}
}
}
return index;
}
@Override
public int getRowAt(int index) {
int rowIndex = -1;
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
TextAreaSkinParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
rowIndex = paragraphView.getRowAt(index - paragraphView.getParagraph().getOffset())
+ paragraphView.getRowOffset();
}
return rowIndex;
}
@Override
public int getRowOffset(int index) {
int rowOffset = -1;
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
TextAreaSkinParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
rowOffset = paragraphView.getRowOffset(index - paragraphView.getParagraph().getOffset())
+ paragraphView.getParagraph().getOffset();
}
return rowOffset;
}
@Override
public int getRowLength(int index) {
int rowLength = -1;
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
TextAreaSkinParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
rowLength = paragraphView.getRowLength(index - paragraphView.getParagraph().getOffset());
}
return rowLength;
}
@Override
public int getRowCount() {
int rowCount = 0;
for (TextAreaSkinParagraphView paragraphView : paragraphViews) {
rowCount += paragraphView.getRowCount();
}
return rowCount;
}
@Override
public Bounds getCharacterBounds(int index) {
Bounds characterBounds = null;
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
TextAreaSkinParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
characterBounds = paragraphView.getCharacterBounds(index
- paragraphView.getParagraph().getOffset());
characterBounds = new Bounds(characterBounds.x + paragraphView.getX(),
characterBounds.y + paragraphView.getY(), characterBounds.width,
characterBounds.height);
}
return characterBounds;
}
public Area getSelection() {
return selection;
}
private void scrollCharacterToVisible(int index) {
Bounds characterBounds = getCharacterBounds(index);
if (characterBounds != null) {
TextArea textArea = (TextArea) getComponent();
textArea.scrollAreaToVisible(characterBounds.x, characterBounds.y,
characterBounds.width, characterBounds.height);
}
}
/**
* @return The font of the text.
*/
public Font getFont() {
return font;
}
/**
* Sets the font of the text.
*
* @param font The new font for the text.
*/
public void setFont(Font font) {
if (font == null) {
throw new IllegalArgumentException("font is null.");
}
this.font = font;
int missingGlyphCode = font.getMissingGlyphCode();
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
new int[] { missingGlyphCode });
Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
averageCharacterSize = new Dimensions((int) Math.ceil(textBounds.getWidth()),
(int) Math.ceil(maxCharBounds.getHeight()));
invalidateComponent();
}
/**
* Sets the font of the text.
*
* @param font A {@link ComponentSkin#decodeFont(String) font specification}
*/
public final void setFont(String font) {
if (font == null) {
throw new IllegalArgumentException("font is null.");
}
setFont(decodeFont(font));
}
/**
* Sets the font of the text.
*
* @param font A dictionary {@link Theme#deriveFont describing a font}
*/
public final void setFont(Dictionary<String, ?> font) {
if (font == null) {
throw new IllegalArgumentException("font is null.");
}
setFont(Theme.deriveFont(font));
}
/**
* @return The foreground color of the text.
*/
public Color getColor() {
return color;
}
/**
* Sets the foreground color of the text.
*
* @param color The new foreground text color.
*/
public void setColor(Color color) {
if (color == null) {
throw new IllegalArgumentException("color is null.");
}
this.color = color;
repaintComponent();
}
/**
* Sets the foreground color of the text.
*
* @param color Any of the {@linkplain GraphicsUtilities#decodeColor color
* values recognized by Pivot}.
*/
public final void setColor(String color) {
if (color == null) {
throw new IllegalArgumentException("color is null.");
}
setColor(GraphicsUtilities.decodeColor(color));
}
public Color getBackgroundColor() {
return backgroundColor;
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
repaintComponent();
}
public final void setBackgroundColor(String backgroundColor) {
if (backgroundColor == null) {
throw new IllegalArgumentException("backgroundColor is null");
}
setBackgroundColor(GraphicsUtilities.decodeColor(backgroundColor));
}
public Color getInactiveColor() {
return inactiveColor;
}
public void setInactiveColor(Color inactiveColor) {
if (inactiveColor == null) {
throw new IllegalArgumentException("inactiveColor is null.");
}
this.inactiveColor = inactiveColor;
repaintComponent();
}
public final void setInactiveColor(String inactiveColor) {
if (inactiveColor == null) {
throw new IllegalArgumentException("inactiveColor is null.");
}
setColor(GraphicsUtilities.decodeColor(inactiveColor));
}
public Color getSelectionColor() {
return selectionColor;
}
public void setSelectionColor(Color selectionColor) {
if (selectionColor == null) {
throw new IllegalArgumentException("selectionColor is null.");
}
this.selectionColor = selectionColor;
repaintComponent();
}
public final void setSelectionColor(String selectionColor) {
if (selectionColor == null) {
throw new IllegalArgumentException("selectionColor is null.");
}
setSelectionColor(GraphicsUtilities.decodeColor(selectionColor));
}
public Color getSelectionBackgroundColor() {
return selectionBackgroundColor;
}
public void setSelectionBackgroundColor(Color selectionBackgroundColor) {
if (selectionBackgroundColor == null) {
throw new IllegalArgumentException("selectionBackgroundColor is null.");
}
this.selectionBackgroundColor = selectionBackgroundColor;
repaintComponent();
}
public final void setSelectionBackgroundColor(String selectionBackgroundColor) {
if (selectionBackgroundColor == null) {
throw new IllegalArgumentException("selectionBackgroundColor is null.");
}
setSelectionBackgroundColor(GraphicsUtilities.decodeColor(selectionBackgroundColor));
}
public Color getInactiveSelectionColor() {
return inactiveSelectionColor;
}
public void setInactiveSelectionColor(Color inactiveSelectionColor) {
if (inactiveSelectionColor == null) {
throw new IllegalArgumentException("inactiveSelectionColor is null.");
}
this.inactiveSelectionColor = inactiveSelectionColor;
repaintComponent();
}
public final void setInactiveSelectionColor(String inactiveSelectionColor) {
if (inactiveSelectionColor == null) {
throw new IllegalArgumentException("inactiveSelectionColor is null.");
}
setInactiveSelectionColor(GraphicsUtilities.decodeColor(inactiveSelectionColor));
}
public Color getInactiveSelectionBackgroundColor() {
return inactiveSelectionBackgroundColor;
}
public void setInactiveSelectionBackgroundColor(Color inactiveSelectionBackgroundColor) {
if (inactiveSelectionBackgroundColor == null) {
throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
}
this.inactiveSelectionBackgroundColor = inactiveSelectionBackgroundColor;
repaintComponent();
}
public final void setInactiveSelectionBackgroundColor(String inactiveSelectionBackgroundColor) {
if (inactiveSelectionBackgroundColor == null) {
throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
}
setInactiveSelectionBackgroundColor(GraphicsUtilities.decodeColor(inactiveSelectionBackgroundColor));
}
/**
* @return The amount of space between the edge of the TextArea and its text.
*/
public Insets getMargin() {
return margin;
}
/**
* Sets the amount of space between the edge of the TextArea and its text.
*
* @param margin The individual margin values for all edges.
*/
public void setMargin(Insets margin) {
if (margin == null) {
throw new IllegalArgumentException("margin is null.");
}
this.margin = margin;
invalidateComponent();
}
/**
* Sets the amount of space between the edge of the TextArea and its text.
*
* @param margin A dictionary with keys in the set {left, top, bottom,
* right}.
*/
public final void setMargin(Dictionary<String, ?> margin) {
if (margin == null) {
throw new IllegalArgumentException("margin is null.");
}
setMargin(new Insets(margin));
}
/**
* Sets the amount of space between the edge of the TextArea and its text.
*
* @param margin The single value to use for all the margins.
*/
public final void setMargin(int margin) {
setMargin(new Insets(margin));
}
/**
* Sets the amount of space between the edge of the TextArea and its text.
*
* @param margin The single value to use for all the margins.
*/
public final void setMargin(Number margin) {
if (margin == null) {
throw new IllegalArgumentException("margin is null.");
}
setMargin(margin.intValue());
}
/**
* Sets the amount of space between the edge of the TextArea and its text.
*
* @param margin A string containing an integer or a JSON dictionary with
* keys left, top, bottom, and/or right.
*/
public final void setMargin(String margin) {
if (margin == null) {
throw new IllegalArgumentException("margin is null.");
}
setMargin(Insets.decode(margin));
}
public boolean getWrapText() {
return wrapText;
}
public void setWrapText(boolean wrapText) {
this.wrapText = wrapText;
invalidateComponent();
}
public boolean getAcceptsEnter() {
return acceptsEnter;
}
public void setAcceptsEnter(boolean acceptsEnter) {
this.acceptsEnter = acceptsEnter;
}
/**
* Gets current value of style that determines the behavior of <tt>TAB</tt>
* and <tt>Ctrl-TAB</tt> characters.
*
* @return <tt>true</tt> if <tt>TAB</tt> inserts an appropriate number of
* spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
* <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
* <tt>Ctrl-TAB</tt> inserts spaces.
*/
public boolean getAcceptsTab() {
return acceptsTab;
}
/**
* Sets current value of style that determines the behavior of <tt>TAB</tt>
* and <tt>Ctrl-TAB</tt> characters.
*
* @param acceptsTab <tt>true</tt> if <tt>TAB</tt> inserts an appropriate
* number of spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
* <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
* <tt>Ctrl-TAB</tt> inserts spaces.
*/
public void setAcceptsTab(boolean acceptsTab) {
this.acceptsTab = acceptsTab;
}
@Override
public int getTabWidth() {
return tabWidth;
}
public void setTabWidth(int tabWidth) {
if (tabWidth < 0) {
throw new IllegalArgumentException("tabWidth is negative.");
}
this.tabWidth = tabWidth;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int lineWidth) {
if (this.lineWidth != lineWidth) {
this.lineWidth = lineWidth;
int missingGlyphCode = font.getMissingGlyphCode();
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
new int[] { missingGlyphCode });
Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
averageCharacterSize = new Dimensions((int) Math.ceil(textBounds.getWidth()),
(int) Math.ceil(maxCharBounds.getHeight()));
invalidateComponent();
}
}
@Override
public boolean mouseMove(Component component, int x, int y) {
boolean consumed = super.mouseMove(component, x, y);
if (Mouse.getCapturer() == component) {
TextArea textArea = (TextArea) getComponent();
Bounds visibleArea = textArea.getVisibleArea();
visibleArea = new Bounds(visibleArea.x, visibleArea.y, visibleArea.width,
visibleArea.height);
// if it's inside the visible area, stop the scroll timer
if (y >= visibleArea.y && y < visibleArea.y + visibleArea.height) {
// Stop the scroll selection timer
if (scheduledScrollSelectionCallback != null) {
scheduledScrollSelectionCallback.cancel();
scheduledScrollSelectionCallback = null;
}
scrollDirection = null;
} else {
// if it's outside the visible area, start the scroll timer
if (scheduledScrollSelectionCallback == null) {
scrollDirection = (y < visibleArea.y) ? TextArea.ScrollDirection.UP
: TextArea.ScrollDirection.DOWN;
scheduledScrollSelectionCallback = ApplicationContext.scheduleRecurringCallback(
scrollSelectionCallback, SCROLL_RATE);
// Run the callback once now to scroll the selection
// immediately
scrollSelectionCallback.run();
}
}
int index = getInsertionPoint(x, y);
if (index != -1) {
// Select the range
if (index > anchor) {
textArea.setSelection(anchor, index - anchor);
} else {
textArea.setSelection(index, anchor - index);
}
}
mouseX = x;
} else {
if (Mouse.isPressed(Mouse.Button.LEFT) && Mouse.getCapturer() == null && anchor != -1) {
// Capture the mouse so we can select text
Mouse.capture(component);
}
}
return consumed;
}
@Override
public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
boolean consumed = super.mouseDown(component, button, x, y);
TextArea textArea = (TextArea) component;
if (button == Mouse.Button.LEFT) {
anchor = getInsertionPoint(x, y);
if (anchor != -1) {
if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
// Select the range
int selectionStart = textArea.getSelectionStart();
if (anchor > selectionStart) {
textArea.setSelection(selectionStart, anchor - selectionStart);
} else {
textArea.setSelection(anchor, selectionStart - anchor);
}
} else {
// Move the caret to the insertion point
textArea.setSelection(anchor, 0);
consumed = true;
}
}
caretX = caret.x;
// Set focus to the text input
textArea.requestFocus();
}
return consumed;
}
@Override
public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
boolean consumed = super.mouseUp(component, button, x, y);
if (Mouse.getCapturer() == component) {
// Stop the scroll selection timer
if (scheduledScrollSelectionCallback != null) {
scheduledScrollSelectionCallback.cancel();
scheduledScrollSelectionCallback = null;
}
Mouse.release();
}
scrollDirection = null;
mouseX = -1;
return consumed;
}
private void selectSpan(TextArea textArea, int start) {
int rowStart = textArea.getRowOffset(start);
int rowLength = textArea.getRowLength(start);
if (start - rowStart >= rowLength) {
start = rowStart + rowLength - 1;
if (start < 0) {
return;
}
char ch = textArea.getCharacterAt(start);
if (ch == '\r' || ch == '\n') {
start--;
}
}
if (start < 0) {
return;
}
char ch = textArea.getCharacterAt(start);
int selectionStart = start;
int selectionLength = 1;
if (Character.isWhitespace(ch)) {
// Move backward to beginning of whitespace block
// but not before the beginning of the line.
do {
selectionStart--;
} while (selectionStart >= rowStart
&& Character.isWhitespace(textArea.getCharacterAt(selectionStart)));
selectionStart++;
selectionLength = start - selectionStart;
// Move forward to end of whitespace block
// but not past the end of the text or the end of line
do {
selectionLength++;
} while (selectionStart + selectionLength - rowStart < rowLength
&& Character.isWhitespace(textArea.getCharacterAt(selectionStart + selectionLength)));
} else if (Character.isJavaIdentifierPart(ch)) {
// Move backward to beginning of identifier block
do {
selectionStart--;
} while (selectionStart >= rowStart
&& Character.isJavaIdentifierPart(textArea.getCharacterAt(selectionStart)));
selectionStart++;
selectionLength = start - selectionStart;
// Move forward to end of identifier block
// but not past end of text
do {
selectionLength++;
} while (selectionStart + selectionLength - rowStart < rowLength
&& Character.isJavaIdentifierPart(textArea.getCharacterAt(selectionStart
+ selectionLength)));
} else {
return;
}
textArea.setSelection(selectionStart, selectionLength);
}
@Override
public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
boolean consumed = super.mouseClick(component, button, x, y, count);
TextArea textArea = (TextArea) component;
if (button == Mouse.Button.LEFT) {
int index = getInsertionPoint(x, y);
if (index != -1) {
if (count == 2) {
selectSpan(textArea, index);
} else if (count == 3) {
textArea.setSelection(textArea.getRowOffset(index),
textArea.getRowLength(index));
}
}
}
return consumed;
}
@Override
public boolean keyTyped(Component component, char character) {
boolean consumed = super.keyTyped(component, character);
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
if (textArea.isEditable()) {
// Ignore characters in the control range and the ASCII delete
// character as well as meta key presses
if (character > 0x1F && character != 0x7F
&& !Keyboard.isPressed(Keyboard.Modifier.META)) {
int selectionLength = textArea.getSelectionLength();
if (textArea.getCharacterCount() - selectionLength + 1 > textArea.getMaximumLength()) {
Toolkit.getDefaultToolkit().beep();
} else {
int selectionStart = textArea.getSelectionStart();
textArea.removeText(selectionStart, selectionLength);
textArea.insertText(Character.toString(character), selectionStart);
}
showCaret(true);
}
}
}
return consumed;
}
@Override
public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
boolean consumed = false;
if (paragraphViews.getLength() > 0) {
TextArea textArea = (TextArea) getComponent();
boolean commandPressed = Keyboard.isPressed(Platform.getCommandModifier());
boolean wordNavPressed = Keyboard.isPressed(Platform.getWordNavigationModifier());
boolean shiftPressed = Keyboard.isPressed(Keyboard.Modifier.SHIFT);
boolean ctrlPressed = Keyboard.isPressed(Keyboard.Modifier.CTRL);
boolean metaPressed = Keyboard.isPressed(Keyboard.Modifier.META);
boolean isEditable = textArea.isEditable();
if (keyCode == Keyboard.KeyCode.ENTER && acceptsEnter && isEditable
&& Keyboard.getModifiers() == 0) {
int index = textArea.getSelectionStart();
textArea.removeText(index, textArea.getSelectionLength());
textArea.insertText("\n", index);
consumed = true;
} else if (keyCode == Keyboard.KeyCode.DELETE && isEditable) {
int index = textArea.getSelectionStart();
if (index < textArea.getCharacterCount()) {
int count = Math.max(textArea.getSelectionLength(), 1);
textArea.removeText(index, count);
anchor = -1;
consumed = true;
}
} else if (keyCode == Keyboard.KeyCode.BACKSPACE && isEditable) {
int index = textArea.getSelectionStart();
int count = textArea.getSelectionLength();
if (count == 0 && index > 0) {
textArea.removeText(index - 1, 1);
consumed = true;
} else {
textArea.removeText(index, count);
consumed = true;
}
anchor = -1;
} else if (keyCode == Keyboard.KeyCode.TAB && (acceptsTab != ctrlPressed) && isEditable) {
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
int rowOffset = textArea.getRowOffset(selectionStart);
int linePos = selectionStart - rowOffset;
StringBuilder tabBuilder = new StringBuilder(tabWidth);
for (int i = 0; i < tabWidth - (linePos % tabWidth); i++) {
tabBuilder.append(" ");
}
if (textArea.getCharacterCount() - selectionLength + tabWidth > textArea.getMaximumLength()) {
Toolkit.getDefaultToolkit().beep();
} else {
textArea.removeText(selectionStart, selectionLength);
textArea.insertText(tabBuilder, selectionStart);
}
showCaret(true);
consumed = true;
} else if (keyCode == Keyboard.KeyCode.HOME
|| (keyCode == Keyboard.KeyCode.LEFT && metaPressed)) {
int start;
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
if (commandPressed) {
// Move the caret to the beginning of the text
start = 0;
} else {
// Move the caret to the beginning of the line
start = getRowOffset(selectionStart);
}
if (shiftPressed) {
selectionLength += selectionStart - start;
} else {
selectionLength = 0;
}
if (selectionStart >= 0) {
textArea.setSelection(start, selectionLength);
scrollCharacterToVisible(start);
caretX = caret.x;
consumed = true;
}
} else if (keyCode == Keyboard.KeyCode.END
|| (keyCode == Keyboard.KeyCode.RIGHT && metaPressed)) {
int end;
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
int index = selectionStart + selectionLength;
if (commandPressed) {
// Move the caret to end of the text
end = textArea.getCharacterCount();
} else {
// Move the caret to the end of the line
int rowOffset = getRowOffset(index);
int rowLength = getRowLength(index);
end = rowOffset + rowLength;
}
if (shiftPressed) {
selectionLength += end - index;
} else {
selectionStart = end;
if (selectionStart < textArea.getCharacterCount()
&& textArea.getCharacterAt(selectionStart) != '\n') {
selectionStart--;
}
selectionLength = 0;
}
if (selectionStart + selectionLength <= textArea.getCharacterCount()) {
textArea.setSelection(selectionStart, selectionLength);
scrollCharacterToVisible(selectionStart + selectionLength);
caretX = caret.x;
if (selection != null) {
caretX += selection.getBounds2D().getWidth();
}
consumed = true;
}
} else if (keyCode == Keyboard.KeyCode.LEFT) {
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
if (wordNavPressed) {
// Move the caret to the start of the next word to the left
if (selectionStart > 0) {
// Skip over any space immediately to the left
int index = selectionStart;
while (index > 0
&& Character.isWhitespace(textArea.getCharacterAt(index - 1))) {
index--;
}
// Skip over any word-letters to the left
while (index > 0
&& !Character.isWhitespace(textArea.getCharacterAt(index - 1))) {
index--;
}
if (shiftPressed) {
selectionLength += selectionStart - index;
} else {
selectionLength = 0;
}
selectionStart = index;
}
} else if (shiftPressed) {
if (anchor != -1) {
if (selectionStart < anchor) {
if (selectionStart > 0) {
selectionStart--;
selectionLength++;
}
} else {
if (selectionLength > 0) {
selectionLength--;
} else {
selectionStart--;
selectionLength++;
}
}
} else {
// Add the previous character to the selection
anchor = selectionStart;
if (selectionStart > 0) {
selectionStart--;
selectionLength++;
}
}
} else {
// Move the caret back by one character
if (selectionLength == 0 && selectionStart > 0) {
selectionStart--;
}
// Clear the selection
anchor = -1;
selectionLength = 0;
}
if (selectionStart >= 0) {
textArea.setSelection(selectionStart, selectionLength);
scrollCharacterToVisible(selectionStart);
caretX = caret.x;
consumed = true;
}
} else if (keyCode == Keyboard.KeyCode.RIGHT) {
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
if (wordNavPressed) {
// Move the caret to the start of the next word to the right
if (selectionStart < textArea.getCharacterCount()) {
int index = selectionStart + selectionLength;
// Skip over any space immediately to the right
while (index < textArea.getCharacterCount()
&& Character.isWhitespace(textArea.getCharacterAt(index))) {
index++;
}
// Skip over any word-letters to the right
while (index < textArea.getCharacterCount()
&& !Character.isWhitespace(textArea.getCharacterAt(index))) {
index++;
}
if (shiftPressed) {
selectionLength = index - selectionStart;
} else {
selectionStart = index;
selectionLength = 0;
}
}
} else if (shiftPressed) {
if (anchor != -1) {
if (selectionStart < anchor) {
selectionStart++;
selectionLength--;
} else {
selectionLength++;
}
} else {
// Add the next character to the selection
anchor = selectionStart;
selectionLength++;
}
} else {
// Move the caret forward by one character
if (selectionLength == 0) {
selectionStart++;
} else {
selectionStart += selectionLength;
}
// Clear the selection
anchor = -1;
selectionLength = 0;
}
if (selectionStart + selectionLength <= textArea.getCharacterCount()) {
textArea.setSelection(selectionStart, selectionLength);
scrollCharacterToVisible(selectionStart + selectionLength);
caretX = caret.x;
if (selection != null) {
caretX += selection.getBounds2D().getWidth();
}
consumed = true;
}
} else if (keyCode == Keyboard.KeyCode.UP) {
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
int index = -1;
if (shiftPressed) {
if (anchor == -1) {
anchor = selectionStart;
index = getNextInsertionPoint(caretX, selectionStart,
TextArea.ScrollDirection.UP);
if (index != -1) {
selectionLength = selectionStart - index;
}
} else {
if (selectionStart < anchor) {
// continue upwards
index = getNextInsertionPoint(caretX, selectionStart,
TextArea.ScrollDirection.UP);
if (index != -1) {
selectionLength = selectionStart + selectionLength - index;
}
} else {
// reduce downward size
Bounds trailingSelectionBounds = getCharacterBounds(selectionStart
+ selectionLength - 1);
int x = trailingSelectionBounds.x + trailingSelectionBounds.width;
index = getNextInsertionPoint(x, selectionStart + selectionLength - 1,
TextArea.ScrollDirection.UP);
if (index != -1) {
if (index < anchor) {
selectionLength = anchor - index;
} else {
selectionLength = index - selectionStart;
index = selectionStart;
}
}
}
}
} else {
index = getNextInsertionPoint(caretX, selectionStart,
TextArea.ScrollDirection.UP);
if (index != -1) {
selectionLength = 0;
}
anchor = -1;
}
if (index != -1) {
textArea.setSelection(index, selectionLength);
scrollCharacterToVisible(index);
caretX = caret.x;
}
consumed = true;
} else if (keyCode == Keyboard.KeyCode.DOWN) {
int selectionStart = textArea.getSelectionStart();
int selectionLength = textArea.getSelectionLength();
if (shiftPressed) {
int from;
int x;
int index;
if (anchor == -1) {
anchor = selectionStart;
index = getNextInsertionPoint(caretX, selectionStart,
TextArea.ScrollDirection.DOWN);
if (index != -1) {
selectionLength = index - selectionStart;
}
} else {
if (selectionStart < anchor) {
// Reducing upward size
// Get next insertion point from leading selection
// character
from = selectionStart;
x = caretX;
index = getNextInsertionPoint(x, from, TextArea.ScrollDirection.DOWN);
if (index != -1) {
if (index < anchor) {
selectionStart = index;
selectionLength = anchor - index;
} else {
selectionStart = anchor;
selectionLength = index - anchor;
}
textArea.setSelection(selectionStart, selectionLength);
scrollCharacterToVisible(selectionStart);
}
} else {
// Increasing downward size
// Get next insertion point from right edge of
// trailing selection
// character
from = selectionStart + selectionLength - 1;
Bounds trailingSelectionBounds = getCharacterBounds(from);
x = trailingSelectionBounds.x + trailingSelectionBounds.width;
index = getNextInsertionPoint(x, from, TextArea.ScrollDirection.DOWN);
if (index != -1) {
// If the next character is a paragraph
// terminator and is
// not the final terminator character, increment
// the selection
if (index < textArea.getCharacterCount() - 1
&& textArea.getCharacterAt(index) == '\n') {
index++;
}
textArea.setSelection(selectionStart, index - selectionStart);
scrollCharacterToVisible(index);
}
}
}
} else {
int from;
if (selectionLength == 0) {
// Get next insertion point from leading selection
// character
from = selectionStart;
} else {
// Get next insertion point from trailing selection
// character
from = selectionStart + selectionLength - 1;
}
int index = getNextInsertionPoint(caretX, from, TextArea.ScrollDirection.DOWN);
if (index != -1) {
textArea.setSelection(index, 0);
scrollCharacterToVisible(index);
caretX = caret.x;
}
anchor = -1;
}
consumed = true;
} else if (commandPressed) {
if (keyCode == Keyboard.KeyCode.A) {
textArea.setSelection(0, textArea.getCharacterCount());
consumed = true;
} else if (keyCode == Keyboard.KeyCode.X && isEditable) {
textArea.cut();
consumed = true;
} else if (keyCode == Keyboard.KeyCode.C) {
textArea.copy();
consumed = true;
} else if (keyCode == Keyboard.KeyCode.V && isEditable) {
textArea.paste();
consumed = true;
} else if (keyCode == Keyboard.KeyCode.Z && isEditable) {
if (!shiftPressed) {
textArea.undo();
}
consumed = true;
} else if (keyCode == Keyboard.KeyCode.TAB) {
// Only here if acceptsTab is false
consumed = super.keyPressed(component, keyCode, keyLocation);
}
} else if (keyCode == Keyboard.KeyCode.INSERT) {
if (shiftPressed && isEditable) {
textArea.paste();
consumed = true;
}
} else {
consumed = super.keyPressed(component, keyCode, keyLocation);
}
}
return consumed;
}
@Override
public void enabledChanged(Component component) {
super.enabledChanged(component);
repaintComponent();
}
@Override
public void focusedChanged(Component component, Component obverseComponent) {
super.focusedChanged(component, obverseComponent);
TextArea textArea = (TextArea) getComponent();
if (textArea.isFocused() && textArea.getSelectionLength() == 0) {
if (textArea.isValid()) {
scrollCharacterToVisible(textArea.getSelectionStart());
}
showCaret(true);
} else {
showCaret(false);
}
repaintComponent();
}
@Override
public void maximumLengthChanged(TextArea textArea, int previousMaximumLength) {
// No-op
}
@Override
public void editableChanged(TextArea textArea) {
// No-op
}
@Override
public void paragraphInserted(TextArea textArea, int index) {
// Create paragraph view and add as paragraph listener
TextArea.Paragraph paragraph = textArea.getParagraphs().get(index);
TextAreaSkinParagraphView paragraphView = new TextAreaSkinParagraphView(this, paragraph);
paragraph.getParagraphListeners().add(paragraphView);
// Insert view
paragraphViews.insert(paragraphView, index);
invalidateComponent();
}
@Override
public void paragraphsRemoved(TextArea textArea, int index, Sequence<TextArea.Paragraph> removed) {
// Remove paragraph views as paragraph listeners
int count = removed.getLength();
for (int i = 0; i < count; i++) {
TextArea.Paragraph paragraph = removed.get(i);
TextAreaSkinParagraphView paragraphView = paragraphViews.get(i + index);
paragraph.getParagraphListeners().remove(paragraphView);
}
// Remove views
paragraphViews.remove(index, count);
invalidateComponent();
}
@Override
public void textChanged(TextArea textArea) {
// No-op
}
@Override
public void selectionChanged(TextArea textArea, int previousSelectionStart,
int previousSelectionLength) {
// If the text area is valid, repaint the selection state; otherwise,
// the selection will be updated in layout()
if (textArea.isValid()) {
if (selection == null) {
// Repaint previous caret bounds
textArea.repaint(caret.x, caret.y, caret.width, caret.height);
} else {
// Repaint previous selection bounds
Rectangle bounds = selection.getBounds();
textArea.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
}
updateSelection();
if (selection == null) {
showCaret(textArea.isFocused());
} else {
showCaret(false);
// Repaint current selection bounds
Rectangle bounds = selection.getBounds();
textArea.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
}
}
}
private void updateSelection() {
TextArea textArea = (TextArea) getComponent();
if (paragraphViews.getLength() > 0) {
// Update the caret
int selectionStart = textArea.getSelectionStart();
Bounds leadingSelectionBounds = getCharacterBounds(selectionStart);
caret = leadingSelectionBounds.toRectangle();
caret.width = 1;
// Update the selection
int selectionLength = textArea.getSelectionLength();
if (selectionLength > 0) {
int selectionEnd = selectionStart + selectionLength - 1;
Bounds trailingSelectionBounds = getCharacterBounds(selectionEnd);
selection = new Area();
int firstRowIndex = getRowAt(selectionStart);
int lastRowIndex = getRowAt(selectionEnd);
if (firstRowIndex == lastRowIndex) {
selection.add(new Area(new Rectangle(leadingSelectionBounds.x,
leadingSelectionBounds.y, trailingSelectionBounds.x
+ trailingSelectionBounds.width - leadingSelectionBounds.x,
trailingSelectionBounds.y + trailingSelectionBounds.height
- leadingSelectionBounds.y)));
} else {
int width = getWidth();
selection.add(new Area(new Rectangle(leadingSelectionBounds.x,
leadingSelectionBounds.y, width - margin.right - leadingSelectionBounds.x,
leadingSelectionBounds.height)));
if (lastRowIndex - firstRowIndex > 0) {
selection.add(new Area(new Rectangle(margin.left, leadingSelectionBounds.y
+ leadingSelectionBounds.height, width - (margin.left + margin.right),
trailingSelectionBounds.y
- (leadingSelectionBounds.y + leadingSelectionBounds.height))));
}
selection.add(new Area(new Rectangle(margin.left, trailingSelectionBounds.y,
trailingSelectionBounds.x + trailingSelectionBounds.width - margin.left,
trailingSelectionBounds.height)));
}
} else {
selection = null;
}
} else {
// Clear the caret and the selection
caret = new Rectangle();
selection = null;
}
}
private void showCaret(boolean show) {
if (scheduledBlinkCaretCallback != null) {
scheduledBlinkCaretCallback.cancel();
}
if (show) {
caretOn = true;
scheduledBlinkCaretCallback = ApplicationContext.scheduleRecurringCallback(
blinkCaretCallback, Platform.getCursorBlinkRate());
// Run the callback once now to show the cursor immediately
blinkCaretCallback.run();
} else {
scheduledBlinkCaretCallback = null;
}
}
}