blob: 64268c611d682b0777739ee8d8aba29d14013998 [file] [log] [blame]
/*
* (C) Copyright IBM Corp. 1998-2005. All Rights Reserved.
*
* The program is provided "as is" without any warranty express or
* implied, including the warranty of non-infringement and the implied
* warranties of merchantibility and fitness for a particular purpose.
* IBM will not be liable for any damages suffered by you as a result
* of using the Program. In no event will IBM be liable for any
* special, indirect or consequential damages or lost profits even if
* IBM has been advised of the possibility of their occurrence. IBM
* will not be liable for any third party claims against you.
*/
package com.ibm.richtext.textpanel;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.FocusListener;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import com.ibm.richtext.styledtext.MConstText;
import com.ibm.richtext.styledtext.MText;
import com.ibm.richtext.textformat.TextOffset;
import com.ibm.richtext.textformat.MFormatter;
import com.ibm.richtext.textlayout.attributes.AttributeMap;
class TextComponent extends FakeComponent
implements BehaviorOwner,
FocusListener,
KeyListener,
MouseListener,
MouseMotionListener,
Scroller.Client {
static final String COPYRIGHT =
"(C) Copyright IBM Corp. 1998-1999 - All Rights Reserved";
public static final int WINDOW_WIDTH = -10;
public static final int DEFAULT_INSET = 10;
private static final Color STRONG_CARET_COLOR = Color.black;
private static final Color WEAK_CARET_COLOR = Color.darkGray;
private Behavior fBehavior;
private MText fText;
private StyledTextClipboard fClipboard;
private boolean fScrolls;
private Scroller fScroller;
private DocumentView fDocumentView = null;
// sigh - can't create DocumentView until addNotify() is called.
// These values hold DocumentView ctor args
private AttributeMap fDefaultValues;
private boolean fViewWraps;
private int fViewWrapWidth;
private int fViewInsetAmount;
private PanelEventBroadcaster fListener;
/**
* Create a new TextComponent.
* @param text the text model. This model will be used for
* the life of the component, even if setText is called
* @param wraps if true, the text is wrapped to the specified
* wrapping width. If false, the text wraps only at paragraph breaks.
* @param wrapWidth ignored if wraps is false. Text wraps to this width
* unless the width is WINDOW_WIDTH, in which case text wraps to width
* of this component. Should not be negative (unless it is WINDOW_WIDTH).
* @param insetAmount the size of the margins around the text
* @param clipboard the clipboard to use for cut/copy/paste operations.
* If null, the component will use its own clipboard.
*/
public TextComponent(MText text,
AttributeMap defaultValues,
boolean wraps,
int wrapWidth,
int insetAmount,
StyledTextClipboard clipboard,
boolean scrolls,
Scroller scroller,
PanelEventBroadcaster listener) {
fBehavior = null;
if (text == null) {
throw new IllegalArgumentException("Text is null.");
}
fText = text;
fDefaultValues = defaultValues;
if (clipboard == null) {
throw new IllegalArgumentException("Clipboard is null.");
}
fClipboard = clipboard;
fScrolls = scrolls;
fScroller = scroller;
fDocumentView = null;
fViewWrapWidth = wrapWidth;
fViewWraps = wraps;
fViewInsetAmount = insetAmount;
fListener = listener;
}
AttributeMap getDefaultValues() {
return fDefaultValues;
}
void setHost(Component component) {
super.setHost(component);
component.addFocusListener(this);
component.addKeyListener(this);
component.addMouseListener(this);
component.addMouseMotionListener(this);
component.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
if (fDocumentView != null) {
fDocumentView.hostSizeChanged();
scrollToShow(fDocumentView.getDocumentBounds());
}
}
});
}
/**
* ATextPanelImpl's use only!
*/
Component getHost() {
return fHost;
}
// Create document view here. TextComponent isn't fully constructed
// until this is called.
// This must be called by host component!
void addNotify() {
Graphics g = getGraphics();
if (g == null) {
throw new Error("Graphics should be valid here but isn't.");
}
fDocumentView = new DocumentView(this,
fText,
fDefaultValues,
fViewWraps,
fViewWrapWidth,
fViewInsetAmount,
fListener);
documentSizeChanged();
fListener.textStateChanged(TextPanelEvent.FORMAT_WIDTH_CHANGED);
}
public Rectangle getBounds() {
if (fHost != null) {
return fHost.getBounds();
}
return new Rectangle(0, 0, 0, 0);
}
Graphics getGraphics() {
return (fHost==null)? null : fHost.getGraphics();
}
void requestFocus() {
if (fHost != null) {
fHost.requestFocus();
}
}
// *** Behavior management ***
public Behavior getBehavior() {
return fBehavior;
}
public void setBehavior(Behavior b) {
fBehavior = b;
}
// *** Events - just forward to behavior ***
public void focusGained(FocusEvent event) {
if (fBehavior != null)
fBehavior.focusGained(event);
}
public void focusLost(FocusEvent event) {
if (fBehavior != null)
fBehavior.focusLost(event);
}
public void keyPressed(KeyEvent event) {
if (fBehavior != null)
fBehavior.keyPressed(event);
}
public void keyTyped(KeyEvent event) {
if (fBehavior != null) {
fBehavior.keyTyped(event);
}
}
public void keyReleased(KeyEvent event) {
if (fBehavior != null)
fBehavior.keyReleased(event);
}
public void mouseClicked(MouseEvent event) {
// no behavior method for this
}
public void mouseDragged(MouseEvent event) {
if (fBehavior != null)
fBehavior.mouseDragged(event);
}
public void mouseEntered(MouseEvent event) {
if (fBehavior != null)
fBehavior.mouseEntered(event);
}
public void mouseExited(MouseEvent event) {
if (fBehavior != null)
fBehavior.mouseExited(event);
}
public void mouseMoved(MouseEvent event) {
if (fBehavior != null)
fBehavior.mouseMoved(event);
}
public void mousePressed(MouseEvent event) {
if (fBehavior != null)
fBehavior.mousePressed(event);
}
public void mouseReleased(MouseEvent event) {
if (fBehavior != null)
fBehavior.mouseReleased(event);
}
public boolean textControlEventOccurred(Behavior.EventType event, Object what) {
boolean handled = false;
if (fBehavior != null) {
handled = fBehavior.textControlEventOccurred(event, what);
}
return handled;
}
// *** Scroll methods - called by Behaviors
// viewStart, viewLimit is visible bounds of window
// targetStart, targetLimit is the region to scroll into view
private static int getScrollDifference(int viewStart,
int viewLimit,
int targetStart,
int targetLimit) {
if (viewStart <= targetStart) {
if (viewLimit >= targetLimit) {
return 0;
}
return Math.max(viewStart-targetStart, viewLimit-targetLimit);
}
else if (viewLimit > targetLimit) {
return viewLimit - targetLimit;
}
else {
return 0;
}
}
void scrollToShow(Rectangle showRect) {
if (fDocumentView != null) {
Rectangle bounds = getBounds();
int dx = getScrollDifference(showRect.x, showRect.x + showRect.width,
bounds.x, bounds.x + bounds.width);
int dy = getScrollDifference(showRect.y, showRect.y + showRect.height,
bounds.y, bounds.y + bounds.height);
scrollSelf(dx, dy);
}
}
void scrollToShow(int showX, int showY) {
if (fDocumentView != null) {
int dx = 0, dy = 0;
Rectangle bounds = getBounds();
if (showX < bounds.x) {
dx = showX - bounds.x;
}
else if (showX > bounds.x + bounds.width) {
dx = showX - (bounds.x + bounds.width);
}
if (showY < bounds.y) {
dy = showY - bounds.y;
}
else if (showY > bounds.y + bounds.height) {
dy = showY - (bounds.y + bounds.height);
}
scrollSelf(dx, dy);
}
}
private int pinScrollOffset(int delta,
int contentStart,
int contentLength,
int viewStart,
int viewLength) {
if (delta > 0) {
int viewLimit = viewStart + viewLength;
int contentLimit = contentStart + contentLength;
if (viewLimit + delta > contentLimit) {
delta = Math.max(0, contentLimit-viewLimit);
}
}
else {
if (viewStart + delta < contentStart) {
delta = Math.min(0, contentStart-viewStart);
}
}
return delta;
}
private void scrollSelf(int dx, int dy) {
boolean scrolled = scrollBy(dx, dy);
if (scrolled && fScroller != null) {
Rectangle documentBounds = fDocumentView.getDocumentBounds();
fScroller.setPosition(-documentBounds.x,
-documentBounds.y);
}
}
private synchronized boolean scrollBy(int dx, int dy) {
boolean scrolled = false;
if (fScrolls) {
Rectangle documentBounds = fDocumentView.getDocumentBounds();
Rectangle viewBounds = getBounds();
// variable not used int oldDx = dx;
dx = pinScrollOffset(dx,
documentBounds.x,
documentBounds.width,
viewBounds.x,
viewBounds.width);
dy = pinScrollOffset(dy,
documentBounds.y,
documentBounds.height,
viewBounds.y,
viewBounds.height);
if (dx != 0 || dy != 0) {
scrolled = true;
fDocumentView.moveBy(-dx, -dy);
}
}
return scrolled;
}
// implementation of Scroller.Client - called by Scroller
// they have to be public since they're in an interface
// no one else should call these methods
public Rectangle getScrollSize() {
if (fDocumentView != null) {
return fDocumentView.getScrollableArea();
}
return new Rectangle(0, 0, 0, 0);
}
public void scrollTo(int x, int y) {
if (fDocumentView != null) {
scrollBy(x + fDocumentView.getDocX(), y + fDocumentView.getDocY());
}
}
// *** Text access ***
MConstText getText() {
return fText;
}
MText getModifiableText() {
return fText;
}
StyledTextClipboard getClipboard() {
return fClipboard;
}
public synchronized void paint(Graphics g) {
if (fDocumentView != null) {
fDocumentView.paint(g);
}
}
// *** Metric info - used by Behaviors
Rectangle getCaretRect(TextOffset offset) {
if (fDocumentView != null) {
return fDocumentView.getCaretRect(offset);
}
return new Rectangle(0, 0);
}
TextOffset pointToTextOffset(TextOffset result,
int x,
int y,
TextOffset anchor,
boolean infiniteMode) {
if (fDocumentView != null) {
return fDocumentView.pointToTextOffset(result, x, y, anchor, infiniteMode);
}
return new TextOffset();
}
// *** Other stuff used by Behaviors - mostly formatter exports
int lineContaining(TextOffset offset) {
if (fDocumentView != null) {
return fDocumentView.lineContaining(offset);
}
return 0;
}
int lineRangeLow(int lineNumber) {
if (fDocumentView != null) {
return fDocumentView.lineRangeLow(lineNumber);
}
return 0;
}
int lineRangeLimit(int lineNumber) {
if (fDocumentView != null) {
return fDocumentView.lineRangeLimit(lineNumber);
}
return 0;
}
void stopBackgroundFormatting() {
if (fDocumentView != null) {
fDocumentView.stopBackgroundFormatting();
}
}
Rectangle getBoundingRect(TextOffset offset1, TextOffset offset2) {
if (fDocumentView != null) {
return fDocumentView.getBoundingRect(offset1, offset2);
}
return new Rectangle(0, 0, 0, 0);
}
synchronized void reformatAndDrawText(int reformatStart,
int reformatLength,
TextOffset selStart,
TextOffset selEnd,
Rectangle additionalUpdateRect,
Color hiliteColor) {
if (fDocumentView != null) {
fDocumentView.reformatAndDrawText(reformatStart,
reformatLength,
selStart,
selEnd,
additionalUpdateRect,
hiliteColor);
}
}
TextOffset findNewInsertionOffset(TextOffset result,
TextOffset initialOffset,
TextOffset previousOffset,
short direction) {
if (fDocumentView != null) {
return fDocumentView.findNewInsertionOffset(result, initialOffset, previousOffset, direction);
}
return new TextOffset(initialOffset);
}
synchronized void drawText(Graphics g,
Rectangle damagedRect,
boolean selectionVisible,
TextOffset selStart,
TextOffset selEnd,
Color hiliteColor) {
if (fDocumentView != null) {
fDocumentView.drawText(g, damagedRect, selectionVisible, selStart, selEnd, hiliteColor);
}
}
private void documentSizeChanged() {
if (fScroller != null) {
fScroller.clientScrollSizeChanged();
}
}
int getFormatWidth() {
if (fDocumentView != null) {
return fDocumentView.getFormatWidth();
}
return 0;
}
/**
* Return true if the paragraph at the given offset is left-to-right.
* @param offset an offset in the text
* @return true if the paragraph at the given offset is left-to-right
*/
boolean paragraphIsLeftToRight(int offset) {
if (fDocumentView != null) {
return fDocumentView.paragraphIsLeftToRight(offset);
}
return true;
}
private static final class DocumentView {
private TextComponent fHost;
private boolean fWraps;
private boolean fWrapToWindowWidth;
private int fInsetAmount;
private PanelEventBroadcaster fListener;
// fBounds is the total scrollable area of the document (including insets)
private Rectangle fBounds = new Rectangle();
private Point fOrigin;
private MFormatter fFormatter;
private OffscreenBufferCache fBufferCache;
// Note, when this is true the caret won't blink in 1.1. Looks like an AWT bug.
private static boolean fNoOffscreenBuffer =
Boolean.getBoolean("TextComponent.NoOffscreenBuffer");
// Amount by which to reduce the format width to allow for right-aligned carets.
private final int CARET_SLOP = 1;
DocumentView(TextComponent host,
MConstText text,
AttributeMap defaultValues,
boolean wraps,
int wrapWidth,
int insetAmount,
PanelEventBroadcaster listener) {
fHost = host;
fWrapToWindowWidth = wrapWidth == WINDOW_WIDTH;
fInsetAmount = insetAmount;
fListener = listener;
initFormatterAndSize(text, defaultValues, wraps, wrapWidth);
fBufferCache = new OffscreenBufferCache(host.fHost);
}
/**
* Note: this computes the bounds rectangle relative to fOrigin
*/
private void calcBoundsRect() {
final int insetDim = 2 * fInsetAmount;
final int minX = fFormatter.minX();
final int minY = fFormatter.minY();
fBounds.setBounds(fOrigin.x + minX - fInsetAmount,
fOrigin.y + minY - fInsetAmount,
fFormatter.maxX() - minX + insetDim,
fFormatter.maxY() - minY + insetDim);
//if (minX <= 0) {
// System.out.println("calcBoundsRect: minX="+minX+
// "; bounds.x="+fBounds.x+"; width="+fBounds.width);
//}
}
private void initFormatterAndSize(MConstText text,
AttributeMap defaultValues,
boolean wraps,
int wrapWidth) {
Rectangle hostBounds = fHost.getBounds();
int formatWidth;
if (!wraps || fWrapToWindowWidth) {
formatWidth = hostBounds.width - 2 * fInsetAmount;
if (formatWidth <= CARET_SLOP) {
formatWidth = CARET_SLOP+1;
}
}
else {
formatWidth = wrapWidth;
}
fFormatter = MFormatter.createFormatter(text,
defaultValues,
formatWidth-CARET_SLOP,
wraps,
fHost.getGraphics());
fFormatter.formatToHeight(hostBounds.height * 2);
fOrigin = new Point(fInsetAmount, fInsetAmount);
calcBoundsRect();
}
// notification method called by TextComponent
void hostSizeChanged() {
final boolean wrap = fFormatter.wrap();
if (fWrapToWindowWidth || !wrap) {
Rectangle hostBounds = fHost.getBounds();
// variable not used final int insetDim = 2 * fInsetAmount;
int formatWidth = hostBounds.width - 2*fInsetAmount;
if (formatWidth <= CARET_SLOP) {
formatWidth = CARET_SLOP+1;
}
fFormatter.setLineBound(formatWidth-CARET_SLOP);
fFormatter.formatToHeight(hostBounds.y + (hostBounds.height*2) - fOrigin.y);
calcBoundsRect();
//System.out.println("Window bounds="+hostBounds+"; document bounds="+fBounds);
fHost.documentSizeChanged();
fListener.textStateChanged(TextPanelEvent.FORMAT_WIDTH_CHANGED);
//System.out.println("formatWidth="+formatWidth);
//System.out.println("document bounds="+fBounds);
//System.out.println();
}
//dumpWidthInfo();
}
int getFormatWidth() {
return fFormatter.lineBound();
}
boolean paragraphIsLeftToRight(int offset) {
int lineNumber = fFormatter.lineContaining(offset);
return fFormatter.lineIsLeftToRight(lineNumber);
}
private void textSizeMightHaveChanged() {
boolean changed = false;
final int insetDim = 2 * fInsetAmount;
int textHeight = fFormatter.maxY() - fFormatter.minY() + insetDim;
if (textHeight != fBounds.height) {
fBounds.height = textHeight;
changed = true;
}
if (!fFormatter.wrap()) {
int textWidth = fFormatter.maxX() - fFormatter.minX() + insetDim;
if (textWidth != fBounds.width) {
fBounds.width = textWidth;
changed = true;
}
}
if (changed) {
//System.out.println("Text size changed. fBounds: " + fBounds);
calcBoundsRect();
fHost.documentSizeChanged();
fHost.scrollToShow(getDocumentBounds());
}
}
private void doDrawText(Graphics g,
Rectangle drawRect,
boolean selectionVisible,
TextOffset selStart,
TextOffset selEnd,
Color hiliteColor) {
Color oldColor = g.getColor();
g.setColor(fHost.getHost().getBackground());
g.fillRect(drawRect.x, drawRect.y, drawRect.width, drawRect.height);
g.setColor(oldColor);
// g.clearRect(drawRect.x, drawRect.y, drawRect.width, drawRect.height);
if (selectionVisible) {
fFormatter.draw(g, drawRect, fOrigin, selStart, selEnd, hiliteColor);
}
else {
fFormatter.draw(g, drawRect, fOrigin, null, null, null);
}
if (selStart != null && selStart.equals(selEnd) && selectionVisible) {
fFormatter.drawCaret(g, selStart, fOrigin,
STRONG_CARET_COLOR, WEAK_CARET_COLOR);
}
}
void drawText(Graphics g,
Rectangle drawRect,
boolean selectionVisible,
TextOffset selStart,
TextOffset selEnd,
Color hiliteColor) {
if (g != null) {
drawRect = drawRect.intersection(fHost.getBounds());
//System.out.println("drawText:drawRect: " + drawRect);
g.clipRect(drawRect.x, drawRect.y, drawRect.width, drawRect.height);
if (fNoOffscreenBuffer) {
doDrawText(g, drawRect, selectionVisible, selStart, selEnd, hiliteColor);
}
else {
Image offscreenBuffer = fBufferCache.getBuffer(drawRect.width, drawRect.height);
Graphics offscreenGraphics = offscreenBuffer.getGraphics();
offscreenGraphics.translate(-drawRect.x, -drawRect.y);
doDrawText(offscreenGraphics, drawRect, selectionVisible, selStart, selEnd, hiliteColor);
g.drawImage(offscreenBuffer, drawRect.x, drawRect.y, fHost.fHost);
}
}
textSizeMightHaveChanged();
}
void reformatAndDrawText(int reformatStart,
int reformatLength,
TextOffset selStart,
TextOffset selEnd,
Rectangle additionalUpdateRect,
Color hiliteColor) {
Rectangle visibleBounds = fHost.getBounds();
Rectangle redrawRect = fFormatter.updateFormat(reformatStart,
reformatLength,
visibleBounds,
fOrigin);
//System.out.println("[1] redrawRect: " + redrawRect);
if (additionalUpdateRect != null) {
redrawRect.add(additionalUpdateRect);
//System.out.println("[2] redrawRect: " + redrawRect);
}
boolean haveSelection;
if (selStart != null && selEnd != null) {
haveSelection = true;
redrawRect.add(fFormatter.getBoundingRect(selStart, selEnd, fOrigin, MFormatter.LOOSE));
//System.out.println("[3] redrawRect: " + redrawRect);
}
else {
haveSelection = false;
}
drawText(fHost.getGraphics(), redrawRect, haveSelection, selStart, selEnd, hiliteColor);
}
private void letBehaviorDraw(Graphics g, Rectangle drawRect) {
boolean result = false;
if (fHost.fBehavior != null) {
result = fHost.fBehavior.paint(g, drawRect);
}
if (!result) {
drawText(g, drawRect, false, null, null, null);
}
}
void moveBy(int dx, int dy) {
Rectangle visibleBounds = fHost.getBounds();
Graphics g = fHost.getGraphics();
fBounds.x += dx;
fBounds.y += dy;
fOrigin.x += dx;
fOrigin.y += dy;
Rectangle refreshRect = new Rectangle(visibleBounds);
if (dx == 0) {
if (g != null) {
g.copyArea(visibleBounds.x, visibleBounds.y, visibleBounds.width, visibleBounds.height, dx, dy);
}
if (dy < 0) {
refreshRect.y = visibleBounds.y + visibleBounds.height + dy;
}
refreshRect.height = Math.abs(dy);
//System.out.println("refreshRect=" + refreshRect);
}
letBehaviorDraw(g, refreshRect);
}
private Rectangle getInsetBounds() {
int insetDim = 2 * fInsetAmount;
return new Rectangle(fBounds.x-fInsetAmount,
fBounds.y-fInsetAmount,
fBounds.width+insetDim,
fBounds.height+insetDim);
}
void paint(Graphics g) {
Rectangle hostBounds = fHost.getBounds();
Rectangle textRefreshRect = hostBounds.intersection(getInsetBounds());
letBehaviorDraw(g, textRefreshRect);
}
Rectangle getCaretRect(TextOffset offset) {
return fFormatter.getCaretRect(offset, fOrigin);
}
TextOffset pointToTextOffset(TextOffset result,
int x,
int y,
TextOffset anchor,
boolean infiniteMode) {
return fFormatter.pointToTextOffset(result, x, y, fOrigin, anchor, infiniteMode);
}
Rectangle getScrollableArea() {
Rectangle area = new Rectangle(fBounds);
area.x += fInsetAmount - fOrigin.x;
area.y += fInsetAmount - fOrigin.y;
return area;
}
/**
* Doesn't clone so TextComponent needs to be nice. TextComponent
* is the only class which can access this anyway.
*/
Rectangle getDocumentBounds() {
return fBounds;
}
int getDocX() {
return fOrigin.x - fInsetAmount;
}
int getDocY() {
return fOrigin.y - fInsetAmount;
}
int lineContaining(TextOffset offset) {
return fFormatter.lineContaining(offset);
}
int lineRangeLow(int lineNumber) {
return fFormatter.lineRangeLow(lineNumber);
}
int lineRangeLimit(int lineNumber) {
return fFormatter.lineRangeLimit(lineNumber);
}
void stopBackgroundFormatting() {
fFormatter.stopBackgroundFormatting();
}
Rectangle getBoundingRect(TextOffset offset1, TextOffset offset2) {
Rectangle r = fFormatter.getBoundingRect(offset1, offset2, fOrigin, MFormatter.TIGHT);
//r.width += CARET_SLOP;
//System.out.println("offset1="+offset1+"; offset2="+offset2);
//System.out.println("bounds width="+r.width+"; host width="+(fHost.getBounds().width));
return r;
}
TextOffset findNewInsertionOffset(TextOffset result,
TextOffset initialOffset,
TextOffset previousOffset,
short direction) {
return fFormatter.findNewInsertionOffset(
result, initialOffset, previousOffset, direction);
}
}
}