1 /*
2 * Copyright 1999-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25 package javax.swing.text;
26
27 import java.awt;
28 import java.text.BreakIterator;
29 import javax.swing.event;
30 import java.util.BitSet;
31 import java.util.Locale;
32
33 import sun.swing.SwingUtilities2;
34
35 /**
36 * A GlyphView is a styled chunk of text that represents a view
37 * mapped over an element in the text model. This view is generally
38 * responsible for displaying text glyphs using character level
39 * attributes in some way.
40 * An implementation of the GlyphPainter class is used to do the
41 * actual rendering and model/view translations. This separates
42 * rendering from layout and management of the association with
43 * the model.
44 * <p>
45 * The view supports breaking for the purpose of formatting.
46 * The fragments produced by breaking share the view that has
47 * primary responsibility for the element (i.e. they are nested
48 * classes and carry only a small amount of state of their own)
49 * so they can share its resources.
50 * <p>
51 * Since this view
52 * represents text that may have tabs embedded in it, it implements the
53 * <code>TabableView</code> interface. Tabs will only be
54 * expanded if this view is embedded in a container that does
55 * tab expansion. ParagraphView is an example of a container
56 * that does tab expansion.
57 * <p>
58 *
59 * @since 1.3
60 *
61 * @author Timothy Prinzing
62 */
63 public class GlyphView extends View implements TabableView, Cloneable {
64
65 /**
66 * Constructs a new view wrapped on an element.
67 *
68 * @param elem the element
69 */
70 public GlyphView(Element elem) {
71 super(elem);
72 offset = 0;
73 length = 0;
74 Element parent = elem.getParentElement();
75 AttributeSet attr = elem.getAttributes();
76
77 // if there was an implied CR
78 impliedCR = (attr != null && attr.getAttribute(IMPLIED_CR) != null &&
79 // if this is non-empty paragraph
80 parent != null && parent.getElementCount() > 1);
81 skipWidth = elem.getName().equals("br");
82 }
83
84 /**
85 * Creates a shallow copy. This is used by the
86 * createFragment and breakView methods.
87 *
88 * @return the copy
89 */
90 protected final Object clone() {
91 Object o;
92 try {
93 o = super.clone();
94 } catch (CloneNotSupportedException cnse) {
95 o = null;
96 }
97 return o;
98 }
99
100 /**
101 * Fetch the currently installed glyph painter.
102 * If a painter has not yet been installed, and
103 * a default was not yet needed, null is returned.
104 */
105 public GlyphPainter getGlyphPainter() {
106 return painter;
107 }
108
109 /**
110 * Sets the painter to use for rendering glyphs.
111 */
112 public void setGlyphPainter(GlyphPainter p) {
113 painter = p;
114 }
115
116 /**
117 * Fetch a reference to the text that occupies
118 * the given range. This is normally used by
119 * the GlyphPainter to determine what characters
120 * it should render glyphs for.
121 *
122 * @param p0 the starting document offset >= 0
123 * @param p1 the ending document offset >= p0
124 * @return the <code>Segment</code> containing the text
125 */
126 public Segment getText(int p0, int p1) {
127 // When done with the returned Segment it should be released by
128 // invoking:
129 // SegmentCache.releaseSharedSegment(segment);
130 Segment text = SegmentCache.getSharedSegment();
131 try {
132 Document doc = getDocument();
133 doc.getText(p0, p1 - p0, text);
134 } catch (BadLocationException bl) {
135 throw new StateInvariantError("GlyphView: Stale view: " + bl);
136 }
137 return text;
138 }
139
140 /**
141 * Fetch the background color to use to render the
142 * glyphs. If there is no background color, null should
143 * be returned. This is implemented to call
144 * <code>StyledDocument.getBackground</code> if the associated
145 * document is a styled document, otherwise it returns null.
146 */
147 public Color getBackground() {
148 Document doc = getDocument();
149 if (doc instanceof StyledDocument) {
150 AttributeSet attr = getAttributes();
151 if (attr.isDefined(StyleConstants.Background)) {
152 return ((StyledDocument)doc).getBackground(attr);
153 }
154 }
155 return null;
156 }
157
158 /**
159 * Fetch the foreground color to use to render the
160 * glyphs. If there is no foreground color, null should
161 * be returned. This is implemented to call
162 * <code>StyledDocument.getBackground</code> if the associated
163 * document is a StyledDocument. If the associated document
164 * is not a StyledDocument, the associated components foreground
165 * color is used. If there is no associated component, null
166 * is returned.
167 */
168 public Color getForeground() {
169 Document doc = getDocument();
170 if (doc instanceof StyledDocument) {
171 AttributeSet attr = getAttributes();
172 return ((StyledDocument)doc).getForeground(attr);
173 }
174 Component c = getContainer();
175 if (c != null) {
176 return c.getForeground();
177 }
178 return null;
179 }
180
181 /**
182 * Fetch the font that the glyphs should be based
183 * upon. This is implemented to call
184 * <code>StyledDocument.getFont</code> if the associated
185 * document is a StyledDocument. If the associated document
186 * is not a StyledDocument, the associated components font
187 * is used. If there is no associated component, null
188 * is returned.
189 */
190 public Font getFont() {
191 Document doc = getDocument();
192 if (doc instanceof StyledDocument) {
193 AttributeSet attr = getAttributes();
194 return ((StyledDocument)doc).getFont(attr);
195 }
196 Component c = getContainer();
197 if (c != null) {
198 return c.getFont();
199 }
200 return null;
201 }
202
203 /**
204 * Determine if the glyphs should be underlined. If true,
205 * an underline should be drawn through the baseline.
206 */
207 public boolean isUnderline() {
208 AttributeSet attr = getAttributes();
209 return StyleConstants.isUnderline(attr);
210 }
211
212 /**
213 * Determine if the glyphs should have a strikethrough
214 * line. If true, a line should be drawn through the center
215 * of the glyphs.
216 */
217 public boolean isStrikeThrough() {
218 AttributeSet attr = getAttributes();
219 return StyleConstants.isStrikeThrough(attr);
220 }
221
222 /**
223 * Determine if the glyphs should be rendered as superscript.
224 */
225 public boolean isSubscript() {
226 AttributeSet attr = getAttributes();
227 return StyleConstants.isSubscript(attr);
228 }
229
230 /**
231 * Determine if the glyphs should be rendered as subscript.
232 */
233 public boolean isSuperscript() {
234 AttributeSet attr = getAttributes();
235 return StyleConstants.isSuperscript(attr);
236 }
237
238 /**
239 * Fetch the TabExpander to use if tabs are present in this view.
240 */
241 public TabExpander getTabExpander() {
242 return expander;
243 }
244
245 /**
246 * Check to see that a glyph painter exists. If a painter
247 * doesn't exist, a default glyph painter will be installed.
248 */
249 protected void checkPainter() {
250 if (painter == null) {
251 if (defaultPainter == null) {
252 // the classname should probably come from a property file.
253 String classname = "javax.swing.text.GlyphPainter1";
254 try {
255 Class c;
256 ClassLoader loader = getClass().getClassLoader();
257 if (loader != null) {
258 c = loader.loadClass(classname);
259 } else {
260 c = Class.forName(classname);
261 }
262 Object o = c.newInstance();
263 if (o instanceof GlyphPainter) {
264 defaultPainter = (GlyphPainter) o;
265 }
266 } catch (Throwable e) {
267 throw new StateInvariantError("GlyphView: Can't load glyph painter: "
268 + classname);
269 }
270 }
271 setGlyphPainter(defaultPainter.getPainter(this, getStartOffset(),
272 getEndOffset()));
273 }
274 }
275
276 // --- TabableView methods --------------------------------------
277
278 /**
279 * Determines the desired span when using the given
280 * tab expansion implementation.
281 *
282 * @param x the position the view would be located
283 * at for the purpose of tab expansion >= 0.
284 * @param e how to expand the tabs when encountered.
285 * @return the desired span >= 0
286 * @see TabableView#getTabbedSpan
287 */
288 public float getTabbedSpan(float x, TabExpander e) {
289 checkPainter();
290
291 TabExpander old = expander;
292 expander = e;
293
294 if (expander != old) {
295 // setting expander can change horizontal span of the view,
296 // so we have to call preferenceChanged()
297 preferenceChanged(null, true, false);
298 }
299
300 this.x = (int) x;
301 int p0 = getStartOffset();
302 int p1 = getEndOffset();
303 float width = painter.getSpan(this, p0, p1, expander, x);
304 return width;
305 }
306
307 /**
308 * Determines the span along the same axis as tab
309 * expansion for a portion of the view. This is
310 * intended for use by the TabExpander for cases
311 * where the tab expansion involves aligning the
312 * portion of text that doesn't have whitespace
313 * relative to the tab stop. There is therefore
314 * an assumption that the range given does not
315 * contain tabs.
316 * <p>
317 * This method can be called while servicing the
318 * getTabbedSpan or getPreferredSize. It has to
319 * arrange for its own text buffer to make the
320 * measurements.
321 *
322 * @param p0 the starting document offset >= 0
323 * @param p1 the ending document offset >= p0
324 * @return the span >= 0
325 */
326 public float getPartialSpan(int p0, int p1) {
327 checkPainter();
328 float width = painter.getSpan(this, p0, p1, expander, x);
329 return width;
330 }
331
332 // --- View methods ---------------------------------------------
333
334 /**
335 * Fetches the portion of the model that this view is responsible for.
336 *
337 * @return the starting offset into the model
338 * @see View#getStartOffset
339 */
340 public int getStartOffset() {
341 Element e = getElement();
342 return (length > 0) ? e.getStartOffset() + offset : e.getStartOffset();
343 }
344
345 /**
346 * Fetches the portion of the model that this view is responsible for.
347 *
348 * @return the ending offset into the model
349 * @see View#getEndOffset
350 */
351 public int getEndOffset() {
352 Element e = getElement();
353 return (length > 0) ? e.getStartOffset() + offset + length : e.getEndOffset();
354 }
355
356 /**
357 * Lazily initializes the selections field
358 */
359 private void initSelections(int p0, int p1) {
360 int viewPosCount = p1 - p0 + 1;
361 if (selections == null || viewPosCount > selections.length) {
362 selections = new byte[viewPosCount];
363 return;
364 }
365 for (int i = 0; i < viewPosCount; selections[i++] = 0);
366 }
367
368 /**
369 * Renders a portion of a text style run.
370 *
371 * @param g the rendering surface to use
372 * @param a the allocated region to render into
373 */
374 public void paint(Graphics g, Shape a) {
375 checkPainter();
376
377 boolean paintedText = false;
378 Component c = getContainer();
379 int p0 = getStartOffset();
380 int p1 = getEndOffset();
381 Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
382 Color bg = getBackground();
383 Color fg = getForeground();
384
385 if (c instanceof JTextComponent) {
386 JTextComponent tc = (JTextComponent) c;
387 if (!tc.isEnabled()) {
388 fg = tc.getDisabledTextColor();
389 }
390 }
391 if (bg != null) {
392 g.setColor(bg);
393 g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height);
394 }
395 if (c instanceof JTextComponent) {
396 JTextComponent tc = (JTextComponent) c;
397 Highlighter h = tc.getHighlighter();
398 if (h instanceof LayeredHighlighter) {
399 ((LayeredHighlighter)h).paintLayeredHighlights
400 (g, p0, p1, a, tc, this);
401 }
402 }
403
404 if (Utilities.isComposedTextElement(getElement())) {
405 Utilities.paintComposedText(g, a.getBounds(), this);
406 paintedText = true;
407 } else if(c instanceof JTextComponent) {
408 JTextComponent tc = (JTextComponent) c;
409 Color selFG = tc.getSelectedTextColor();
410
411 if (// there's a highlighter (bug 4532590), and
412 (tc.getHighlighter() != null) &&
413 // selected text color is different from regular foreground
414 (selFG != null) && !selFG.equals(fg)) {
415
416 Highlighter.Highlight[] h = tc.getHighlighter().getHighlights();
417 if(h.length != 0) {
418 boolean initialized = false;
419 int viewSelectionCount = 0;
420 for (int i = 0; i < h.length; i++) {
421 Highlighter.Highlight highlight = h[i];
422 int hStart = highlight.getStartOffset();
423 int hEnd = highlight.getEndOffset();
424 if (hStart > p1 || hEnd < p0) {
425 // the selection is out of this view
426 continue;
427 }
428 if (!SwingUtilities2.useSelectedTextColor(highlight, tc)) {
429 continue;
430 }
431 if (hStart <= p0 && hEnd >= p1){
432 // the whole view is selected
433 paintTextUsingColor(g, a, selFG, p0, p1);
434 paintedText = true;
435 break;
436 }
437 // the array is lazily created only when the view
438 // is partially selected
439 if (!initialized) {
440 initSelections(p0, p1);
441 initialized = true;
442 }
443 hStart = Math.max(p0, hStart);
444 hEnd = Math.min(p1, hEnd);
445 paintTextUsingColor(g, a, selFG, hStart, hEnd);
446 // the array represents view positions [0, p1-p0+1]
447 // later will iterate this array and sum its
448 // elements. Positions with sum == 0 are not selected.
449 selections[hStart-p0]++;
450 selections[hEnd-p0]--;
451
452 viewSelectionCount++;
453 }
454
455 if (!paintedText && viewSelectionCount > 0) {
456 // the view is partially selected
457 int curPos = -1;
458 int startPos = 0;
459 int viewLen = p1 - p0;
460 while (curPos++ < viewLen) {
461 // searching for the next selection start
462 while(curPos < viewLen &&
463 selections[curPos] == 0) curPos++;
464 if (startPos != curPos) {
465 // paint unselected text
466 paintTextUsingColor(g, a, fg,
467 p0 + startPos, p0 + curPos);
468 }
469 int checkSum = 0;
470 // searching for next start position of unselected text
471 while (curPos < viewLen &&
472 (checkSum += selections[curPos]) != 0) curPos++;
473 startPos = curPos;
474 }
475 paintedText = true;
476 }
477 }
478 }
479 }
480 if(!paintedText)
481 paintTextUsingColor(g, a, fg, p0, p1);
482 }
483
484 /**
485 * Paints the specified region of text in the specified color.
486 */
487 final void paintTextUsingColor(Graphics g, Shape a, Color c, int p0, int p1) {
488 // render the glyphs
489 g.setColor(c);
490 painter.paint(this, g, a, p0, p1);
491
492 // render underline or strikethrough if set.
493 boolean underline = isUnderline();
494 boolean strike = isStrikeThrough();
495 if (underline || strike) {
496 // calculate x coordinates
497 Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
498 View parent = getParent();
499 if ((parent != null) && (parent.getEndOffset() == p1)) {
500 // strip whitespace on end
501 Segment s = getText(p0, p1);
502 while (Character.isWhitespace(s.last())) {
503 p1 -= 1;
504 s.count -= 1;
505 }
506 SegmentCache.releaseSharedSegment(s);
507 }
508 int x0 = alloc.x;
509 int p = getStartOffset();
510 if (p != p0) {
511 x0 += (int) painter.getSpan(this, p, p0, getTabExpander(), x0);
512 }
513 int x1 = x0 + (int) painter.getSpan(this, p0, p1, getTabExpander(), x0);
514
515 // calculate y coordinate
516 int y = alloc.y + alloc.height - (int) painter.getDescent(this);
517 if (underline) {
518 int yTmp = y + 1;
519 g.drawLine(x0, yTmp, x1, yTmp);
520 }
521 if (strike) {
522 // move y coordinate above baseline
523 int yTmp = y - (int) (painter.getAscent(this) * 0.3f);
524 g.drawLine(x0, yTmp, x1, yTmp);
525 }
526
527 }
528 }
529
530 /**
531 * Determines the minimum span for this view along an axis.
532 *
533 * <p>This implementation returns the longest non-breakable area within
534 * the view as a minimum span for {@code View.X_AXIS}.</p>
535 *
536 * @param axis may be either {@code View.X_AXIS} or {@code View.Y_AXIS}
537 * @return the minimum span the view can be rendered into
538 * @throws IllegalArgumentException if the {@code axis} parameter is invalid
539 * @see javax.swing.text.View#getMinimumSpan
540 */
541 @Override
542 public float getMinimumSpan(int axis) {
543 switch (axis) {
544 case View.X_AXIS:
545 if (minimumSpan < 0) {
546 minimumSpan = 0;
547 int p0 = getStartOffset();
548 int p1 = getEndOffset();
549 while (p1 > p0) {
550 int breakSpot = getBreakSpot(p0, p1);
551 if (breakSpot == BreakIterator.DONE) {
552 // the rest of the view is non-breakable
553 breakSpot = p0;
554 }
555 minimumSpan = Math.max(minimumSpan,
556 getPartialSpan(breakSpot, p1));
557 // Note: getBreakSpot returns the *last* breakspot
558 p1 = breakSpot - 1;
559 }
560 }
561 return minimumSpan;
562 case View.Y_AXIS:
563 return super.getMinimumSpan(axis);
564 default:
565 throw new IllegalArgumentException("Invalid axis: " + axis);
566 }
567 }
568
569 /**
570 * Determines the preferred span for this view along an
571 * axis.
572 *
573 * @param axis may be either View.X_AXIS or View.Y_AXIS
574 * @return the span the view would like to be rendered into >= 0.
575 * Typically the view is told to render into the span
576 * that is returned, although there is no guarantee.
577 * The parent may choose to resize or break the view.
578 */
579 public float getPreferredSpan(int axis) {
580 if (impliedCR) {
581 return 0;
582 }
583 checkPainter();
584 int p0 = getStartOffset();
585 int p1 = getEndOffset();
586 switch (axis) {
587 case View.X_AXIS:
588 if (skipWidth) {
589 return 0;
590 }
591 return painter.getSpan(this, p0, p1, expander, this.x);
592 case View.Y_AXIS:
593 float h = painter.getHeight(this);
594 if (isSuperscript()) {
595 h += h/3;
596 }
597 return h;
598 default:
599 throw new IllegalArgumentException("Invalid axis: " + axis);
600 }
601 }
602
603 /**
604 * Determines the desired alignment for this view along an
605 * axis. For the label, the alignment is along the font
606 * baseline for the y axis, and the superclasses alignment
607 * along the x axis.
608 *
609 * @param axis may be either View.X_AXIS or View.Y_AXIS
610 * @return the desired alignment. This should be a value
611 * between 0.0 and 1.0 inclusive, where 0 indicates alignment at the
612 * origin and 1.0 indicates alignment to the full span
613 * away from the origin. An alignment of 0.5 would be the
614 * center of the view.
615 */
616 public float getAlignment(int axis) {
617 checkPainter();
618 if (axis == View.Y_AXIS) {
619 boolean sup = isSuperscript();
620 boolean sub = isSubscript();
621 float h = painter.getHeight(this);
622 float d = painter.getDescent(this);
623 float a = painter.getAscent(this);
624 float align;
625 if (sup) {
626 align = 1.0f;
627 } else if (sub) {
628 align = (h > 0) ? (h - (d + (a / 2))) / h : 0;
629 } else {
630 align = (h > 0) ? (h - d) / h : 0;
631 }
632 return align;
633 }
634 return super.getAlignment(axis);
635 }
636
637 /**
638 * Provides a mapping from the document model coordinate space
639 * to the coordinate space of the view mapped to it.
640 *
641 * @param pos the position to convert >= 0
642 * @param a the allocated region to render into
643 * @param b either <code>Position.Bias.Forward</code>
644 * or <code>Position.Bias.Backward</code>
645 * @return the bounding box of the given position
646 * @exception BadLocationException if the given position does not represent a
647 * valid location in the associated document
648 * @see View#modelToView
649 */
650 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
651 checkPainter();
652 return painter.modelToView(this, pos, b, a);
653 }
654
655 /**
656 * Provides a mapping from the view coordinate space to the logical
657 * coordinate space of the model.
658 *
659 * @param x the X coordinate >= 0
660 * @param y the Y coordinate >= 0
661 * @param a the allocated region to render into
662 * @param biasReturn either <code>Position.Bias.Forward</code>
663 * or <code>Position.Bias.Backward</code> is returned as the
664 * zero-th element of this array
665 * @return the location within the model that best represents the
666 * given point of view >= 0
667 * @see View#viewToModel
668 */
669 public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) {
670 checkPainter();
671 return painter.viewToModel(this, x, y, a, biasReturn);
672 }
673
674 /**
675 * Determines how attractive a break opportunity in
676 * this view is. This can be used for determining which
677 * view is the most attractive to call <code>breakView</code>
678 * on in the process of formatting. The
679 * higher the weight, the more attractive the break. A
680 * value equal to or lower than <code>View.BadBreakWeight</code>
681 * should not be considered for a break. A value greater
682 * than or equal to <code>View.ForcedBreakWeight</code> should
683 * be broken.
684 * <p>
685 * This is implemented to forward to the superclass for
686 * the Y_AXIS. Along the X_AXIS the following values
687 * may be returned.
688 * <dl>
689 * <dt><b>View.ExcellentBreakWeight</b>
690 * <dd>if there is whitespace proceeding the desired break
691 * location.
692 * <dt><b>View.BadBreakWeight</b>
693 * <dd>if the desired break location results in a break
694 * location of the starting offset.
695 * <dt><b>View.GoodBreakWeight</b>
696 * <dd>if the other conditions don't occur.
697 * </dl>
698 * This will normally result in the behavior of breaking
699 * on a whitespace location if one can be found, otherwise
700 * breaking between characters.
701 *
702 * @param axis may be either View.X_AXIS or View.Y_AXIS
703 * @param pos the potential location of the start of the
704 * broken view >= 0. This may be useful for calculating tab
705 * positions.
706 * @param len specifies the relative length from <em>pos</em>
707 * where a potential break is desired >= 0.
708 * @return the weight, which should be a value between
709 * View.ForcedBreakWeight and View.BadBreakWeight.
710 * @see LabelView
711 * @see ParagraphView
712 * @see View#BadBreakWeight
713 * @see View#GoodBreakWeight
714 * @see View#ExcellentBreakWeight
715 * @see View#ForcedBreakWeight
716 */
717 public int getBreakWeight(int axis, float pos, float len) {
718 if (axis == View.X_AXIS) {
719 checkPainter();
720 int p0 = getStartOffset();
721 int p1 = painter.getBoundedPosition(this, p0, pos, len);
722 return ((p1 > p0) && (getBreakSpot(p0, p1) != BreakIterator.DONE)) ?
723 View.ExcellentBreakWeight : View.BadBreakWeight;
724 }
725 return super.getBreakWeight(axis, pos, len);
726 }
727
728 /**
729 * Breaks this view on the given axis at the given length.
730 * This is implemented to attempt to break on a whitespace
731 * location, and returns a fragment with the whitespace at
732 * the end. If a whitespace location can't be found, the
733 * nearest character is used.
734 *
735 * @param axis may be either View.X_AXIS or View.Y_AXIS
736 * @param p0 the location in the model where the
737 * fragment should start it's representation >= 0.
738 * @param pos the position along the axis that the
739 * broken view would occupy >= 0. This may be useful for
740 * things like tab calculations.
741 * @param len specifies the distance along the axis
742 * where a potential break is desired >= 0.
743 * @return the fragment of the view that represents the
744 * given span, if the view can be broken. If the view
745 * doesn't support breaking behavior, the view itself is
746 * returned.
747 * @see View#breakView
748 */
749 public View breakView(int axis, int p0, float pos, float len) {
750 if (axis == View.X_AXIS) {
751 checkPainter();
752 int p1 = painter.getBoundedPosition(this, p0, pos, len);
753 int breakSpot = getBreakSpot(p0, p1);
754
755 if (breakSpot != -1) {
756 p1 = breakSpot;
757 }
758 // else, no break in the region, return a fragment of the
759 // bounded region.
760 if (p0 == getStartOffset() && p1 == getEndOffset()) {
761 return this;
762 }
763 GlyphView v = (GlyphView) createFragment(p0, p1);
764 v.x = (int) pos;
765 return v;
766 }
767 return this;
768 }
769
770 /**
771 * Returns a location to break at in the passed in region, or
772 * BreakIterator.DONE if there isn't a good location to break at
773 * in the specified region.
774 */
775 private int getBreakSpot(int p0, int p1) {
776 if (breakSpots == null) {
777 // Re-calculate breakpoints for the whole view
778 int start = getStartOffset();
779 int end = getEndOffset();
780 int[] bs = new int[end + 1 - start];
781 int ix = 0;
782
783 // Breaker should work on the parent element because there may be
784 // a valid breakpoint at the end edge of the view (space, etc.)
785 Element parent = getElement().getParentElement();
786 int pstart = (parent == null ? start : parent.getStartOffset());
787 int pend = (parent == null ? end : parent.getEndOffset());
788
789 Segment s = getText(pstart, pend);
790 s.first();
791 BreakIterator breaker = getBreaker();
792 breaker.setText(s);
793
794 // Backward search should start from end+1 unless there's NO end+1
795 int startFrom = end + (pend > end ? 1 : 0);
796 for (;;) {
797 startFrom = breaker.preceding(s.offset + (startFrom - pstart))
798 + (pstart - s.offset);
799 if (startFrom > start) {
800 // The break spot is within the view
801 bs[ix++] = startFrom;
802 } else {
803 break;
804 }
805 }
806
807 SegmentCache.releaseSharedSegment(s);
808 breakSpots = new int[ix];
809 System.arraycopy(bs, 0, breakSpots, 0, ix);
810 }
811
812 int breakSpot = BreakIterator.DONE;
813 for (int i = 0; i < breakSpots.length; i++) {
814 int bsp = breakSpots[i];
815 if (bsp <= p1) {
816 if (bsp > p0) {
817 breakSpot = bsp;
818 }
819 break;
820 }
821 }
822 return breakSpot;
823 }
824
825 /**
826 * Return break iterator appropriate for the current document.
827 *
828 * For non-i18n documents a fast whitespace-based break iterator is used.
829 */
830 private BreakIterator getBreaker() {
831 Document doc = getDocument();
832 if ((doc != null) && Boolean.TRUE.equals(
833 doc.getProperty(AbstractDocument.MultiByteProperty))) {
834 Container c = getContainer();
835 Locale locale = (c == null ? Locale.getDefault() : c.getLocale());
836 return BreakIterator.getLineInstance(locale);
837 } else {
838 return new WhitespaceBasedBreakIterator();
839 }
840 }
841
842 /**
843 * Creates a view that represents a portion of the element.
844 * This is potentially useful during formatting operations
845 * for taking measurements of fragments of the view. If
846 * the view doesn't support fragmenting (the default), it
847 * should return itself.
848 * <p>
849 * This view does support fragmenting. It is implemented
850 * to return a nested class that shares state in this view
851 * representing only a portion of the view.
852 *
853 * @param p0 the starting offset >= 0. This should be a value
854 * greater or equal to the element starting offset and
855 * less than the element ending offset.
856 * @param p1 the ending offset > p0. This should be a value
857 * less than or equal to the elements end offset and
858 * greater than the elements starting offset.
859 * @return the view fragment, or itself if the view doesn't
860 * support breaking into fragments
861 * @see LabelView
862 */
863 public View createFragment(int p0, int p1) {
864 checkPainter();
865 Element elem = getElement();
866 GlyphView v = (GlyphView) clone();
867 v.offset = p0 - elem.getStartOffset();
868 v.length = p1 - p0;
869 v.painter = painter.getPainter(v, p0, p1);
870 v.justificationInfo = null;
871 return v;
872 }
873
874 /**
875 * Provides a way to determine the next visually represented model
876 * location that one might place a caret. Some views may not be
877 * visible, they might not be in the same order found in the model, or
878 * they just might not allow access to some of the locations in the
879 * model.
880 *
881 * @param pos the position to convert >= 0
882 * @param a the allocated region to render into
883 * @param direction the direction from the current position that can
884 * be thought of as the arrow keys typically found on a keyboard.
885 * This may be SwingConstants.WEST, SwingConstants.EAST,
886 * SwingConstants.NORTH, or SwingConstants.SOUTH.
887 * @return the location within the model that best represents the next
888 * location visual position.
889 * @exception BadLocationException
890 * @exception IllegalArgumentException for an invalid direction
891 */
892 public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
893 int direction,
894 Position.Bias[] biasRet)
895 throws BadLocationException {
896
897 return painter.getNextVisualPositionFrom(this, pos, b, a, direction, biasRet);
898 }
899
900 /**
901 * Gives notification that something was inserted into
902 * the document in a location that this view is responsible for.
903 * This is implemented to call preferenceChanged along the
904 * axis the glyphs are rendered.
905 *
906 * @param e the change information from the associated document
907 * @param a the current allocation of the view
908 * @param f the factory to use to rebuild if the view has children
909 * @see View#insertUpdate
910 */
911 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) {
912 justificationInfo = null;
913 breakSpots = null;
914 minimumSpan = -1;
915 syncCR();
916 preferenceChanged(null, true, false);
917 }
918
919 /**
920 * Gives notification that something was removed from the document
921 * in a location that this view is responsible for.
922 * This is implemented to call preferenceChanged along the
923 * axis the glyphs are rendered.
924 *
925 * @param e the change information from the associated document
926 * @param a the current allocation of the view
927 * @param f the factory to use to rebuild if the view has children
928 * @see View#removeUpdate
929 */
930 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) {
931 justificationInfo = null;
932 breakSpots = null;
933 minimumSpan = -1;
934 syncCR();
935 preferenceChanged(null, true, false);
936 }
937
938 /**
939 * Gives notification from the document that attributes were changed
940 * in a location that this view is responsible for.
941 * This is implemented to call preferenceChanged along both the
942 * horizontal and vertical axis.
943 *
944 * @param e the change information from the associated document
945 * @param a the current allocation of the view
946 * @param f the factory to use to rebuild if the view has children
947 * @see View#changedUpdate
948 */
949 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
950 minimumSpan = -1;
951 syncCR();
952 preferenceChanged(null, true, true);
953 }
954
955 // checks if the paragraph is empty and updates impliedCR flag
956 // accordingly
957 private void syncCR() {
958 if (impliedCR) {
959 Element parent = getElement().getParentElement();
960 impliedCR = (parent != null && parent.getElementCount() > 1);
961 }
962 }
963
964 /**
965 * Class to hold data needed to justify this GlyphView in a PargraphView.Row
966 */
967 static class JustificationInfo {
968 //justifiable content start
969 final int start;
970 //justifiable content end
971 final int end;
972 final int leadingSpaces;
973 final int contentSpaces;
974 final int trailingSpaces;
975 final boolean hasTab;
976 final BitSet spaceMap;
977 JustificationInfo(int start, int end,
978 int leadingSpaces,
979 int contentSpaces,
980 int trailingSpaces,
981 boolean hasTab,
982 BitSet spaceMap) {
983 this.start = start;
984 this.end = end;
985 this.leadingSpaces = leadingSpaces;
986 this.contentSpaces = contentSpaces;
987 this.trailingSpaces = trailingSpaces;
988 this.hasTab = hasTab;
989 this.spaceMap = spaceMap;
990 }
991 }
992
993
994
995 JustificationInfo getJustificationInfo(int rowStartOffset) {
996 if (justificationInfo != null) {
997 return justificationInfo;
998 }
999 //states for the parsing
1000 final int TRAILING = 0;
1001 final int CONTENT = 1;
1002 final int SPACES = 2;
1003 int startOffset = getStartOffset();
1004 int endOffset = getEndOffset();
1005 Segment segment = getText(startOffset, endOffset);
1006 int txtOffset = segment.offset;
1007 int txtEnd = segment.offset + segment.count - 1;
1008 int startContentPosition = txtEnd + 1;
1009 int endContentPosition = txtOffset - 1;
1010 int lastTabPosition = txtOffset - 1;
1011 int trailingSpaces = 0;
1012 int contentSpaces = 0;
1013 int leadingSpaces = 0;
1014 boolean hasTab = false;
1015 BitSet spaceMap = new BitSet(endOffset - startOffset + 1);
1016
1017 //we parse conent to the right of the rightmost TAB only.
1018 //we are looking for the trailing and leading spaces.
1019 //position after the leading spaces (startContentPosition)
1020 //position before the trailing spaces (endContentPosition)
1021 for (int i = txtEnd, state = TRAILING; i >= txtOffset; i--) {
1022 if (' ' == segment.array[i]) {
1023 spaceMap.set(i - txtOffset);
1024 if (state == TRAILING) {
1025 trailingSpaces++;
1026 } else if (state == CONTENT) {
1027 state = SPACES;
1028 leadingSpaces = 1;
1029 } else if (state == SPACES) {
1030 leadingSpaces++;
1031 }
1032 } else if ('\t' == segment.array[i]) {
1033 hasTab = true;
1034 break;
1035 } else {
1036 if (state == TRAILING) {
1037 if ('\n' != segment.array[i]
1038 && '\r' != segment.array[i]) {
1039 state = CONTENT;
1040 endContentPosition = i;
1041 }
1042 } else if (state == CONTENT) {
1043 //do nothing
1044 } else if (state == SPACES) {
1045 contentSpaces += leadingSpaces;
1046 leadingSpaces = 0;
1047 }
1048 startContentPosition = i;
1049 }
1050 }
1051
1052 SegmentCache.releaseSharedSegment(segment);
1053
1054 int startJustifiableContent = -1;
1055 if (startContentPosition < txtEnd) {
1056 startJustifiableContent =
1057 startContentPosition - txtOffset;
1058 }
1059 int endJustifiableContent = -1;
1060 if (endContentPosition > txtOffset) {
1061 endJustifiableContent =
1062 endContentPosition - txtOffset;
1063 }
1064 justificationInfo =
1065 new JustificationInfo(startJustifiableContent,
1066 endJustifiableContent,
1067 leadingSpaces,
1068 contentSpaces,
1069 trailingSpaces,
1070 hasTab,
1071 spaceMap);
1072 return justificationInfo;
1073 }
1074
1075 // --- variables ------------------------------------------------
1076
1077 /**
1078 * Used by paint() to store highlighted view positions
1079 */
1080 private byte[] selections = null;
1081
1082 int offset;
1083 int length;
1084 // if it is an implied newline character
1085 boolean impliedCR;
1086 private static final String IMPLIED_CR = "CR";
1087 boolean skipWidth;
1088
1089 /**
1090 * how to expand tabs
1091 */
1092 TabExpander expander;
1093
1094 /** Cached minimum x-span value */
1095 private float minimumSpan = -1;
1096
1097 /** Cached breakpoints within the view */
1098 private int[] breakSpots = null;
1099
1100 /**
1101 * location for determining tab expansion against.
1102 */
1103 int x;
1104
1105 /**
1106 * Glyph rendering functionality.
1107 */
1108 GlyphPainter painter;
1109
1110 /**
1111 * The prototype painter used by default.
1112 */
1113 static GlyphPainter defaultPainter;
1114
1115 private JustificationInfo justificationInfo = null;
1116
1117 /**
1118 * A class to perform rendering of the glyphs.
1119 * This can be implemented to be stateless, or
1120 * to hold some information as a cache to
1121 * facilitate faster rendering and model/view
1122 * translation. At a minimum, the GlyphPainter
1123 * allows a View implementation to perform its
1124 * duties independant of a particular version
1125 * of JVM and selection of capabilities (i.e.
1126 * shaping for i18n, etc).
1127 *
1128 * @since 1.3
1129 */
1130 public static abstract class GlyphPainter {
1131
1132 /**
1133 * Determine the span the glyphs given a start location
1134 * (for tab expansion).
1135 */
1136 public abstract float getSpan(GlyphView v, int p0, int p1, TabExpander e, float x);
1137
1138 public abstract float getHeight(GlyphView v);
1139
1140 public abstract float getAscent(GlyphView v);
1141
1142 public abstract float getDescent(GlyphView v);
1143
1144 /**
1145 * Paint the glyphs representing the given range.
1146 */
1147 public abstract void paint(GlyphView v, Graphics g, Shape a, int p0, int p1);
1148
1149 /**
1150 * Provides a mapping from the document model coordinate space
1151 * to the coordinate space of the view mapped to it.
1152 * This is shared by the broken views.
1153 *
1154 * @param v the <code>GlyphView</code> containing the
1155 * destination coordinate space
1156 * @param pos the position to convert
1157 * @param bias either <code>Position.Bias.Forward</code>
1158 * or <code>Position.Bias.Backward</code>
1159 * @param a Bounds of the View
1160 * @return the bounding box of the given position
1161 * @exception BadLocationException if the given position does not represent a
1162 * valid location in the associated document
1163 * @see View#modelToView
1164 */
1165 public abstract Shape modelToView(GlyphView v,
1166 int pos, Position.Bias bias,
1167 Shape a) throws BadLocationException;
1168
1169 /**
1170 * Provides a mapping from the view coordinate space to the logical
1171 * coordinate space of the model.
1172 *
1173 * @param v the <code>GlyphView</code> to provide a mapping for
1174 * @param x the X coordinate
1175 * @param y the Y coordinate
1176 * @param a the allocated region to render into
1177 * @param biasReturn either <code>Position.Bias.Forward</code>
1178 * or <code>Position.Bias.Backward</code>
1179 * is returned as the zero-th element of this array
1180 * @return the location within the model that best represents the
1181 * given point of view
1182 * @see View#viewToModel
1183 */
1184 public abstract int viewToModel(GlyphView v,
1185 float x, float y, Shape a,
1186 Position.Bias[] biasReturn);
1187
1188 /**
1189 * Determines the model location that represents the
1190 * maximum advance that fits within the given span.
1191 * This could be used to break the given view. The result
1192 * should be a location just shy of the given advance. This
1193 * differs from viewToModel which returns the closest
1194 * position which might be proud of the maximum advance.
1195 *
1196 * @param v the view to find the model location to break at.
1197 * @param p0 the location in the model where the
1198 * fragment should start it's representation >= 0.
1199 * @param x the graphic location along the axis that the
1200 * broken view would occupy >= 0. This may be useful for
1201 * things like tab calculations.
1202 * @param len specifies the distance into the view
1203 * where a potential break is desired >= 0.
1204 * @return the maximum model location possible for a break.
1205 * @see View#breakView
1206 */
1207 public abstract int getBoundedPosition(GlyphView v, int p0, float x, float len);
1208
1209 /**
1210 * Create a painter to use for the given GlyphView. If
1211 * the painter carries state it can create another painter
1212 * to represent a new GlyphView that is being created. If
1213 * the painter doesn't hold any significant state, it can
1214 * return itself. The default behavior is to return itself.
1215 * @param v the <code>GlyphView</code> to provide a painter for
1216 * @param p0 the starting document offset >= 0
1217 * @param p1 the ending document offset >= p0
1218 */
1219 public GlyphPainter getPainter(GlyphView v, int p0, int p1) {
1220 return this;
1221 }
1222
1223 /**
1224 * Provides a way to determine the next visually represented model
1225 * location that one might place a caret. Some views may not be
1226 * visible, they might not be in the same order found in the model, or
1227 * they just might not allow access to some of the locations in the
1228 * model.
1229 *
1230 * @param v the view to use
1231 * @param pos the position to convert >= 0
1232 * @param b either <code>Position.Bias.Forward</code>
1233 * or <code>Position.Bias.Backward</code>
1234 * @param a the allocated region to render into
1235 * @param direction the direction from the current position that can
1236 * be thought of as the arrow keys typically found on a keyboard.
1237 * This may be SwingConstants.WEST, SwingConstants.EAST,
1238 * SwingConstants.NORTH, or SwingConstants.SOUTH.
1239 * @param biasRet either <code>Position.Bias.Forward</code>
1240 * or <code>Position.Bias.Backward</code>
1241 * is returned as the zero-th element of this array
1242 * @return the location within the model that best represents the next
1243 * location visual position.
1244 * @exception BadLocationException
1245 * @exception IllegalArgumentException for an invalid direction
1246 */
1247 public int getNextVisualPositionFrom(GlyphView v, int pos, Position.Bias b, Shape a,
1248 int direction,
1249 Position.Bias[] biasRet)
1250 throws BadLocationException {
1251
1252 int startOffset = v.getStartOffset();
1253 int endOffset = v.getEndOffset();
1254 Segment text;
1255
1256 switch (direction) {
1257 case View.NORTH:
1258 case View.SOUTH:
1259 if (pos != -1) {
1260 // Presumably pos is between startOffset and endOffset,
1261 // since GlyphView is only one line, we won't contain
1262 // the position to the nort/south, therefore return -1.
1263 return -1;
1264 }
1265 Container container = v.getContainer();
1266
1267 if (container instanceof JTextComponent) {
1268 Caret c = ((JTextComponent)container).getCaret();
1269 Point magicPoint;
1270 magicPoint = (c != null) ? c.getMagicCaretPosition() :null;
1271
1272 if (magicPoint == null) {
1273 biasRet[0] = Position.Bias.Forward;
1274 return startOffset;
1275 }
1276 int value = v.viewToModel(magicPoint.x, 0f, a, biasRet);
1277 return value;
1278 }
1279 break;
1280 case View.EAST:
1281 if(startOffset == v.getDocument().getLength()) {
1282 if(pos == -1) {
1283 biasRet[0] = Position.Bias.Forward;
1284 return startOffset;
1285 }
1286 // End case for bidi text where newline is at beginning
1287 // of line.
1288 return -1;
1289 }
1290 if(pos == -1) {
1291 biasRet[0] = Position.Bias.Forward;
1292 return startOffset;
1293 }
1294 if(pos == endOffset) {
1295 return -1;
1296 }
1297 if(++pos == endOffset) {
1298 // Assumed not used in bidi text, GlyphPainter2 will
1299 // override as necessary, therefore return -1.
1300 return -1;
1301 }
1302 else {
1303 biasRet[0] = Position.Bias.Forward;
1304 }
1305 return pos;
1306 case View.WEST:
1307 if(startOffset == v.getDocument().getLength()) {
1308 if(pos == -1) {
1309 biasRet[0] = Position.Bias.Forward;
1310 return startOffset;
1311 }
1312 // End case for bidi text where newline is at beginning
1313 // of line.
1314 return -1;
1315 }
1316 if(pos == -1) {
1317 // Assumed not used in bidi text, GlyphPainter2 will
1318 // override as necessary, therefore return -1.
1319 biasRet[0] = Position.Bias.Forward;
1320 return endOffset - 1;
1321 }
1322 if(pos == startOffset) {
1323 return -1;
1324 }
1325 biasRet[0] = Position.Bias.Forward;
1326 return (pos - 1);
1327 default:
1328 throw new IllegalArgumentException("Bad direction: " + direction);
1329 }
1330 return pos;
1331
1332 }
1333 }
1334 }