/*
* Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javax.swing.text;
import java.awt.*;
import java.text.
BreakIterator;
import javax.swing.event.*;
import java.util.
BitSet;
import java.util.
Locale;
import javax.swing.
UIManager;
import sun.swing.
SwingUtilities2;
import static sun.swing.
SwingUtilities2.
IMPLIED_CR;
/**
* A GlyphView is a styled chunk of text that represents a view
* mapped over an element in the text model. This view is generally
* responsible for displaying text glyphs using character level
* attributes in some way.
* An implementation of the GlyphPainter class is used to do the
* actual rendering and model/view translations. This separates
* rendering from layout and management of the association with
* the model.
* <p>
* The view supports breaking for the purpose of formatting.
* The fragments produced by breaking share the view that has
* primary responsibility for the element (i.e. they are nested
* classes and carry only a small amount of state of their own)
* so they can share its resources.
* <p>
* Since this view
* represents text that may have tabs embedded in it, it implements the
* <code>TabableView</code> interface. Tabs will only be
* expanded if this view is embedded in a container that does
* tab expansion. ParagraphView is an example of a container
* that does tab expansion.
* <p>
*
* @since 1.3
*
* @author Timothy Prinzing
*/
public class
GlyphView extends
View implements
TabableView,
Cloneable {
/**
* Constructs a new view wrapped on an element.
*
* @param elem the element
*/
public
GlyphView(
Element elem) {
super(
elem);
offset = 0;
length = 0;
Element parent =
elem.
getParentElement();
AttributeSet attr =
elem.
getAttributes();
// if there was an implied CR
impliedCR = (
attr != null &&
attr.
getAttribute(
IMPLIED_CR) != null &&
// if this is non-empty paragraph
parent != null &&
parent.
getElementCount() > 1);
skipWidth =
elem.
getName().
equals("br");
}
/**
* Creates a shallow copy. This is used by the
* createFragment and breakView methods.
*
* @return the copy
*/
protected final
Object clone() {
Object o;
try {
o = super.clone();
} catch (
CloneNotSupportedException cnse) {
o = null;
}
return
o;
}
/**
* Fetch the currently installed glyph painter.
* If a painter has not yet been installed, and
* a default was not yet needed, null is returned.
*/
public
GlyphPainter getGlyphPainter() {
return
painter;
}
/**
* Sets the painter to use for rendering glyphs.
*/
public void
setGlyphPainter(
GlyphPainter p) {
painter =
p;
}
/**
* Fetch a reference to the text that occupies
* the given range. This is normally used by
* the GlyphPainter to determine what characters
* it should render glyphs for.
*
* @param p0 the starting document offset >= 0
* @param p1 the ending document offset >= p0
* @return the <code>Segment</code> containing the text
*/
public
Segment getText(int
p0, int
p1) {
// When done with the returned Segment it should be released by
// invoking:
// SegmentCache.releaseSharedSegment(segment);
Segment text =
SegmentCache.
getSharedSegment();
try {
Document doc =
getDocument();
doc.
getText(
p0,
p1 -
p0,
text);
} catch (
BadLocationException bl) {
throw new
StateInvariantError("GlyphView: Stale view: " +
bl);
}
return
text;
}
/**
* Fetch the background color to use to render the
* glyphs. If there is no background color, null should
* be returned. This is implemented to call
* <code>StyledDocument.getBackground</code> if the associated
* document is a styled document, otherwise it returns null.
*/
public
Color getBackground() {
Document doc =
getDocument();
if (
doc instanceof
StyledDocument) {
AttributeSet attr =
getAttributes();
if (
attr.
isDefined(
StyleConstants.
Background)) {
return ((
StyledDocument)
doc).
getBackground(
attr);
}
}
return null;
}
/**
* Fetch the foreground color to use to render the
* glyphs. If there is no foreground color, null should
* be returned. This is implemented to call
* <code>StyledDocument.getBackground</code> if the associated
* document is a StyledDocument. If the associated document
* is not a StyledDocument, the associated components foreground
* color is used. If there is no associated component, null
* is returned.
*/
public
Color getForeground() {
Document doc =
getDocument();
if (
doc instanceof
StyledDocument) {
AttributeSet attr =
getAttributes();
return ((
StyledDocument)
doc).
getForeground(
attr);
}
Component c =
getContainer();
if (
c != null) {
return
c.
getForeground();
}
return null;
}
/**
* Fetch the font that the glyphs should be based
* upon. This is implemented to call
* <code>StyledDocument.getFont</code> if the associated
* document is a StyledDocument. If the associated document
* is not a StyledDocument, the associated components font
* is used. If there is no associated component, null
* is returned.
*/
public
Font getFont() {
Document doc =
getDocument();
if (
doc instanceof
StyledDocument) {
AttributeSet attr =
getAttributes();
return ((
StyledDocument)
doc).
getFont(
attr);
}
Component c =
getContainer();
if (
c != null) {
return
c.
getFont();
}
return null;
}
/**
* Determine if the glyphs should be underlined. If true,
* an underline should be drawn through the baseline.
*/
public boolean
isUnderline() {
AttributeSet attr =
getAttributes();
return
StyleConstants.
isUnderline(
attr);
}
/**
* Determine if the glyphs should have a strikethrough
* line. If true, a line should be drawn through the center
* of the glyphs.
*/
public boolean
isStrikeThrough() {
AttributeSet attr =
getAttributes();
return
StyleConstants.
isStrikeThrough(
attr);
}
/**
* Determine if the glyphs should be rendered as superscript.
*/
public boolean
isSubscript() {
AttributeSet attr =
getAttributes();
return
StyleConstants.
isSubscript(
attr);
}
/**
* Determine if the glyphs should be rendered as subscript.
*/
public boolean
isSuperscript() {
AttributeSet attr =
getAttributes();
return
StyleConstants.
isSuperscript(
attr);
}
/**
* Fetch the TabExpander to use if tabs are present in this view.
*/
public
TabExpander getTabExpander() {
return
expander;
}
/**
* Check to see that a glyph painter exists. If a painter
* doesn't exist, a default glyph painter will be installed.
*/
protected void
checkPainter() {
if (
painter == null) {
if (
defaultPainter == null) {
// the classname should probably come from a property file.
String classname = "javax.swing.text.GlyphPainter1";
try {
Class c;
ClassLoader loader =
getClass().
getClassLoader();
if (
loader != null) {
c =
loader.
loadClass(
classname);
} else {
c =
Class.
forName(
classname);
}
Object o =
c.
newInstance();
if (
o instanceof
GlyphPainter) {
defaultPainter = (
GlyphPainter)
o;
}
} catch (
Throwable e) {
throw new
StateInvariantError("GlyphView: Can't load glyph painter: "
+
classname);
}
}
setGlyphPainter(
defaultPainter.
getPainter(this,
getStartOffset(),
getEndOffset()));
}
}
// --- TabableView methods --------------------------------------
/**
* Determines the desired span when using the given
* tab expansion implementation.
*
* @param x the position the view would be located
* at for the purpose of tab expansion >= 0.
* @param e how to expand the tabs when encountered.
* @return the desired span >= 0
* @see TabableView#getTabbedSpan
*/
public float
getTabbedSpan(float
x,
TabExpander e) {
checkPainter();
TabExpander old =
expander;
expander =
e;
if (
expander !=
old) {
// setting expander can change horizontal span of the view,
// so we have to call preferenceChanged()
preferenceChanged(null, true, false);
}
this.
x = (int)
x;
int
p0 =
getStartOffset();
int
p1 =
getEndOffset();
float
width =
painter.
getSpan(this,
p0,
p1,
expander,
x);
return
width;
}
/**
* Determines the span along the same axis as tab
* expansion for a portion of the view. This is
* intended for use by the TabExpander for cases
* where the tab expansion involves aligning the
* portion of text that doesn't have whitespace
* relative to the tab stop. There is therefore
* an assumption that the range given does not
* contain tabs.
* <p>
* This method can be called while servicing the
* getTabbedSpan or getPreferredSize. It has to
* arrange for its own text buffer to make the
* measurements.
*
* @param p0 the starting document offset >= 0
* @param p1 the ending document offset >= p0
* @return the span >= 0
*/
public float
getPartialSpan(int
p0, int
p1) {
checkPainter();
float
width =
painter.
getSpan(this,
p0,
p1,
expander,
x);
return
width;
}
// --- View methods ---------------------------------------------
/**
* Fetches the portion of the model that this view is responsible for.
*
* @return the starting offset into the model
* @see View#getStartOffset
*/
public int
getStartOffset() {
Element e =
getElement();
return (
length > 0) ?
e.
getStartOffset() +
offset :
e.
getStartOffset();
}
/**
* Fetches the portion of the model that this view is responsible for.
*
* @return the ending offset into the model
* @see View#getEndOffset
*/
public int
getEndOffset() {
Element e =
getElement();
return (
length > 0) ?
e.
getStartOffset() +
offset +
length :
e.
getEndOffset();
}
/**
* Lazily initializes the selections field
*/
private void
initSelections(int
p0, int
p1) {
int
viewPosCount =
p1 -
p0 + 1;
if (
selections == null ||
viewPosCount >
selections.length) {
selections = new byte[
viewPosCount];
return;
}
for (int
i = 0;
i <
viewPosCount;
selections[
i++] = 0);
}
/**
* Renders a portion of a text style run.
*
* @param g the rendering surface to use
* @param a the allocated region to render into
*/
public void
paint(
Graphics g,
Shape a) {
checkPainter();
boolean
paintedText = false;
Component c =
getContainer();
int
p0 =
getStartOffset();
int
p1 =
getEndOffset();
Rectangle alloc = (
a instanceof
Rectangle) ? (
Rectangle)
a :
a.
getBounds();
Color bg =
getBackground();
Color fg =
getForeground();
if (
c != null && !
c.
isEnabled()) {
fg = (
c instanceof
JTextComponent ?
((
JTextComponent)
c).
getDisabledTextColor() :
UIManager.
getColor("textInactiveText"));
}
if (
bg != null) {
g.
setColor(
bg);
g.
fillRect(
alloc.
x,
alloc.
y,
alloc.
width,
alloc.
height);
}
if (
c instanceof
JTextComponent) {
JTextComponent tc = (
JTextComponent)
c;
Highlighter h =
tc.
getHighlighter();
if (
h instanceof
LayeredHighlighter) {
((
LayeredHighlighter)
h).
paintLayeredHighlights
(
g,
p0,
p1,
a,
tc, this);
}
}
if (
Utilities.
isComposedTextElement(
getElement())) {
Utilities.
paintComposedText(
g,
a.
getBounds(), this);
paintedText = true;
} else if(
c instanceof
JTextComponent) {
JTextComponent tc = (
JTextComponent)
c;
Color selFG =
tc.
getSelectedTextColor();
if (// there's a highlighter (bug 4532590), and
(
tc.
getHighlighter() != null) &&
// selected text color is different from regular foreground
(
selFG != null) && !
selFG.
equals(
fg)) {
Highlighter.
Highlight[]
h =
tc.
getHighlighter().
getHighlights();
if(
h.length != 0) {
boolean
initialized = false;
int
viewSelectionCount = 0;
for (int
i = 0;
i <
h.length;
i++) {
Highlighter.
Highlight highlight =
h[
i];
int
hStart =
highlight.
getStartOffset();
int
hEnd =
highlight.
getEndOffset();
if (
hStart >
p1 ||
hEnd <
p0) {
// the selection is out of this view
continue;
}
if (!
SwingUtilities2.
useSelectedTextColor(
highlight,
tc)) {
continue;
}
if (
hStart <=
p0 &&
hEnd >=
p1){
// the whole view is selected
paintTextUsingColor(
g,
a,
selFG,
p0,
p1);
paintedText = true;
break;
}
// the array is lazily created only when the view
// is partially selected
if (!
initialized) {
initSelections(
p0,
p1);
initialized = true;
}
hStart =
Math.
max(
p0,
hStart);
hEnd =
Math.
min(
p1,
hEnd);
paintTextUsingColor(
g,
a,
selFG,
hStart,
hEnd);
// the array represents view positions [0, p1-p0+1]
// later will iterate this array and sum its
// elements. Positions with sum == 0 are not selected.
selections[
hStart-
p0]++;
selections[
hEnd-
p0]--;
viewSelectionCount++;
}
if (!
paintedText &&
viewSelectionCount > 0) {
// the view is partially selected
int
curPos = -1;
int
startPos = 0;
int
viewLen =
p1 -
p0;
while (
curPos++ <
viewLen) {
// searching for the next selection start
while(
curPos <
viewLen &&
selections[
curPos] == 0)
curPos++;
if (
startPos !=
curPos) {
// paint unselected text
paintTextUsingColor(
g,
a,
fg,
p0 +
startPos,
p0 +
curPos);
}
int
checkSum = 0;
// searching for next start position of unselected text
while (
curPos <
viewLen &&
(
checkSum +=
selections[
curPos]) != 0)
curPos++;
startPos =
curPos;
}
paintedText = true;
}
}
}
}
if(!
paintedText)
paintTextUsingColor(
g,
a,
fg,
p0,
p1);
}
/**
* Paints the specified region of text in the specified color.
*/
final void
paintTextUsingColor(
Graphics g,
Shape a,
Color c, int
p0, int
p1) {
// render the glyphs
g.
setColor(
c);
painter.
paint(this,
g,
a,
p0,
p1);
// render underline or strikethrough if set.
boolean
underline =
isUnderline();
boolean
strike =
isStrikeThrough();
if (
underline ||
strike) {
// calculate x coordinates
Rectangle alloc = (
a instanceof
Rectangle) ? (
Rectangle)
a :
a.
getBounds();
View parent =
getParent();
if ((
parent != null) && (
parent.
getEndOffset() ==
p1)) {
// strip whitespace on end
Segment s =
getText(
p0,
p1);
while (
Character.
isWhitespace(
s.
last())) {
p1 -= 1;
s.
count -= 1;
}
SegmentCache.
releaseSharedSegment(
s);
}
int
x0 =
alloc.
x;
int
p =
getStartOffset();
if (
p !=
p0) {
x0 += (int)
painter.
getSpan(this,
p,
p0,
getTabExpander(),
x0);
}
int
x1 =
x0 + (int)
painter.
getSpan(this,
p0,
p1,
getTabExpander(),
x0);
// calculate y coordinate
int
y =
alloc.
y + (int)(
painter.
getHeight(this) -
painter.
getDescent(this));
if (
underline) {
int
yTmp =
y + 1;
g.
drawLine(
x0,
yTmp,
x1,
yTmp);
}
if (
strike) {
// move y coordinate above baseline
int
yTmp =
y - (int) (
painter.
getAscent(this) * 0.3f);
g.
drawLine(
x0,
yTmp,
x1,
yTmp);
}
}
}
/**
* Determines the minimum span for this view along an axis.
*
* <p>This implementation returns the longest non-breakable area within
* the view as a minimum span for {@code View.X_AXIS}.</p>
*
* @param axis may be either {@code View.X_AXIS} or {@code View.Y_AXIS}
* @return the minimum span the view can be rendered into
* @throws IllegalArgumentException if the {@code axis} parameter is invalid
* @see javax.swing.text.View#getMinimumSpan
*/
@
Override
public float
getMinimumSpan(int
axis) {
switch (
axis) {
case
View.
X_AXIS:
if (
minimumSpan < 0) {
minimumSpan = 0;
int
p0 =
getStartOffset();
int
p1 =
getEndOffset();
while (
p1 >
p0) {
int
breakSpot =
getBreakSpot(
p0,
p1);
if (
breakSpot ==
BreakIterator.
DONE) {
// the rest of the view is non-breakable
breakSpot =
p0;
}
minimumSpan =
Math.
max(
minimumSpan,
getPartialSpan(
breakSpot,
p1));
// Note: getBreakSpot returns the *last* breakspot
p1 =
breakSpot - 1;
}
}
return
minimumSpan;
case
View.
Y_AXIS:
return super.getMinimumSpan(
axis);
default:
throw new
IllegalArgumentException("Invalid axis: " +
axis);
}
}
/**
* Determines the preferred span for this view along an
* axis.
*
* @param axis may be either View.X_AXIS or View.Y_AXIS
* @return the span the view would like to be rendered into >= 0.
* Typically the view is told to render into the span
* that is returned, although there is no guarantee.
* The parent may choose to resize or break the view.
*/
public float
getPreferredSpan(int
axis) {
if (
impliedCR) {
return 0;
}
checkPainter();
int
p0 =
getStartOffset();
int
p1 =
getEndOffset();
switch (
axis) {
case
View.
X_AXIS:
if (
skipWidth) {
return 0;
}
return
painter.
getSpan(this,
p0,
p1,
expander, this.
x);
case
View.
Y_AXIS:
float
h =
painter.
getHeight(this);
if (
isSuperscript()) {
h +=
h/3;
}
return
h;
default:
throw new
IllegalArgumentException("Invalid axis: " +
axis);
}
}
/**
* Determines the desired alignment for this view along an
* axis. For the label, the alignment is along the font
* baseline for the y axis, and the superclasses alignment
* along the x axis.
*
* @param axis may be either View.X_AXIS or View.Y_AXIS
* @return the desired alignment. This should be a value
* between 0.0 and 1.0 inclusive, where 0 indicates alignment at the
* origin and 1.0 indicates alignment to the full span
* away from the origin. An alignment of 0.5 would be the
* center of the view.
*/
public float
getAlignment(int
axis) {
checkPainter();
if (
axis ==
View.
Y_AXIS) {
boolean
sup =
isSuperscript();
boolean
sub =
isSubscript();
float
h =
painter.
getHeight(this);
float
d =
painter.
getDescent(this);
float
a =
painter.
getAscent(this);
float
align;
if (
sup) {
align = 1.0f;
} else if (
sub) {
align = (
h > 0) ? (
h - (
d + (
a / 2))) /
h : 0;
} else {
align = (
h > 0) ? (
h -
d) /
h : 0;
}
return
align;
}
return super.getAlignment(
axis);
}
/**
* Provides a mapping from the document model coordinate space
* to the coordinate space of the view mapped to it.
*
* @param pos the position to convert >= 0
* @param a the allocated region to render into
* @param b either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code>
* @return the bounding box of the given position
* @exception BadLocationException if the given position does not represent a
* valid location in the associated document
* @see View#modelToView
*/
public
Shape modelToView(int
pos,
Shape a,
Position.
Bias b) throws
BadLocationException {
checkPainter();
return
painter.
modelToView(this,
pos,
b,
a);
}
/**
* Provides a mapping from the view coordinate space to the logical
* coordinate space of the model.
*
* @param x the X coordinate >= 0
* @param y the Y coordinate >= 0
* @param a the allocated region to render into
* @param biasReturn either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code> is returned as the
* zero-th element of this array
* @return the location within the model that best represents the
* given point of view >= 0
* @see View#viewToModel
*/
public int
viewToModel(float
x, float
y,
Shape a,
Position.
Bias[]
biasReturn) {
checkPainter();
return
painter.
viewToModel(this,
x,
y,
a,
biasReturn);
}
/**
* Determines how attractive a break opportunity in
* this view is. This can be used for determining which
* view is the most attractive to call <code>breakView</code>
* on in the process of formatting. The
* higher the weight, the more attractive the break. A
* value equal to or lower than <code>View.BadBreakWeight</code>
* should not be considered for a break. A value greater
* than or equal to <code>View.ForcedBreakWeight</code> should
* be broken.
* <p>
* This is implemented to forward to the superclass for
* the Y_AXIS. Along the X_AXIS the following values
* may be returned.
* <dl>
* <dt><b>View.ExcellentBreakWeight</b>
* <dd>if there is whitespace proceeding the desired break
* location.
* <dt><b>View.BadBreakWeight</b>
* <dd>if the desired break location results in a break
* location of the starting offset.
* <dt><b>View.GoodBreakWeight</b>
* <dd>if the other conditions don't occur.
* </dl>
* This will normally result in the behavior of breaking
* on a whitespace location if one can be found, otherwise
* breaking between characters.
*
* @param axis may be either View.X_AXIS or View.Y_AXIS
* @param pos the potential location of the start of the
* broken view >= 0. This may be useful for calculating tab
* positions.
* @param len specifies the relative length from <em>pos</em>
* where a potential break is desired >= 0.
* @return the weight, which should be a value between
* View.ForcedBreakWeight and View.BadBreakWeight.
* @see LabelView
* @see ParagraphView
* @see View#BadBreakWeight
* @see View#GoodBreakWeight
* @see View#ExcellentBreakWeight
* @see View#ForcedBreakWeight
*/
public int
getBreakWeight(int
axis, float
pos, float
len) {
if (
axis ==
View.
X_AXIS) {
checkPainter();
int
p0 =
getStartOffset();
int
p1 =
painter.
getBoundedPosition(this,
p0,
pos,
len);
return
p1 ==
p0 ?
View.
BadBreakWeight :
getBreakSpot(
p0,
p1) !=
BreakIterator.
DONE ?
View.
ExcellentBreakWeight :
View.
GoodBreakWeight;
}
return super.getBreakWeight(
axis,
pos,
len);
}
/**
* Breaks this view on the given axis at the given length.
* This is implemented to attempt to break on a whitespace
* location, and returns a fragment with the whitespace at
* the end. If a whitespace location can't be found, the
* nearest character is used.
*
* @param axis may be either View.X_AXIS or View.Y_AXIS
* @param p0 the location in the model where the
* fragment should start it's representation >= 0.
* @param pos the position along the axis that the
* broken view would occupy >= 0. This may be useful for
* things like tab calculations.
* @param len specifies the distance along the axis
* where a potential break is desired >= 0.
* @return the fragment of the view that represents the
* given span, if the view can be broken. If the view
* doesn't support breaking behavior, the view itself is
* returned.
* @see View#breakView
*/
public
View breakView(int
axis, int
p0, float
pos, float
len) {
if (
axis ==
View.
X_AXIS) {
checkPainter();
int
p1 =
painter.
getBoundedPosition(this,
p0,
pos,
len);
int
breakSpot =
getBreakSpot(
p0,
p1);
if (
breakSpot != -1) {
p1 =
breakSpot;
}
// else, no break in the region, return a fragment of the
// bounded region.
if (
p0 ==
getStartOffset() &&
p1 ==
getEndOffset()) {
return this;
}
GlyphView v = (
GlyphView)
createFragment(
p0,
p1);
v.
x = (int)
pos;
return
v;
}
return this;
}
/**
* Returns a location to break at in the passed in region, or
* BreakIterator.DONE if there isn't a good location to break at
* in the specified region.
*/
private int
getBreakSpot(int
p0, int
p1) {
if (
breakSpots == null) {
// Re-calculate breakpoints for the whole view
int
start =
getStartOffset();
int
end =
getEndOffset();
int[]
bs = new int[
end + 1 -
start];
int
ix = 0;
// Breaker should work on the parent element because there may be
// a valid breakpoint at the end edge of the view (space, etc.)
Element parent =
getElement().
getParentElement();
int
pstart = (
parent == null ?
start :
parent.
getStartOffset());
int
pend = (
parent == null ?
end :
parent.
getEndOffset());
Segment s =
getText(
pstart,
pend);
s.
first();
BreakIterator breaker =
getBreaker();
breaker.
setText(
s);
// Backward search should start from end+1 unless there's NO end+1
int
startFrom =
end + (
pend >
end ? 1 : 0);
for (;;) {
startFrom =
breaker.
preceding(
s.
offset + (
startFrom -
pstart))
+ (
pstart -
s.
offset);
if (
startFrom >
start) {
// The break spot is within the view
bs[
ix++] =
startFrom;
} else {
break;
}
}
SegmentCache.
releaseSharedSegment(
s);
breakSpots = new int[
ix];
System.
arraycopy(
bs, 0,
breakSpots, 0,
ix);
}
int
breakSpot =
BreakIterator.
DONE;
for (int
i = 0;
i <
breakSpots.length;
i++) {
int
bsp =
breakSpots[
i];
if (
bsp <=
p1) {
if (
bsp >
p0) {
breakSpot =
bsp;
}
break;
}
}
return
breakSpot;
}
/**
* Return break iterator appropriate for the current document.
*
* For non-i18n documents a fast whitespace-based break iterator is used.
*/
private
BreakIterator getBreaker() {
Document doc =
getDocument();
if ((
doc != null) &&
Boolean.
TRUE.
equals(
doc.
getProperty(
AbstractDocument.
MultiByteProperty))) {
Container c =
getContainer();
Locale locale = (
c == null ?
Locale.
getDefault() :
c.
getLocale());
return
BreakIterator.
getLineInstance(
locale);
} else {
return new
WhitespaceBasedBreakIterator();
}
}
/**
* Creates a view that represents a portion of the element.
* This is potentially useful during formatting operations
* for taking measurements of fragments of the view. If
* the view doesn't support fragmenting (the default), it
* should return itself.
* <p>
* This view does support fragmenting. It is implemented
* to return a nested class that shares state in this view
* representing only a portion of the view.
*
* @param p0 the starting offset >= 0. This should be a value
* greater or equal to the element starting offset and
* less than the element ending offset.
* @param p1 the ending offset > p0. This should be a value
* less than or equal to the elements end offset and
* greater than the elements starting offset.
* @return the view fragment, or itself if the view doesn't
* support breaking into fragments
* @see LabelView
*/
public
View createFragment(int
p0, int
p1) {
checkPainter();
Element elem =
getElement();
GlyphView v = (
GlyphView)
clone();
v.
offset =
p0 -
elem.
getStartOffset();
v.
length =
p1 -
p0;
v.
painter =
painter.
getPainter(
v,
p0,
p1);
v.
justificationInfo = null;
return
v;
}
/**
* Provides a way to determine the next visually represented model
* location that one might place a caret. Some views may not be
* visible, they might not be in the same order found in the model, or
* they just might not allow access to some of the locations in the
* model.
* This method enables specifying a position to convert
* within the range of >=0. If the value is -1, a position
* will be calculated automatically. If the value < -1,
* the {@code BadLocationException} will be thrown.
*
* @param pos the position to convert
* @param a the allocated region to render into
* @param direction the direction from the current position that can
* be thought of as the arrow keys typically found on a keyboard.
* This may be SwingConstants.WEST, SwingConstants.EAST,
* SwingConstants.NORTH, or SwingConstants.SOUTH.
* @return the location within the model that best represents the next
* location visual position.
* @exception BadLocationException the given position is not a valid
* position within the document
* @exception IllegalArgumentException for an invalid direction
*/
public int
getNextVisualPositionFrom(int
pos,
Position.
Bias b,
Shape a,
int
direction,
Position.
Bias[]
biasRet)
throws
BadLocationException {
if (
pos < -1) {
throw new
BadLocationException("invalid position",
pos);
}
return
painter.
getNextVisualPositionFrom(this,
pos,
b,
a,
direction,
biasRet);
}
/**
* Gives notification that something was inserted into
* the document in a location that this view is responsible for.
* This is implemented to call preferenceChanged along the
* axis the glyphs are rendered.
*
* @param e the change information from the associated document
* @param a the current allocation of the view
* @param f the factory to use to rebuild if the view has children
* @see View#insertUpdate
*/
public void
insertUpdate(
DocumentEvent e,
Shape a,
ViewFactory f) {
justificationInfo = null;
breakSpots = null;
minimumSpan = -1;
syncCR();
preferenceChanged(null, true, false);
}
/**
* Gives notification that something was removed from the document
* in a location that this view is responsible for.
* This is implemented to call preferenceChanged along the
* axis the glyphs are rendered.
*
* @param e the change information from the associated document
* @param a the current allocation of the view
* @param f the factory to use to rebuild if the view has children
* @see View#removeUpdate
*/
public void
removeUpdate(
DocumentEvent e,
Shape a,
ViewFactory f) {
justificationInfo = null;
breakSpots = null;
minimumSpan = -1;
syncCR();
preferenceChanged(null, true, false);
}
/**
* Gives notification from the document that attributes were changed
* in a location that this view is responsible for.
* This is implemented to call preferenceChanged along both the
* horizontal and vertical axis.
*
* @param e the change information from the associated document
* @param a the current allocation of the view
* @param f the factory to use to rebuild if the view has children
* @see View#changedUpdate
*/
public void
changedUpdate(
DocumentEvent e,
Shape a,
ViewFactory f) {
minimumSpan = -1;
syncCR();
preferenceChanged(null, true, true);
}
// checks if the paragraph is empty and updates impliedCR flag
// accordingly
private void
syncCR() {
if (
impliedCR) {
Element parent =
getElement().
getParentElement();
impliedCR = (
parent != null &&
parent.
getElementCount() > 1);
}
}
/** {@inheritDoc} */
@
Override
void
updateAfterChange() {
// Drop the break spots. They will be re-calculated during
// layout. It is necessary for proper line break calculation.
breakSpots = null;
}
/**
* Class to hold data needed to justify this GlyphView in a PargraphView.Row
*/
static class
JustificationInfo {
//justifiable content start
final int
start;
//justifiable content end
final int
end;
final int
leadingSpaces;
final int
contentSpaces;
final int
trailingSpaces;
final boolean
hasTab;
final
BitSet spaceMap;
JustificationInfo(int
start, int
end,
int
leadingSpaces,
int
contentSpaces,
int
trailingSpaces,
boolean
hasTab,
BitSet spaceMap) {
this.
start =
start;
this.
end =
end;
this.
leadingSpaces =
leadingSpaces;
this.
contentSpaces =
contentSpaces;
this.
trailingSpaces =
trailingSpaces;
this.
hasTab =
hasTab;
this.
spaceMap =
spaceMap;
}
}
JustificationInfo getJustificationInfo(int
rowStartOffset) {
if (
justificationInfo != null) {
return
justificationInfo;
}
//states for the parsing
final int
TRAILING = 0;
final int
CONTENT = 1;
final int
SPACES = 2;
int
startOffset =
getStartOffset();
int
endOffset =
getEndOffset();
Segment segment =
getText(
startOffset,
endOffset);
int
txtOffset =
segment.
offset;
int
txtEnd =
segment.
offset +
segment.
count - 1;
int
startContentPosition =
txtEnd + 1;
int
endContentPosition =
txtOffset - 1;
int
lastTabPosition =
txtOffset - 1;
int
trailingSpaces = 0;
int
contentSpaces = 0;
int
leadingSpaces = 0;
boolean
hasTab = false;
BitSet spaceMap = new
BitSet(
endOffset -
startOffset + 1);
//we parse conent to the right of the rightmost TAB only.
//we are looking for the trailing and leading spaces.
//position after the leading spaces (startContentPosition)
//position before the trailing spaces (endContentPosition)
for (int
i =
txtEnd,
state =
TRAILING;
i >=
txtOffset;
i--) {
if (' ' ==
segment.
array[
i]) {
spaceMap.
set(
i -
txtOffset);
if (
state ==
TRAILING) {
trailingSpaces++;
} else if (
state ==
CONTENT) {
state =
SPACES;
leadingSpaces = 1;
} else if (
state ==
SPACES) {
leadingSpaces++;
}
} else if ('\t' ==
segment.
array[
i]) {
hasTab = true;
break;
} else {
if (
state ==
TRAILING) {
if ('\n' !=
segment.
array[
i]
&& '\r' !=
segment.
array[
i]) {
state =
CONTENT;
endContentPosition =
i;
}
} else if (
state ==
CONTENT) {
//do nothing
} else if (
state ==
SPACES) {
contentSpaces +=
leadingSpaces;
leadingSpaces = 0;
}
startContentPosition =
i;
}
}
SegmentCache.
releaseSharedSegment(
segment);
int
startJustifiableContent = -1;
if (
startContentPosition <
txtEnd) {
startJustifiableContent =
startContentPosition -
txtOffset;
}
int
endJustifiableContent = -1;
if (
endContentPosition >
txtOffset) {
endJustifiableContent =
endContentPosition -
txtOffset;
}
justificationInfo =
new
JustificationInfo(
startJustifiableContent,
endJustifiableContent,
leadingSpaces,
contentSpaces,
trailingSpaces,
hasTab,
spaceMap);
return
justificationInfo;
}
// --- variables ------------------------------------------------
/**
* Used by paint() to store highlighted view positions
*/
private byte[]
selections = null;
int
offset;
int
length;
// if it is an implied newline character
boolean
impliedCR;
boolean
skipWidth;
/**
* how to expand tabs
*/
TabExpander expander;
/** Cached minimum x-span value */
private float
minimumSpan = -1;
/** Cached breakpoints within the view */
private int[]
breakSpots = null;
/**
* location for determining tab expansion against.
*/
int
x;
/**
* Glyph rendering functionality.
*/
GlyphPainter painter;
/**
* The prototype painter used by default.
*/
static
GlyphPainter defaultPainter;
private
JustificationInfo justificationInfo = null;
/**
* A class to perform rendering of the glyphs.
* This can be implemented to be stateless, or
* to hold some information as a cache to
* facilitate faster rendering and model/view
* translation. At a minimum, the GlyphPainter
* allows a View implementation to perform its
* duties independant of a particular version
* of JVM and selection of capabilities (i.e.
* shaping for i18n, etc).
*
* @since 1.3
*/
public static abstract class
GlyphPainter {
/**
* Determine the span the glyphs given a start location
* (for tab expansion).
*/
public abstract float
getSpan(
GlyphView v, int
p0, int
p1,
TabExpander e, float
x);
public abstract float
getHeight(
GlyphView v);
public abstract float
getAscent(
GlyphView v);
public abstract float
getDescent(
GlyphView v);
/**
* Paint the glyphs representing the given range.
*/
public abstract void
paint(
GlyphView v,
Graphics g,
Shape a, int
p0, int
p1);
/**
* Provides a mapping from the document model coordinate space
* to the coordinate space of the view mapped to it.
* This is shared by the broken views.
*
* @param v the <code>GlyphView</code> containing the
* destination coordinate space
* @param pos the position to convert
* @param bias either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code>
* @param a Bounds of the View
* @return the bounding box of the given position
* @exception BadLocationException if the given position does not represent a
* valid location in the associated document
* @see View#modelToView
*/
public abstract
Shape modelToView(
GlyphView v,
int
pos,
Position.
Bias bias,
Shape a) throws
BadLocationException;
/**
* Provides a mapping from the view coordinate space to the logical
* coordinate space of the model.
*
* @param v the <code>GlyphView</code> to provide a mapping for
* @param x the X coordinate
* @param y the Y coordinate
* @param a the allocated region to render into
* @param biasReturn either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code>
* is returned as the zero-th element of this array
* @return the location within the model that best represents the
* given point of view
* @see View#viewToModel
*/
public abstract int
viewToModel(
GlyphView v,
float
x, float
y,
Shape a,
Position.
Bias[]
biasReturn);
/**
* Determines the model location that represents the
* maximum advance that fits within the given span.
* This could be used to break the given view. The result
* should be a location just shy of the given advance. This
* differs from viewToModel which returns the closest
* position which might be proud of the maximum advance.
*
* @param v the view to find the model location to break at.
* @param p0 the location in the model where the
* fragment should start it's representation >= 0.
* @param x the graphic location along the axis that the
* broken view would occupy >= 0. This may be useful for
* things like tab calculations.
* @param len specifies the distance into the view
* where a potential break is desired >= 0.
* @return the maximum model location possible for a break.
* @see View#breakView
*/
public abstract int
getBoundedPosition(
GlyphView v, int
p0, float
x, float
len);
/**
* Create a painter to use for the given GlyphView. If
* the painter carries state it can create another painter
* to represent a new GlyphView that is being created. If
* the painter doesn't hold any significant state, it can
* return itself. The default behavior is to return itself.
* @param v the <code>GlyphView</code> to provide a painter for
* @param p0 the starting document offset >= 0
* @param p1 the ending document offset >= p0
*/
public
GlyphPainter getPainter(
GlyphView v, int
p0, int
p1) {
return this;
}
/**
* Provides a way to determine the next visually represented model
* location that one might place a caret. Some views may not be
* visible, they might not be in the same order found in the model, or
* they just might not allow access to some of the locations in the
* model.
*
* @param v the view to use
* @param pos the position to convert >= 0
* @param b either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code>
* @param a the allocated region to render into
* @param direction the direction from the current position that can
* be thought of as the arrow keys typically found on a keyboard.
* This may be SwingConstants.WEST, SwingConstants.EAST,
* SwingConstants.NORTH, or SwingConstants.SOUTH.
* @param biasRet either <code>Position.Bias.Forward</code>
* or <code>Position.Bias.Backward</code>
* is returned as the zero-th element of this array
* @return the location within the model that best represents the next
* location visual position.
* @exception BadLocationException
* @exception IllegalArgumentException for an invalid direction
*/
public int
getNextVisualPositionFrom(
GlyphView v, int
pos,
Position.
Bias b,
Shape a,
int
direction,
Position.
Bias[]
biasRet)
throws
BadLocationException {
int
startOffset =
v.
getStartOffset();
int
endOffset =
v.
getEndOffset();
Segment text;
switch (
direction) {
case
View.
NORTH:
case
View.
SOUTH:
if (
pos != -1) {
// Presumably pos is between startOffset and endOffset,
// since GlyphView is only one line, we won't contain
// the position to the nort/south, therefore return -1.
return -1;
}
Container container =
v.
getContainer();
if (
container instanceof
JTextComponent) {
Caret c = ((
JTextComponent)
container).
getCaret();
Point magicPoint;
magicPoint = (
c != null) ?
c.
getMagicCaretPosition() :null;
if (
magicPoint == null) {
biasRet[0] =
Position.
Bias.
Forward;
return
startOffset;
}
int
value =
v.
viewToModel(
magicPoint.
x, 0f,
a,
biasRet);
return
value;
}
break;
case
View.
EAST:
if(
startOffset ==
v.
getDocument().
getLength()) {
if(
pos == -1) {
biasRet[0] =
Position.
Bias.
Forward;
return
startOffset;
}
// End case for bidi text where newline is at beginning
// of line.
return -1;
}
if(
pos == -1) {
biasRet[0] =
Position.
Bias.
Forward;
return
startOffset;
}
if(
pos ==
endOffset) {
return -1;
}
if(++
pos ==
endOffset) {
// Assumed not used in bidi text, GlyphPainter2 will
// override as necessary, therefore return -1.
return -1;
}
else {
biasRet[0] =
Position.
Bias.
Forward;
}
return
pos;
case
View.
WEST:
if(
startOffset ==
v.
getDocument().
getLength()) {
if(
pos == -1) {
biasRet[0] =
Position.
Bias.
Forward;
return
startOffset;
}
// End case for bidi text where newline is at beginning
// of line.
return -1;
}
if(
pos == -1) {
// Assumed not used in bidi text, GlyphPainter2 will
// override as necessary, therefore return -1.
biasRet[0] =
Position.
Bias.
Forward;
return
endOffset - 1;
}
if(
pos ==
startOffset) {
return -1;
}
biasRet[0] =
Position.
Bias.
Forward;
return (
pos - 1);
default:
throw new
IllegalArgumentException("Bad direction: " +
direction);
}
return
pos;
}
}
}