Mercurial > projects > dwt-addons
diff dwtx/draw2d/text/TextFlow.d @ 98:95307ad235d9
Added Draw2d code, still work in progress
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sun, 03 Aug 2008 00:52:14 +0200 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/draw2d/text/TextFlow.d Sun Aug 03 00:52:14 2008 +0200 @@ -0,0 +1,686 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module dwtx.draw2d.text.TextFlow; + +import dwt.dwthelper.utils; +import dwtx.dwtxhelper.Collection; + +import dwt.DWT; +import dwt.graphics.Color; +import dwt.graphics.TextLayout; +import dwtx.draw2d.ColorConstants; +import dwtx.draw2d.Graphics; +import dwtx.draw2d.TextUtilities; +import dwtx.draw2d.geometry.Dimension; +import dwtx.draw2d.geometry.Point; +import dwtx.draw2d.geometry.Rectangle; +import dwtx.draw2d.text.InlineFlow; +import dwtx.draw2d.text.BidiInfo; +import dwtx.draw2d.text.BidiProcessor; +import dwtx.draw2d.text.FlowFigureLayout; +import dwtx.draw2d.text.TextFragmentBox; +import dwtx.draw2d.text.CaretInfo; +import dwtx.draw2d.text.FlowUtilities; +import dwtx.draw2d.text.ParagraphTextLayout; +import dwtx.draw2d.text.BidiChars; +import dwtx.draw2d.text.FlowBorder; + +import tango.text.convert.Format; + +/** + * An inline flow figure that renders a string of text across one or more lines. A + * TextFlow cannot contain children. All <code>InlineFlow</code> figure's must be + * parented by a <code>FlowFigure</code>. + * <p> + * WARNING: This class is not intended to be subclassed by clients. + * @author hudsonr + * @author Pratik Shah + * @since 2.1 + */ +public class TextFlow + : InlineFlow +{ + +static final String ELLIPSIS = "..."; //$NON-NLS-1$ +private BidiInfo bidiInfo; +private int selectionEnd = -1; +private String text; + +/** + * Constructs a new TextFlow with the empty String. + * @see java.lang.Object#Object() + */ +public this() { + this(""/+new String()+/); +} + +/** + * Constructs a new TextFlow with the specified String. + * @param s the string + */ +public this(String s) { + text = s; +} + +/** + * Returns the width of the text until the first line-break. + * @see dwtx.draw2d.text.FlowFigure#addLeadingWordRequirements(int[]) + */ +public bool addLeadingWordRequirements(int[] width) { + return addLeadingWordWidth(getText(), width); +} + +/** + * Calculates the width taken up by the given text before a line-break is encountered. + * + * @param text the text in which the break is to be found + * @param width the width before the next line-break (if one's found; the width of all + * the given text, otherwise) will be added on to the first int in the given array + * @return <code>true</code> if a line-break was found + * @since 3.1 + */ +bool addLeadingWordWidth(String text, int[] width) { + if (text.length is 0) + return false; + if (CharacterIsWhitespace(text.firstCodePoint())) + return true; + + text = 'a' ~ text ~ 'a'; + FlowUtilities.LINE_BREAK.setText(text); + int index = FlowUtilities.LINE_BREAK.next() - 1; + if (index is 0) + return true; + while (CharacterIsWhitespace(text[index..$].firstCodePoint())) + index--; + bool result = index < text.length - 1; + // index should point to the end of the actual text (not including the 'a' that was + // appended), if there were no breaks + if (index is text.length - 1) + index--; + text = text.substring(1, index + 1); + + if (bidiInfo is null) + width[0] += getTextUtilities().getStringExtents(text, getFont()).width; + else { + TextLayout textLayout = FlowUtilities.getTextLayout(); + textLayout.setFont(getFont()); + textLayout.setText(text); + width[0] += textLayout.getBounds().width; + } + return result; +} + +/** + * A TextFlow contributes its text. + * @see dwtx.draw2d.text.FlowFigure#contributeBidi(dwtx.draw2d.text.BidiProcessor) + */ +protected void contributeBidi(BidiProcessor proc) { + bidiInfo = null; + proc.add(this, getText()); +} + +/** + * @see dwtx.draw2d.text.InlineFlow#createDefaultFlowLayout() + */ +protected FlowFigureLayout createDefaultFlowLayout() { + return new ParagraphTextLayout(this); +} + +private int findNextLineOffset(Point p, int[] trailing) { + if (getBounds().bottom() <= p.y) + return -1; + + TextFragmentBox closestBox = null; + int index = 0; + List fragments = getFragmentsWithoutBorder(); + for (int i = fragments.size() - 1; i >= 0; i--) { + TextFragmentBox box = cast(TextFragmentBox)fragments.get(i); + if (box.getBaseline() - box.getLineRoot().getAscent() > p.y + && (closestBox is null + || box.getBaseline() < closestBox.getBaseline() + || (box.getBaseline() is closestBox.getBaseline() + && hDistanceBetween(box, p.x) < hDistanceBetween(closestBox, p.x)))) { + closestBox = box; + index = i; + } + } + return findOffset(p, trailing, closestBox, index); +} + +private int findOffset(Point p, int[] trailing, TextFragmentBox box, int boxIndex) { + if (box is null) + return -1; + TextLayout layout = FlowUtilities.getTextLayout(); + layout.setFont(getFont()); + layout.setText(getBidiSubstring(box, boxIndex)); + int x = p.x - box.getX(); + if (isMirrored()) + x = box.getWidth() - x; + int layoutOffset = layout.getOffset(x, p.y - box.getTextTop(), trailing); + return box.offset + layoutOffset - getBidiPrefixLength(box, boxIndex); +} + +private int findPreviousLineOffset(Point p, int[] trailing) { + if (getBounds().y > p.y) + return -1; + + TextFragmentBox closestBox = null; + int index = 0; + List fragments = getFragmentsWithoutBorder(); + for (int i = fragments.size() - 1; i >= 0; i--) { + TextFragmentBox box = cast(TextFragmentBox)fragments.get(i); + if (box.getBaseline() + box.getLineRoot().getDescent() < p.y + && (closestBox is null + || box.getBaseline() > closestBox.getBaseline() + || (box.getBaseline() is closestBox.getBaseline() + && hDistanceBetween(box, p.x) < hDistanceBetween(closestBox, p.x)))) { + closestBox = box; + index = i; + } + } + return findOffset(p, trailing, closestBox, index); +} + +int getAscent() { + return getTextUtilities().getAscent(getFont()); +} + +/** + * Returns the BidiInfo for this figure or <code>null</code>. + * @return <code>null</code> or the info + * @since 3.1 + */ +public BidiInfo getBidiInfo() { + return bidiInfo; +} + +private int getBidiPrefixLength(TextFragmentBox box, int index) { + if (box.getBidiLevel() < 1) + return 0; + if (index > 0 || !bidiInfo.leadingJoiner) + return 1; + return 2; +} + +/** + * @param box which fragment + * @param index the fragment index + * @return the bidi string for that fragment + * @since 3.1 + */ +protected String getBidiSubstring(TextFragmentBox box, int index) { + if (box.getBidiLevel() < 1) + return getText().substring(box.offset, box.offset + box.length); + + StringBuffer buffer = new StringBuffer(box.length + 3); + buffer.append( dcharToString( box.isRightToLeft() ? BidiChars.RLO : BidiChars.LRO )); + if (index is 0 && bidiInfo.leadingJoiner) + buffer.append(dcharToString(BidiChars.ZWJ)); + buffer.append(getText().substring(box.offset, box.offset + box.length)); + if (index is getFragmentsWithoutBorder().size() - 1 && bidiInfo.trailingJoiner) + buffer.append(dcharToString(BidiChars.ZWJ)); + return buffer.toString(); +} + +/** + * Returns the CaretInfo in absolute coordinates. The offset must be between 0 and the + * length of the String being displayed. + * @since 3.1 + * @param offset the location in this figure's text + * @param trailing true if the caret is being placed after the offset + * @exception IllegalArgumentException If the offset is not between <code>0</code> and the + * length of the string inclusively + * @return the caret bounds relative to this figure + */ +public CaretInfo getCaretPlacement(int offset, bool trailing) { + if (offset < 0 || offset > getText().length) + throw new IllegalArgumentException(Format("Offset: {} is invalid", offset //$NON-NLS-1$ + )); //$NON-NLS-1$ + + if (offset is getText().length) + trailing = false; + + List fragments = getFragmentsWithoutBorder(); + int i = fragments.size(); + TextFragmentBox box; + do + box = cast(TextFragmentBox)fragments.get(--i); + while (offset < box.offset && i > 0); + + // Cannot be trailing and after the last char, so go to first char in next box + if (trailing && box.offset + box.length <= offset) { + box = cast(TextFragmentBox)fragments.get(++i); + offset = box.offset; + trailing = false; + } + + Point where = getPointInBox(box, offset, i, trailing); + CaretInfo info = new CaretInfo(where.x, where.y, box.getAscent(), box.getDescent(), + box.getLineRoot().getAscent(), box.getLineRoot().getDescent()); + translateToAbsolute(info); + return info; +} + +Point getPointInBox(TextFragmentBox box, int offset, int index, bool trailing) { + offset -= box.offset; + offset = Math.min(box.length, offset); + Point result = new Point(0, box.getTextTop()); + if (bidiInfo is null) { + if (trailing && offset < box.length) + offset++; + String substring = getText().substring(box.offset, box.offset + offset); + result.x = getTextUtilities().getStringExtents(substring, getFont()).width; + } else { + TextLayout layout = FlowUtilities.getTextLayout(); + layout.setFont(getFont()); + String fragString = getBidiSubstring(box, index); + layout.setText(fragString); + offset += getBidiPrefixLength(box, index); + result.x = layout.getLocation(offset, trailing).x; + if (isMirrored()) + result.x = box.width - result.x; + } + result.x += box.getX(); + return result; +} + +int getDescent() { + return getTextUtilities().getDescent(getFont()); +} + +/** + * Returns the minimum character offset which is on the given baseline y-coordinate. The y + * location should be relative to this figure. The return value will be between + * 0 and N-1. If no fragment is located on the baseline, <code>-1</code> is returned. + * @since 3.1 + * @param baseline the relative baseline coordinate + * @return -1 or the lowest offset for the line + */ +public int getFirstOffsetForLine(int baseline) { + TextFragmentBox box; + List fragments = getFragmentsWithoutBorder(); + for (int i = 0; i < fragments.size(); i++) { + box = cast(TextFragmentBox)fragments.get(i); + if (baseline is box.getBaseline()) + return box.offset; + } + return -1; +} + +/** + * Returns the <code>TextFragmentBox</code> fragments contained in this TextFlow, not + * including the border fragments. The returned list should not be modified. + * @return list of fragments without the border fragments + * @since 3.4 + */ +protected List getFragmentsWithoutBorder() { + List fragments = getFragments(); + if (getBorder() !is null) + fragments = fragments.subList(1, fragments.size() - 1); + return fragments; +} + +/** + * Returns the maximum offset for a character which is on the given baseline y-coordinate. + * The y location should be relative to this figure. The return value will be between + * 0 and N-1. If no fragment is located on the baseline, <code>-1</code> is returned. + * @since 3.1 + * @param baseline the relative baseline coordinate + * @return -1 or the highest offset at the given baseline + */ +public int getLastOffsetForLine(int baseline) { + TextFragmentBox box; + List fragments = getFragmentsWithoutBorder(); + for (int i = fragments.size() - 1; i >= 0; i--) { + box = cast(TextFragmentBox)fragments.get(i); + if (baseline is box.getBaseline()) + return box.offset + box.length - 1; + } + return -1; +} + +/** + * Returns the offset nearest the given point either up or down one line. If no offset + * is found, -1 is returned. <code>trailing[0]</code> will be set to 1 if the reference + * point is closer to the trailing edge of the offset than it is to the leading edge. + * @since 3.1 + * @param p a reference point + * @param down <code>true</code> if the search is down + * @param trailing an int array + * @return the next offset or <code>-1</code> + */ +public int getNextOffset(Point p, bool down, int[] trailing) { + return down ? findNextLineOffset(p, trailing) : findPreviousLineOffset(p, trailing); +} + +/** + * Returns the next offset which is visible in at least one fragment or -1 if there is + * not one. A visible offset means that the character or the one preceding it is + * displayed, which implies that a caret can be positioned at such an offset. This is + * useful for advancing a caret past characters which resulted in a line wrap. + * + * @param offset the reference offset + * @return the next offset which is visible + * @since 3.1 + */ +public int getNextVisibleOffset(int offset) { + TextFragmentBox box; + List fragments = getFragmentsWithoutBorder(); + for (int i = 0; i < fragments.size(); i++) { + box = cast(TextFragmentBox)fragments.get(i); + if (box.offset + box.length <= offset) + continue; + return Math.max(box.offset, offset + 1); + } + return -1; +} + +/** + * Returns the offset of the character directly below or nearest the given location. The + * point must be relative to this figure. The return value will be between 0 and N-1. If + * the proximity argument is not <code>null</code>, the result may also be <code>-1</code> + * if no offset was found within the proximity. + * <P> + * For a typical character, the trailing argument will be filled in to indicate whether + * the point is closer to the leading edge (0) or the trailing edge (1). When the point + * is over a cluster composed of multiple characters, the trailing argument will be filled + * with the position of the character in the cluster that is closest to the point. + * <P> + * If the proximity argument is not <code>null</code>, then the location may be no further + * than the proximity given. Passing <code>null</code> is equivalent to passing <code>new + * Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)</code>. The <code>width</code> field of + * the proximity will contain the horizontal distance, <code>height</code> will contain + * vertical. Vertical proximity is more important than horizontal. The returned offset is + * the lowest index with minimum vertical proximity not exceeding the given limit, with + * horizontal proximity not exceeding the given limit. If an offset that is within the + * proximity is found, then the given <code>Dimension</code> will be updated to reflect + * the new proximity. + * + * + * @since 3.1 + * @param p the point relative to this figure + * @param trailing the trailing buffer + * @param proximity restricts and records the distance of the returned offset + * @return the nearest offset in this figure's text + */ +public int getOffset(Point p, int trailing[], Dimension proximity) { + if (proximity is null) + proximity = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + TextFragmentBox closestBox = null; + int index = 0; + int dy; + int dx; + int i = 0; + int size = fragments.size(); + if (null !is cast(FlowBorder)getBorder() ) { + i++; + size--; + } + for (; i < size; i++) { + TextFragmentBox box = cast(TextFragmentBox)fragments.get(i); + dy = vDistanceBetween(box, p.y); + if (dy > proximity.height) + continue; + dx = hDistanceBetween(box, p.x); + if (dy is proximity.height && dx >= proximity.width) + continue; + proximity.height = dy; + proximity.width = dx; + closestBox = box; + index = i; + } + return findOffset(p, trailing, closestBox, index); +} + +/** + * Returns the previous offset which is visible in at least one fragment or -1 if there + * is not one. See {@link #getNextVisibleOffset(int)} for more. + * + * @param offset a reference offset + * @return -1 or the previous offset which is visible + * @since 3.1 + */ + +public int getPreviousVisibleOffset(int offset) { + TextFragmentBox box; + if (offset is -1) + offset = Integer.MAX_VALUE; + List fragments = getFragmentsWithoutBorder(); + for (int i = fragments.size() - 1; i >= 0; i--) { + box = cast(TextFragmentBox)fragments.get(i); + if (box.offset >= offset) + continue; + return Math.min(box.offset + box.length, offset - 1); + } + return -1; +} + +/** + * @return the String being displayed; will not be <code>null</code> + */ +public String getText() { + return text; +} + +int getVisibleAscent() { + if (null !is cast(FlowBorder)getBorder() ) { + FlowBorder border = cast(FlowBorder)getBorder(); + return border.getInsets(this).top + getAscent(); + } + return getAscent(); +} + +int getVisibleDescent() { + if (null !is cast(FlowBorder)getBorder() ) { + FlowBorder border = cast(FlowBorder)getBorder(); + return border.getInsets(this).bottom + getDescent(); + } + return getDescent(); +} + +private int hDistanceBetween(TextFragmentBox box, int x) { + if (x < box.getX()) + return box.getX() - x; + return Math.max(0, x - (box.getX() + box.getWidth())); +} + +/** + * Returns <code>true</code> if a portion if the text is truncated using ellipses ("..."). + * @return <code>true</code> if the text is truncated with ellipses + */ +public bool isTextTruncated() { + for (int i = 0; i < fragments.size(); i++) { + if ((cast(TextFragmentBox)fragments.get(i)).isTruncated()) + return true; + } + return false; +} + +/** + * @see dwtx.draw2d.Figure#paintFigure(Graphics) + */ +protected void paintFigure(Graphics g) { + TextFragmentBox frag; + g.getClip(Rectangle.SINGLETON); + int yStart = Rectangle.SINGLETON.y; + int yEnd = Rectangle.SINGLETON.bottom(); + + for (int i = 0; i < fragments.size(); i++) { + frag = cast(TextFragmentBox)fragments.get(i); +// g.drawLine(frag.getX(), frag.getLineRoot().getVisibleTop(), +// frag.getWidth() + frag.getX(), frag.getLineRoot().getVisibleTop()); +// g.drawLine(frag.getX(), frag.getBaseline(), frag.getWidth() + frag.getX(), frag.getBaseline()); + if (frag.offset is -1) + continue; + //Loop until first visible fragment + if (yStart > frag.getLineRoot().getVisibleBottom() + 1)//The + 1 is for disabled text + continue; + //Break loop at first non-visible fragment + if (yEnd < frag.getLineRoot().getVisibleTop()) + break; + + String draw = getBidiSubstring(frag, i); + + if (frag.isTruncated()) + draw ~= ELLIPSIS; + + if (!isEnabled()) { + Color fgColor = g.getForegroundColor(); + g.setForegroundColor(ColorConstants.buttonLightest); + paintText(g, draw, + frag.getX() + 1, + frag.getBaseline() - getAscent() + 1, + frag.getBidiLevel()); + g.setForegroundColor(ColorConstants.buttonDarker); + paintText(g, draw, + frag.getX(), + frag.getBaseline() - getAscent(), + frag.getBidiLevel()); + g.setForegroundColor(fgColor); + } else { + paintText(g, draw, + frag.getX(), + frag.getBaseline() - getAscent(), + frag.getBidiLevel()); + } + } +} + +/** + * @see InlineFlow#paintSelection(dwtx.draw2d.Graphics) + */ +protected void paintSelection(Graphics graphics) { + if (selectionStart is -1) + return; + graphics.setXORMode(true); + graphics.setBackgroundColor(ColorConstants.white); + + TextFragmentBox frag; + for (int i = 0; i < fragments.size(); i++) { + frag = cast(TextFragmentBox)fragments.get(i); + //Loop until first visible fragment + if (frag.offset + frag.length <= selectionStart) + continue; + if (frag.offset > selectionEnd) + return; + if (selectionStart <= frag.offset && selectionEnd >= frag.offset + frag.length) { + int y = frag.getLineRoot().getVisibleTop(); + int height = frag.getLineRoot().getVisibleBottom() - y; + graphics.fillRectangle(frag.getX(), y, frag.getWidth(), height); + } else if (selectionEnd > frag.offset && selectionStart < frag.offset + frag.length) { + Point p1 = getPointInBox(frag, Math.max(frag.offset, selectionStart), i, false); + Point p2 = getPointInBox(frag, Math.min(frag.offset + frag.length, selectionEnd) - 1, i, true); + Rectangle rect = new Rectangle(p1, p2); + rect.width--; + rect.y = frag.getLineRoot().getVisibleTop(); + rect.height = frag.getLineRoot().getVisibleBottom() - rect.y; + graphics.fillRectangle(rect); + } + } +} + +protected void paintText(Graphics g, String draw, int x, int y, int bidiLevel) { + if (bidiLevel is -1) { + g.drawString(draw, x, y); + } else { + TextLayout tl = FlowUtilities.getTextLayout(); + if (isMirrored()) + tl.setOrientation(DWT.RIGHT_TO_LEFT); + tl.setFont(g.getFont()); + tl.setText(draw); + g.drawTextLayout(tl, x, y); + } +} + +/** + * @see dwtx.draw2d.text.FlowFigure#setBidiInfo(dwtx.draw2d.text.BidiInfo) + */ +public void setBidiInfo(BidiInfo info) { + this.bidiInfo = info; +} + +/** + * Sets the extent of selection. The selection range is inclusive. For example, the + * range [0, 0] indicates that the first character is selected. + * @param start the start offset + * @param end the end offset + * @since 3.1 + */ +public void setSelection(int start, int end) { + bool repaint_ = false; + + if (selectionStart is start) { + if (selectionEnd is end) + return; + repaint_ = true; + } else + repaint_ = selectionStart !is selectionEnd || start !is end; + + selectionStart = start; + selectionEnd = end; + if (repaint_) + repaint(); +} + +/** + * Sets the text being displayed. The string may not be <code>null</code>. + * @param s The new text + */ +public void setText(String s) { + if (s !is null && !s.equals(text)) { + text = s; + revalidateBidi(this); + repaint(); + } +} + +/** + * @see java.lang.Object#toString() + */ +public String toString() { + return text; +} + +private int vDistanceBetween(TextFragmentBox box, int y) { + int top = box.getBaseline() - box.getLineRoot().getAscent(); + if (y < top) + return top - y; + return Math.max(0, y - (box.getBaseline() + box.getLineRoot().getDescent())); +} + +/** + * Gets the <code>FlowUtilities</code> instance to be used in measurement + * calculations. + * + * @return a <code>FlowUtilities</code> instance + * @since 3.4 + */ +protected FlowUtilities getFlowUtilities() { + return FlowUtilities.INSTANCE; +} +package FlowUtilities getFlowUtilities_package() { + return getFlowUtilities(); +} + +/** + * Gets the <code>TextUtilities</code> instance to be used in measurement + * calculations. + * + * @return a <code>TextUtilities</code> instance + * @since 3.4 + */ +protected TextUtilities getTextUtilities() { + return TextUtilities.INSTANCE; +} + +}