All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.readytalk.swt.text.painter.TextPainter Maven / Gradle / Ivy

There is a newer version: 3.0.17
Show newest version
package com.readytalk.swt.text.painter;

import java.util.ArrayList;
import java.util.List;

import com.readytalk.swt.util.ColorFactory;
import com.readytalk.swt.util.FontFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

import com.readytalk.swt.text.navigation.Hyperlink;
import com.readytalk.swt.text.navigation.NavigationEvent;
import com.readytalk.swt.text.navigation.NavigationListener;
import com.readytalk.swt.text.tokenizer.TextToken;
import com.readytalk.swt.text.tokenizer.TextTokenizer;
import com.readytalk.swt.text.tokenizer.TextTokenizerFactory;

/**
 * TextPainter is a utility class used to render text onto a Composite within a 
 * given boundary with a call to its handlePaint method.  It can render the 
 * following TextTypes:
 * 

*

  • TEXT
  • *
  • BOLD
  • *
  • ITALIC
  • *
  • BOLD_AND_ITALIC
  • *
  • PLAIN_URL
  • *
  • LINK_URL
  • *
  • LINK_AND_NAMED_URL
  • *
  • NAMED_URL
  • *

    *

    * TextPainter works by tokenizing text into a TextToken list of the * aforementioned TextTypes and then painting them into the given boundary using * the TextPainter options for guidance. By default, TextPainter uses a * PlainText tokenizer which does not recognize any text styling rules such as * BOLD or ITALIC. *

    *
     * Example:
     * {@code
     * final TextPainter painter = new TextPainter(this)
     *         .setTokenizer(TextTokenizerFactory.createTextTokenizer(TextTokenizerType.WIKI))
     *         .setText(
     *             "This is '''wiki text''' is auto-wrapped and can display "
     *                 + "''Italic Text,'' '''Bold Text,''' and "
     *                 + "'''''Bold and Italic Text'''''"
     *                 + " naked url: http://www.google.com"
     *                 + " wiki url: [http://www.readytalk.com ReadyTalk]"  
     *                 + " url: [http://www.readytalk.com]")
     *         .setClipping(false).setBounds(wikiTextBounds).setDrawBounds(true)
     *         .setWrapping(true).addNavigationListener(new NavigationListener() {
     *           public void navigate(NavigationEvent event) {
     *             System.out.println("Navigate to: " + event.getUrl());
     *           }
     *         });
     * }
     * 
    */ public class TextPainter { private Color boundaryColor; private Color textColor; private Color hyperlinkColor; private Cursor handCursor; private Font boldAndItalicFont; private Font boldFont; private Font font; private Font headerFont; private Font italicFont; private Font underlineFont; private Hyperlink activeHyperlink; private Rectangle bounds; private boolean drawCalculatedBounds; private boolean clipping; private boolean drawBounds; private List hyperlinks; private int justification; private float lineSpacing; private List navigationListeners; private int paddingBottom; private int paddingLeft; private int paddingRight; private int paddingTop; private Composite parent; private String text; private TextTokenizer textTokenizer; private List tokens; private int verticalAlignment; private boolean wrapping; /** * Creates a new TextPainter to paint text onto the given Composite. * * @param parent : Composite */ public TextPainter(final Composite parent) { this.parent = parent; Listener listener = new Listener() { public void handleEvent(Event event) { switch (event.type) { case SWT.Dispose: dispose(); break; } } }; parent.addListener(SWT.Dispose, listener); handCursor = new Cursor(parent.getDisplay(), SWT.CURSOR_HAND); text = ""; clipping = true; wrapping = true; textTokenizer = TextTokenizerFactory.createDefault(); textColor = parent.getForeground(); hyperlinkColor = ColorFactory.getColor(100, 50, 200); boundaryColor = ColorFactory.getColor(255, 30, 30); justification = SWT.LEFT; lineSpacing = 1.0f; verticalAlignment = SWT.TOP; FontData fontData = parent.getFont().getFontData()[0]; setFont(fontData.getName(), fontData.getHeight()); Point size = parent.getSize(); bounds = new Rectangle(0, 0, size.x, size.y); navigationListeners = new ArrayList(); paddingBottom = 0; paddingLeft = 0; paddingRight = 0; paddingTop = 0; hyperlinks = new ArrayList(); parent.addMouseMoveListener(new MouseMoveListener() { private Cursor defaultCursor = parent.getCursor(); @Override public void mouseMove(MouseEvent e) { for (Hyperlink hyperlink : hyperlinks) { if (hyperlink.contains(e.x, e.y)) { activeHyperlink = hyperlink; break; } else { activeHyperlink = null; } } if (activeHyperlink != null) { parent.setCursor(handCursor); } else { parent.setCursor(defaultCursor); } } }); parent.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { if (activeHyperlink != null) { notifyNavigationListeners(activeHyperlink); } } }); } /** * Disposes of the operating system resources associated with the * receiver and all its descendants. */ public void dispose() { handCursor.dispose(); } /** * Returns the list of TextTokens as parsed by the TextTokenizer * @return List : The list of TextTokens */ public List getTokens() { return tokens; } /** * Returns the raw text passed to TextPainter via the setText() call. * * @return the raw text passed in the setText() call. */ public String getText() { return text; } private TextPainter setFont(final String name, final int height) { font = FontFactory.getFont(this.parent.getDisplay(), height, SWT.NORMAL, name); boldFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.BOLD, name); italicFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.ITALIC, name); underlineFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.UNDERLINE_LINK, name); boldAndItalicFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.ITALIC|SWT.BOLD, name); return this; } /** * Sets the font height (in px) of the default fonts painted by this painter. This includes * normal, bold, italics and their combination. * @param height : an int representing the height of the font in pixels * @return {@link TextPainter} */ public TextPainter setDefaultFontHeight(final int height) { font = FontFactory.getFont(this.parent.getDisplay(), height, SWT.NORMAL, font.getFontData()[0].getName()); boldFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.BOLD, boldFont.getFontData()[0].getName()); italicFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.ITALIC, italicFont.getFontData()[0].getName()); underlineFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.UNDERLINE_LINK, underlineFont.getFontData()[0].getName()); boldAndItalicFont = FontFactory.getFont(this.parent.getDisplay(), height, SWT.ITALIC|SWT.BOLD, boldAndItalicFont.getFontData()[0].getName()); return this; } /** * Sets the boundary color. By default, it is set to (255, 30, 30). * Colors are managed by the color factory. * * @param r : an int representing the red component * @param g : an int representing the green component * @param b : an int representing the blue component * @return {@link TextPainter} */ public TextPainter setBoundaryColor(final int r, final int g, final int b) { boundaryColor = ColorFactory.getColor(r, g, b); return this; } /** * Sets the text color. By default, it clones the parent Composite's * foreground color upon construction. Colors are managed by the * color factory. * * @param r : an int representing the red component * @param g : an int representing the green component * @param b : an int representing the blue component * @return {@link TextPainter} */ public TextPainter setTextColor(final int r, final int g, final int b) { textColor.dispose(); textColor = ColorFactory.getColor(r, g, b); return this; } /** * Sets the text color. By default, it clones the parent Composite's * foreground color upon construction. * * @param rgb : an RGB value for the text color * @return {@link TextPainter} */ public TextPainter setTextColor(final RGB rgb) { textColor.dispose(); textColor = ColorFactory.getColor(rgb.red, rgb.green, rgb.blue); return this; } /** * Sets the hyperlink text color. By default, it is set to (100, 50, 200). * Colors are managed by the color factory. * * @param r : an int representing the red component * @param g : an int representing the green component * @param b : an int representing the blue component * @return {@link TextPainter} */ public TextPainter setHyperlinkColor(final int r, final int g, final int b) { hyperlinkColor = ColorFactory.getColor(r, g, b); return this; } /** * Sets the text painting boundary. By default, it is the size of the * parent Composite. * * @return {@link TextPainter} */ public TextPainter setBounds (final Rectangle bounds) { this.bounds = bounds; return this; } /** * Sets the calculated boundary painting rule. By default, set to false and the calculated boundary * will not be painted. * * @return {@link TextPainter} */ public TextPainter setDrawCalculatedBounds(boolean drawCalculatedBounds) { this.drawCalculatedBounds = drawCalculatedBounds; return this; } /** * Sets the clipping paint rule. By default, clipping is enabled. * * @return {@link TextPainter} */ public TextPainter setClipping(final boolean clipping) { this.clipping = clipping; return this; } /** * Sets the boundary painting rule. By default, set to false and the boundary * will not be painted. * * @return {@link TextPainter} */ public TextPainter setDrawBounds(final boolean drawBounds) { this.drawBounds = drawBounds; return this; } /** * Sets the horizontal alignment of text. * @param justification * @return {@link TextPainter} */ public TextPainter setJustification(final int justification) { this.justification = justification; return this; } /** * A factor for the amount of space between lines. The default is set to 1.0. * @param lineSpacing * @return {@link TextPainter} */ public TextPainter setLineSpacing(final float lineSpacing) { this.lineSpacing = lineSpacing; return this; } /** * Sets the inset padding for the text. This may effect layout in some instances. * @param top * @param bottom * @param left * @param right * @return {@link TextPainter} */ public TextPainter setPadding(final int top, final int bottom, final int left, final int right) { this.paddingTop = top; this.paddingBottom = bottom; this.paddingLeft = left; this.paddingRight = right; return this; } void tokenizeText() { tokens = textTokenizer.tokenize(text); } /** * Sets the text to be painted. By default, this is an empty string. * * @return {@link TextPainter} */ public TextPainter setText(final String text) { this.text = text; textTokenizer.reset(); tokenizeText(); return this; } /** * Appends the text passed to the previously defined text * * @return {@link TextPainter} */ public TextPainter appendText(final String appendText) { this.text = text + appendText; tokenizeText(); return this; } /** * Sets the Tokenizer strategy. By default, this is a PlainTextTokenizer. * * @param textTokenizer : {@link TextTokenizer} * @return {@link TextPainter} */ public TextPainter setTokenizer(final TextTokenizer textTokenizer){ this.textTokenizer = textTokenizer; tokenizeText(); return this; } /** * Sets the text wrapping paint rule. By default, this is set to true and * text will be wrapped. * * @return {@link TextPainter} */ public TextPainter setWrapping(final boolean wrapping) { this.wrapping = wrapping; return this; } boolean isOverHyperlink(final int x, final int y) { for(Hyperlink hyperlink : hyperlinks) { if (hyperlink.contains(x, y)) { return true; } } return false; } /** * Adds a NavigationListener to be called upon navigation events. Navigation * events occur when a mouse up event falls within the boundary of Hyperlink * as discovered when painting TextTokens of TextTypes: *

    *

  • PLAIN_URL
  • *
  • LINK_URL
  • *
  • LINK_AND_NAMED_URL
  • *
  • NAMED_URL
  • *

    * * @return {@link TextPainter} */ public TextPainter addNavigationListener(final NavigationListener listener) { if (!navigationListeners.contains(listener)) { navigationListeners.add(listener); } return this; } void notifyNavigationListeners(final Hyperlink hyperlink) { NavigationEvent event = new NavigationEvent(hyperlink); for (int i = navigationListeners.size() - 1; i >= 0; i--) { navigationListeners.get(i).navigate(event); } } /** * Removes the NavigationListener from the list of NavigationListeners. The * given NavigationListener will no longer be notified of NavigationEvents. * * @param listener : The {@link NavigationListener} to remove */ public void removeNavigationListener(final NavigationListener listener) { navigationListeners.remove(listener); } void addIfHyperlink(final DrawData drawData, final int x, final int y) { if (drawData.token.getType().equals(TextType.LINK_AND_NAMED_URL) || drawData.token.getType().equals(TextType.LINK_URL) || drawData.token.getType().equals(TextType.NAKED_URL) || drawData.token.getType().equals(TextType.PLAIN_URL)) { hyperlinks.add( new Hyperlink(drawData.token, new Rectangle(x, y, drawData.extent.x + x, drawData.extent.y + y))); } } void configureForStyle(final GC gc, final TextToken token) { gc.setForeground(textColor); switch(token.getType()) { case BOLD: gc.setFont(boldFont); break; case BOLD_AND_ITALIC: gc.setFont(boldAndItalicFont); break; case HEADER: gc.setFont(headerFont); break; case ITALIC: gc.setFont(italicFont); break; case LINK_AND_NAMED_URL: case LINK_URL: case NAKED_URL: case PLAIN_URL: gc.setForeground(hyperlinkColor); gc.setFont(underlineFont); break; case TEXT: gc.setForeground(textColor); gc.setFont(font); break; } } /** * Calculates the bounds required to render the text. This value is constrained by the configured bounds, the amount * of text given, the type of tokenizer used, the fonts used, and whether it is to be rendered wrapped or not. * * @return Rectangle representing the size required to paint the text as configured. */ public Rectangle computeSize(final GC gc) { Rectangle bounds = conditionallyPaintText(gc, false); return new Rectangle(0, 0, bounds.width, bounds.height); } /** * Calculates the bounds the text is attempting to occupy; it only takes the text, assigned styles, & padding into * account. Please take note this is subtly different than computeSize; computeSize takes the widget bounds and * wrapping into account in addition to the text and the assigned styles. * * @return Rectangle representing the bounds the text represents */ public Rectangle precomputeSize(final GC gc) { List> lines = buildLines(gc); int maxX = 0; int maxY = 0; for (List line: lines) { int y = 0; int x = 0; int startIndex = getStartIndex(line); int endIndex = getEndIndex(line); for (int i = startIndex; i <= endIndex; i++) { DrawData drawData = line.get(i); x += drawData.extent.x; if (y < drawData.extent.y && !TextType.NEWLINE.equals(drawData.token.getType())) { y = drawData.extent.y; } } if (x > maxX) { maxX = x; } maxY += y; } return new Rectangle(0, 0, maxX + paddingLeft + paddingRight, maxY + paddingBottom + paddingTop); } class DrawData { TextToken token; Point extent; DrawData(GC gc, TextToken token) { configureForStyle(gc, token); this.token = token; this.extent = gc.textExtent(token.getText()); } } List buildDrawDataList(final GC gc) { List list = new ArrayList(); for (int i = 0; i < tokens.size(); i++) { list.add(new DrawData(gc, tokens.get(i))); } return list; } List> buildLines(final GC gc) { List> lines = new ArrayList>(); List line = new ArrayList(); lines.add(line); List data = buildDrawDataList(gc); int lineWidth = 0; for (DrawData drawData:data) { lineWidth += drawData.extent.x; if ((lineWidth > bounds.width - (paddingRight + paddingLeft) && wrapping) || TextType.NEWLINE.equals(drawData.token.getType())) { List newline = new ArrayList(); lines.add(newline); if (!TextType.NEWLINE.equals(drawData.token.getType())){ // if there is only one token on the line and it is too // wide to fit, force it onto the line if (line.size() == 0) { line.add(drawData); } else { newline.add(drawData); } } line = newline; lineWidth = drawData.extent.x; } else { line.add(drawData); } } return lines; } /** * Paints the text using the GC found in the given PaintEvent. For example, * one might call this from the parent Composite's paintControl event handler: *
       * {@code
       *   ...
       *   addPaintListener(new PaintListener() {
       *     public void paintControl(PaintEvent e) {
       *       textPainter.handlePaint(e);
       *     }
       *   });
       * }
       * 
    * * @param e : {@link PaintEvent} */ public void handlePaint(final PaintEvent e) { conditionallyPaintText(e.gc, true); } /** * Paints the text using the GC you pass. * Remember, if you create a GC you must always dispose() it. * * @param gc : {@link GC} */ public void handlePaint(final GC gc) { conditionallyPaintText(gc, true); } /** * Paints text and/or returns a Rectangle representing the computed bounds * of the text. You may only want they rectangle if you are trying to lay the * text out. * * @param gc * @param paint * @return Rectangle representing the computed bounds */ Rectangle conditionallyPaintText(final GC gc, final boolean paint) { final Rectangle clip = gc.getClipping(); final Color bg = gc.getBackground(); if (clipping) { gc.setClipping(this.bounds); } hyperlinks.clear(); int y = bounds.y + paddingTop; List> lines = buildLines(gc); for (int i = 0; i < lines.size(); i++) { if(justification == SWT.RIGHT) { y += drawRightJustified(gc, paint, lines.get(i), y); } else if (justification == SWT.LEFT) { y += drawLeftJustified(gc, paint, lines.get(i), y); } else if (justification == SWT.CENTER) { y += drawCenterJustified(gc, paint, lines.get(i), y); } } if (drawBounds) { gc.setForeground(boundaryColor); gc.drawRectangle(bounds); } Rectangle calculatedBounds = new Rectangle(bounds.x, bounds.y, bounds.width, y - bounds.y); if (drawCalculatedBounds) { gc.setForeground(ColorFactory.getColor(gc.getDevice(), 0, 255, 0)); gc.drawRectangle(calculatedBounds); } gc.setClipping(clip); gc.setBackground(bg); return calculatedBounds; } int drawRightJustified(GC gc, boolean paint, List line, int y) { int maxY = 0; if (line.size() > 0) { int startIndex = line.size() - 1; DrawData drawData = line.get(startIndex); if(drawData.token.getType() == TextType.WHITESPACE && line.size() > 1) { startIndex--; } int x = bounds.width + bounds.x - paddingRight; for (int i = startIndex; i >= 0; i--) { drawData = line.get(i); configureForStyle(gc, drawData.token); if (paint) { gc.drawText(drawData.token.getText(), x - drawData.extent.x, y, true); addIfHyperlink(drawData, x - drawData.extent.x, y); } x -= drawData.extent.x; if (drawData.extent.y > maxY) { maxY = drawData.extent.y; } } } return maxY; } int drawCenterJustified(GC gc, boolean paint, List line, int y) { int maxY = 0; if (line.size() > 0) { int startIndex = getStartIndex(line); int endIndex = getEndIndex(line); int width = computeLineWidth(line, startIndex, endIndex); int x = bounds.x + ((bounds.width + paddingLeft - paddingRight - width) / 2); for (int i = startIndex; i <= endIndex; i++) { DrawData drawData = line.get(i); x = drawTextToken(gc, paint, y, x, drawData); if (drawData.extent.y > maxY) { maxY = drawData.extent.y; } } } return maxY; } int drawLeftJustified(GC gc, boolean paint, List line, int y) { int maxY = 0; if (line.size() > 0) { int startIndex = getStartIndex(line); int x = bounds.x + paddingLeft; for (int i = startIndex; i < line.size(); i++) { DrawData drawData = line.get(i); x = drawTextToken(gc, paint, y, x, drawData); if (drawData.extent.y > maxY) { maxY = drawData.extent.y; } } } return maxY; } private int drawTextToken(GC gc, boolean paint, int y, int x, DrawData drawData) { configureForStyle(gc, drawData.token); if (paint) { gc.drawText(drawData.token.getText(), x, y, true); addIfHyperlink(drawData, x, y); } x += drawData.extent.x; return x; } int getStartIndex(final List line) { int startIndex = 0; for (int i = 0; i < line.size(); i++) { DrawData drawData = line.get(i); if (startIndex==0 && drawData.token.getType() != TextType.WHITESPACE) { startIndex = i; break; } } return startIndex; } int getEndIndex(final List line) { int endIndex = 0; for (int i = line.size()-1; i >= 0; i--) { DrawData drawData = line.get(i); if (endIndex==0 && drawData.token.getType() != TextType.WHITESPACE) { endIndex = i; break; } } return endIndex; } int computeLineWidth(final List line, final int startIndex, final int endIndex) { int w = 0; // determine width of line while dropping the leading and trailing whitespace for (int i = startIndex; i <= endIndex; i++) { DrawData drawData = line.get(i); w += drawData.extent.x; } return w; } }




    © 2015 - 2025 Weber Informatics LLC | Privacy Policy