001: package com.xoetrope.swing;
002:
003: import com.xoetrope.swing.animation.AnimationStep;
004: import com.xoetrope.swing.animation.XAnimationContext;
005: import com.xoetrope.swing.animation.XAnimationSurface;
006: import com.xoetrope.swing.util.XShapeGenerator;
007: import com.xoetrope.util.XTextDefaults;
008: import java.text.AttributedString;
009:
010: import java.awt.Component;
011: import java.awt.Container;
012: import java.awt.Dimension;
013: import java.awt.Font;
014: import java.awt.FontMetrics;
015: import java.awt.Graphics2D;
016: import java.awt.Point;
017: import java.awt.Rectangle;
018: import java.awt.Shape;
019: import java.awt.font.FontRenderContext;
020: import java.awt.font.LineBreakMeasurer;
021: import java.awt.font.TextAttribute;
022: import java.awt.font.TextLayout;
023: import java.awt.geom.AffineTransform;
024: import java.awt.geom.Area;
025: import java.awt.geom.Point2D;
026: import java.awt.geom.Rectangle2D;
027: import java.text.AttributedCharacterIterator;
028: import java.text.BreakIterator;
029: import java.util.ArrayList;
030: import java.util.Hashtable;
031: import javax.swing.text.View;
032: import net.xoetrope.xui.XAttributedComponent;
033:
034: /**
035: * A component that flows text around other components, child components and
036: * components that overlap this component. Assumes that the area within which the
037: * text is flowed is composed of rectangular blocks.
038: *
039: * <p> Copyright (c) Xoetrope Ltd., 2001-2006, This software is licensed under
040: * the GNU Public License (GPL), please see license.txt for more details. If
041: * you make commercial use of this software you must purchase a commercial
042: * license from Xoetrope.</p>
043: * <p> $Revision: 1.23 $</p>
044: */
045: public class XFlowedTextComponent extends XAnimationSurface //implements XAnimationContext, AnimationStep, XAttributedComponent
046: {
047: protected boolean clip;
048: protected float ascent = 0;
049: protected Area flowArea;
050: protected int oX, oY, oW, oH;
051: protected int deltaX, deltaY;
052: protected int cols, colSpacing;
053: protected int currentColumn;
054:
055: protected Hashtable attributedStrings;
056:
057: /**
058: * Create a new flowed text component
059: */
060: public XFlowedTextComponent() {
061: deltaX = 10;
062: deltaY = 15;
063: cols = 0;
064: colSpacing = 10;
065: autoStart = false;
066: clip = true;
067: }
068:
069: /**
070: * Set the number of columns in which to divide the flow area
071: * @param columns the number of columns, if <1 a single flow area is used
072: */
073: public void setNumColumns(int columns) {
074: cols = columns;
075: }
076:
077: /**
078: * Get the number of columns into which the flow area is divided
079: * @return the number of columns
080: */
081: public int getNumColumns() {
082: return cols;
083: }
084:
085: /**
086: * Set the column spacing as the number of pixels between each column. The
087: * spacing has no effect if there is a single column.
088: * @param spacing the column spacing in pixels
089: */
090: public void setColSpacing(int spacing) {
091: colSpacing = spacing;
092: }
093:
094: /**
095: * Get the column spacing
096: * @return the spacing in pixels
097: */
098: public int getColSpacing() {
099: return colSpacing;
100: }
101:
102: /**
103: * Set the label/text of the component. Usage depends on the concrete component
104: * type
105: * @param text the new text.
106: */
107: public void setText(String text) {
108: super .setText(text);
109: attributedStrings = null;
110: }
111:
112: /**
113: * Get the label/text of the component.
114: * @return the component text.
115: */
116: public String getText() {
117: return super .getText();
118: }
119:
120: /**
121: * Set one or more attributes of the component. Currently this handles the
122: * attributes
123: * <OL>
124: * <li>text - set the text</li>
125: * <li>content - set the text</li>
126: * <li>increment - set the step size</li>
127: * <li>cols - the number of columns in which to flow the text, <1 = a single area/column<li>
128: * </OL>
129: * @param attribName the attribute name
130: * @param attribValue the attribute value
131: * @return 0 for success, non zero otherwise
132: */
133: public int setAttribute(String attribName, Object attribValue) {
134: String attribNameLwr = attribName.toLowerCase();
135: String attribValueStr = (String) attribValue;
136: String attribValueLwr = attribValueStr.toLowerCase();
137: if (attribNameLwr.equals("cols"))
138: cols = Integer.parseInt(attribValueLwr);
139: else if (attribNameLwr.equals("colspacing"))
140: colSpacing = Integer.parseInt(attribValueLwr);
141: else if (attribNameLwr.equals("clip"))
142: clip = (attribValueLwr.equals("true") || attribValueLwr
143: .equals("1"));
144: else
145: super .setAttribute(attribName, attribValue);
146: return 0;
147: }
148:
149: /**
150: * Attempts to load and reference the associated view/datastore.
151: */
152: public void init() {
153: super .init();
154: bimg = null;
155: Point p = getLocation();
156: Dimension size = getSize();
157: oX = p.x;
158: oY = p.y;
159: oW = size.width;
160: oH = size.height;
161: flowArea = new Area(new Rectangle(oX, oY, oW, oH));
162:
163: Container parent = (Container) getParent();
164: int count = parent.getComponentCount();
165: for (int i = 0; i < count; i++) {
166: Component cc = parent.getComponent(i);
167: if (cc != this ) {
168: Area childArea;
169: if (cc instanceof XShapeGenerator) {
170: childArea = new Area(((XShapeGenerator) cc)
171: .getShape());
172: Rectangle rect = childArea.getBounds();
173: double scale = 1.0 + 20.0 / Math.max(rect.width,
174: rect.height);
175: AffineTransform growTransform = AffineTransform
176: .getScaleInstance(scale, scale);
177:
178: /*
179: * The grow transform needs to move the centre back to the unscaled location
180: * with a translate transform
181: */
182: scale -= 1.0;
183: growTransform.translate(-10.0 * scale, -10.0
184: * scale);
185: childArea.transform(growTransform);
186: } else {
187: Rectangle rCC = cc.getBounds();
188: rCC.grow(deltaX, deltaY);
189: childArea = new Area(rCC);
190: }
191: flowArea.subtract(childArea);
192: }
193: }
194: repaint();
195: }
196:
197: //=========================================================================
198: // Rendering
199: //=========================================================================
200: /**
201: * Adjust the settings for the next step.
202: * @param w the width
203: * @param h the height
204: */
205: public void reset(int w, int h) {
206: }
207:
208: /**
209: * Adjust the settings for the next step.
210: *
211: * @param w the width
212: * @param h the height
213: */
214: public void step(int w, int h) {
215: }
216:
217: /**
218: * All classes that extend JAnimationSurface must implement this routine...
219: * This component renders text in the flow area. The flow area is calculated
220: * to be the non-overlapped client area
221: * @param w the width
222: * @param h the height
223: * @param g2 the graphics context
224: */
225: public void drawObjects(int w, int h, Graphics2D g2) {
226: if ((label == null) || (label.length() <= 0))
227: return;
228: if (flowArea == null)
229: init();
230: if (attributedStrings == null)
231: attributedStrings = new Hashtable();
232:
233: applyState(g2);
234: Font font = getFont();
235: FontMetrics fm = g2.getFontMetrics(font);
236: FontRenderContext frc = g2.getFontRenderContext();
237:
238: // Calculate teh columns areas
239: Point2D.Double pen = new Point2D.Double(0, 0);
240: ascent = fm.getAscent();
241: int numColumns = Math.max(cols, 1);
242: double columnWidth = (flowArea.getBounds2D().getWidth() - (numColumns == 1 ? 0
243: : numColumns * colSpacing))
244: / numColumns;
245: Area[] columns = new Area[numColumns];
246: double x = flowArea.getBounds().getX();
247: for (int i = 0; i < numColumns; i++) {
248: Area basicColumn = new Area(new Rectangle2D.Double(x, oY,
249: columnWidth, oH));
250: basicColumn.intersect(flowArea);
251: columns[i] = basicColumn;
252: x += columnWidth + colSpacing;
253: }
254:
255: View v = (View) getClientProperty("html");
256: if (v != null)
257: v.paint(g2, columns[0].getBounds());
258: else {
259: double yPos = 0;
260: double currentPos = 0;
261: currentColumn = 0;
262: label = replace(label, "<br>".subSequence(0, 4),
263: XTextDefaults.CRLF_PAIR.subSequence(0,
264: XTextDefaults.CRLF_PAIR.length()));
265: int idx = label.indexOf(XTextDefaults.CRLF_PAIR);
266: String targetStr = label;
267: while (idx > -1) {
268: if (idx > 6)
269: currentPos = wrapString(g2, targetStr.substring(0,
270: idx), currentPos, yPos, columns);
271: currentPos += ascent;
272: targetStr = targetStr.substring(idx
273: + XTextDefaults.CRLF_PAIR.length());
274: idx = targetStr.indexOf(XTextDefaults.CRLF_PAIR);
275: }
276: yPos = wrapString(g2, targetStr, currentPos, yPos, columns);
277: }
278: }
279:
280: /**
281: * Apply the current state to the graphics rendering context
282: * @param g2 the graphics context
283: */
284: public void applyState(Graphics2D g2) {
285: g2.setColor(getForeground());
286: }
287:
288: /**
289: * Draw text that wraps within the flow area
290: * @param g2 the graphics context
291: * @param text the text
292: * @param currentPos the current y offset for a new column
293: * @param y the current y offset within the column for a new block of text
294: * @param columns the area of the columns
295: * @return the latest y position for drawing/rendering
296: */
297: protected double wrapString(Graphics2D g2, String text,
298: double currentPos, double y, Area[] columns) {
299: if (text.length() <= 0)
300: return y;
301:
302: Point2D.Double pen = new Point2D.Double(0, currentPos);
303: float wrappingWidth = oW - 15;
304: Font font = getFont();
305:
306: TextLayout layout;
307: AffineTransform at = AffineTransform.getTranslateInstance(-oX,
308: -oY);
309: FontRenderContext frc = g2.getFontRenderContext();
310: Object[] cachedRefs = (Object[]) attributedStrings.get(text);
311: AttributedString as;
312: int[] lengths;
313: if (cachedRefs == null) {
314: Object[] refs = new Object[2];
315: lengths = new int[1];
316: refs[0] = as = getAttributedString(text, font, lengths);
317: refs[1] = lengths;
318: attributedStrings.put(text, refs);
319: } else {
320: as = (AttributedString) cachedRefs[0];
321: lengths = (int[]) cachedRefs[1];
322: }
323:
324: LineBreakMeasurer measurer = new LineBreakMeasurer(as
325: .getIterator(), BreakIterator.getWordInstance(), frc);
326: int ii = 0;
327: int len = lengths[0];
328: try {
329: for (int columnIdx = currentColumn; columnIdx < columns.length; columnIdx++) {
330: if ((columnIdx > 0) && (measurer.getPosition() < len))
331: pen.y = currentPos;
332: while ((measurer.getPosition() < len) && (pen.y < oH)) {
333: ii++;
334: Rectangle2D.Double rLine = new Rectangle2D.Double(
335: oX, oY + pen.y, oW, ascent);
336: Area aArea = new Area(rLine);
337: if (flowArea != null)
338: aArea.intersect(columns[columnIdx]);
339: Shape s = at.createTransformedShape(aArea);
340: Rectangle r = s.getBounds();
341: int w = (int) r.getWidth();
342: if (w > 15)
343: layout = measurer.nextLayout(w - 15);
344: else
345: layout = null;
346: pen.y += ascent;
347: if (layout == null)
348: continue;
349: else
350: layout
351: .draw(g2, (float) r.getX(),
352: (float) pen.y);
353:
354: // Only increment the current column index when something has actually
355: // been drawn in the column. If we get this far then something has
356: // been drawn
357: currentColumn = columnIdx;
358: }
359: currentPos = y;
360: }
361: } catch (Exception ex1) {
362: ex1.printStackTrace();
363: }
364: return pen.y;
365: }
366:
367: /**
368: * Get an AttribtedString for this component, converting the tags
369: * <b>, <i>, <u>, <sub>, <super>, <swap>, <justify>, <strike>
370: * to the proper internal form.
371: * @param localCopy the text being rendered
372: * @param font the current font
373: * @param lengths on return contains the length of the text to be rendered - the argument should be a string[1]
374: * @return the new AttributedString
375: */
376: protected AttributedString getAttributedString(String localCopy,
377: Font font, int[] lengths) {
378: ArrayList attributes = new ArrayList();
379:
380: String[] startTags = { "<b>", "<i>", "<u>", "<sub>", "<super>",
381: "<swap>", "<justify>", "<strike>" };
382: String[] endTags = { "</b>", "</i>", "</u>", "</sub>",
383: "</super>", "</swap>", "</justify>", "</strike>" };
384: AttributedCharacterIterator.Attribute[] keys = {
385: TextAttribute.WEIGHT, TextAttribute.POSTURE,
386: TextAttribute.UNDERLINE, TextAttribute.SUPERSCRIPT,
387: TextAttribute.SUPERSCRIPT, TextAttribute.SWAP_COLORS,
388: TextAttribute.JUSTIFICATION,
389: TextAttribute.STRIKETHROUGH };
390: for (int i = 0; i < startTags.length; i++) {
391: int pos = localCopy.indexOf(startTags[i]);
392: while (pos >= 0) {
393: int endPos = localCopy.indexOf(endTags[i]);
394: if (endPos > pos) {
395: int tagLen = startTags[i].length();
396: localCopy = localCopy.substring(0, pos)
397: + localCopy.substring(pos + tagLen, endPos)
398: + localCopy.substring(endPos + tagLen + 1);
399: Object value = null;
400: switch (i) {
401: case 0: //TextAttribute.WEIGHT:
402: value = new Float(TextAttribute.WEIGHT_BOLD
403: .floatValue());
404: break;
405: case 1: //TextAttribute.POSTURE:
406: value = new Float(TextAttribute.POSTURE_OBLIQUE
407: .floatValue());
408: break;
409: case 2: //TextAttribute.UNDERLINE:
410: value = new Float(TextAttribute.UNDERLINE_ON
411: .floatValue());
412: break;
413: case 3: //TextAttribute.SUPERSCRIPT:
414: case 4: //TextAttribute.SUPERSCRIPT:
415: value = new Float(
416: i == 3 ? TextAttribute.SUPERSCRIPT_SUPER
417: .floatValue()
418: : TextAttribute.SUPERSCRIPT_SUB
419: .floatValue());
420: break;
421: case 5: //TextAttribute.SWAP_COLORS:
422: value = TextAttribute.SWAP_COLORS_ON;
423: break;
424: case 6: //TextAttribute.JUSTIFICATION:
425: value = new Float(
426: TextAttribute.JUSTIFICATION_FULL
427: .floatValue());
428: break;
429: case 7: //TextAttribute.STRIKETHROUGH:
430: value = TextAttribute.STRIKETHROUGH_ON;
431: break;
432: }
433: int numAttribs = attributes.size();
434: for (int j = 0; j < numAttribs; j++) {
435: Attrib attr = (Attrib) attributes.get(j);
436: if (attr.start > pos)
437: attr.start -= tagLen;
438: if (attr.end > pos)
439: attr.end -= tagLen;
440: if (attr.start > endPos)
441: attr.start -= tagLen + 1;
442: if (attr.end > endPos)
443: attr.end -= tagLen + 1;
444: }
445: attributes.add(new Attrib(keys[i], value, pos,
446: endPos - tagLen));
447: }
448: pos = localCopy.indexOf(startTags[i]);
449: }
450: }
451:
452: AttributedString as = new AttributedString(localCopy, font
453: .getAttributes());
454: int numAttribs = attributes.size();
455: for (int i = 0; i < numAttribs; i++) {
456: Attrib attr = (Attrib) attributes.get(i);
457: as
458: .addAttribute(attr.type, attr.value, attr.start,
459: attr.end);
460: }
461: lengths[0] = localCopy.length();
462: return as;
463: }
464:
465: /**
466: * Set the clip state
467: * @param state true to clip siblings
468: */
469: public void setClip(boolean state) {
470: clip = state;
471: init();
472: repaint();
473: }
474:
475: /**
476: * Get the clip state
477: * @return true if siblings are clipped
478: */
479: public boolean getClip() {
480: return clip;
481: }
482:
483: /**
484: * A structure for holding references to the attributes of the string
485: */
486: protected class Attrib {
487: int start, end;
488: AttributedCharacterIterator.Attribute type;
489: Object value;
490:
491: /**
492: * Create a new Attrib holder
493: * @param t the type of attribute
494: * @param v the value of the attribute
495: * @param s the starting offset
496: * @param e the ending offset
497: */
498: public Attrib(AttributedCharacterIterator.Attribute t,
499: Object v, int s, int e) {
500: type = t;
501: value = v;
502: start = s;
503: end = e;
504: }
505: }
506: }
|