view 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 source

/*******************************************************************************
 * 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;
}

}