blob: 91b0869b89cd02d3a115eccc1a443481f07b8423 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*/
package org.apache.poi.xslf.usermodel;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.poi.POIXMLException;
import org.apache.poi.sl.draw.DrawFactory;
import org.apache.poi.sl.draw.DrawTextShape;
import org.apache.poi.sl.usermodel.Insets2D;
import org.apache.poi.sl.usermodel.Placeholder;
import org.apache.poi.sl.usermodel.TextShape;
import org.apache.poi.sl.usermodel.VerticalAlignment;
import org.apache.poi.util.Beta;
import org.apache.poi.util.Units;
import org.apache.poi.xslf.model.PropertyFetcher;
import org.apache.poi.xslf.model.TextBodyPropertyFetcher;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBody;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBodyProperties;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties;
import org.openxmlformats.schemas.drawingml.x2006.main.STTextAnchoringType;
import org.openxmlformats.schemas.drawingml.x2006.main.STTextVerticalType;
import org.openxmlformats.schemas.drawingml.x2006.main.STTextWrappingType;
import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder;
/**
* Represents a shape that can hold text.
*/
@Beta
public abstract class XSLFTextShape extends XSLFSimpleShape
implements TextShape<XSLFShape,XSLFTextParagraph> {
private final List<XSLFTextParagraph> _paragraphs;
/*package*/ XSLFTextShape(XmlObject shape, XSLFSheet sheet) {
super(shape, sheet);
_paragraphs = new ArrayList<XSLFTextParagraph>();
CTTextBody txBody = getTextBody(false);
if (txBody != null) {
for (CTTextParagraph p : txBody.getPArray()) {
_paragraphs.add(newTextParagraph(p));
}
}
}
public Iterator<XSLFTextParagraph> iterator(){
return getTextParagraphs().iterator();
}
@Override
public String getText() {
StringBuilder out = new StringBuilder();
for (XSLFTextParagraph p : _paragraphs) {
if (out.length() > 0) out.append('\n');
out.append(p.getText());
}
return out.toString();
}
/**
* unset text from this shape
*/
public void clearText(){
_paragraphs.clear();
CTTextBody txBody = getTextBody(true);
txBody.setPArray(null); // remove any existing paragraphs
}
@Override
public XSLFTextRun setText(String text) {
// calling clearText or setting to a new Array leads to a XmlValueDisconnectedException
if (!_paragraphs.isEmpty()) {
CTTextBody txBody = getTextBody(false);
int cntPs = txBody.sizeOfPArray();
for (int i = cntPs; i > 1; i--) {
txBody.removeP(i-1);
_paragraphs.remove(i-1);
}
_paragraphs.get(0).clearButKeepProperties();
}
return appendText(text, false);
}
@Override
public XSLFTextRun appendText(String text, boolean newParagraph) {
if (text == null) return null;
// copy properties from last paragraph / textrun or paragraph end marker
CTTextParagraphProperties otherPPr = null;
CTTextCharacterProperties otherRPr = null;
boolean firstPara;
XSLFTextParagraph para;
if (_paragraphs.isEmpty()) {
firstPara = false;
para = null;
} else {
firstPara = !newParagraph;
para = _paragraphs.get(_paragraphs.size()-1);
CTTextParagraph ctp = para.getXmlObject();
otherPPr = ctp.getPPr();
List<XSLFTextRun> runs = para.getTextRuns();
if (!runs.isEmpty()) {
XSLFTextRun r0 = runs.get(runs.size()-1);
otherRPr = r0.getRPr(false);
if (otherRPr == null) {
otherRPr = ctp.getEndParaRPr();
}
}
// don't copy endParaRPr to the run in case there aren't any other runs
// this is the case when setText() was called initially
// otherwise the master style will be overridden/ignored
}
XSLFTextRun run = null;
for (String lineTxt : text.split("\\r\\n?|\\n")) {
if (!firstPara) {
if (para != null) {
CTTextParagraph ctp = para.getXmlObject();
CTTextCharacterProperties unexpectedRPr = ctp.getEndParaRPr();
if (unexpectedRPr != null && unexpectedRPr != otherRPr) {
ctp.unsetEndParaRPr();
}
}
para = addNewTextParagraph();
if (otherPPr != null) {
para.getXmlObject().setPPr(otherPPr);
}
}
boolean firstRun = true;
for (String runText : lineTxt.split("[\u000b]")) {
if (!firstRun) {
para.addLineBreak();
}
run = para.addNewTextRun();
run.setText(runText);
if (otherRPr != null) {
run.getRPr(true).set(otherRPr);
}
firstRun = false;
}
firstPara = false;
}
assert(run != null);
return run;
}
@Override
public List<XSLFTextParagraph> getTextParagraphs() {
return _paragraphs;
}
/**
* add a new paragraph run to this shape
*
* @return created paragraph run
*/
public XSLFTextParagraph addNewTextParagraph() {
CTTextBody txBody = getTextBody(false);
CTTextParagraph p;
if (txBody == null) {
txBody = getTextBody(true);
p = txBody.getPArray(0);
p.removeR(0);
} else {
p = txBody.addNewP();
}
XSLFTextParagraph paragraph = newTextParagraph(p);
_paragraphs.add(paragraph);
return paragraph;
}
@Override
public void setVerticalAlignment(VerticalAlignment anchor){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(anchor == null) {
if(bodyPr.isSetAnchor()) bodyPr.unsetAnchor();
} else {
bodyPr.setAnchor(STTextAnchoringType.Enum.forInt(anchor.ordinal() + 1));
}
}
}
@Override
public VerticalAlignment getVerticalAlignment(){
PropertyFetcher<VerticalAlignment> fetcher = new TextBodyPropertyFetcher<VerticalAlignment>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetAnchor()){
int val = props.getAnchor().intValue();
setValue(VerticalAlignment.values()[val - 1]);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
return fetcher.getValue() == null ? VerticalAlignment.TOP : fetcher.getValue();
}
@Override
public void setHorizontalCentered(Boolean isCentered){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if (isCentered == null) {
if (bodyPr.isSetAnchorCtr()) bodyPr.unsetAnchorCtr();
} else {
bodyPr.setAnchorCtr(isCentered);
}
}
}
@Override
public boolean isHorizontalCentered(){
PropertyFetcher<Boolean> fetcher = new TextBodyPropertyFetcher<Boolean>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetAnchorCtr()){
setValue(props.getAnchorCtr());
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
return fetcher.getValue() == null ? false : fetcher.getValue();
}
@Override
public void setTextDirection(TextDirection orientation){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(orientation == null) {
if(bodyPr.isSetVert()) bodyPr.unsetVert();
} else {
bodyPr.setVert(STTextVerticalType.Enum.forInt(orientation.ordinal() + 1));
}
}
}
@Override
public TextDirection getTextDirection(){
CTTextBodyProperties bodyPr = getTextBodyPr();
if (bodyPr != null) {
STTextVerticalType.Enum val = bodyPr.getVert();
if(val != null) {
switch (val.intValue()) {
default:
case STTextVerticalType.INT_HORZ:
return TextDirection.HORIZONTAL;
case STTextVerticalType.INT_EA_VERT:
case STTextVerticalType.INT_MONGOLIAN_VERT:
case STTextVerticalType.INT_VERT:
return TextDirection.VERTICAL;
case STTextVerticalType.INT_VERT_270:
return TextDirection.VERTICAL_270;
case STTextVerticalType.INT_WORD_ART_VERT_RTL:
case STTextVerticalType.INT_WORD_ART_VERT:
return TextDirection.STACKED;
}
}
}
return TextDirection.HORIZONTAL;
}
@Override
public Double getTextRotation() {
CTTextBodyProperties bodyPr = getTextBodyPr();
if (bodyPr != null && bodyPr.isSetRot()) {
return bodyPr.getRot() / 60000.;
}
return null;
}
@Override
public void setTextRotation(Double rotation) {
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
bodyPr.setRot((int)(rotation * 60000.));
}
}
/**
* Returns the distance (in points) between the bottom of the text frame
* and the bottom of the inscribed rectangle of the shape that contains the text.
*
* @return the bottom inset in points
*/
public double getBottomInset(){
PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetBIns()){
double val = Units.toPoints(props.getBIns());
setValue(val);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
// If this attribute is omitted, then a value of 0.05 inches is implied
return fetcher.getValue() == null ? 3.6 : fetcher.getValue();
}
/**
* Returns the distance (in points) between the left edge of the text frame
* and the left edge of the inscribed rectangle of the shape that contains
* the text.
*
* @return the left inset in points
*/
public double getLeftInset(){
PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetLIns()){
double val = Units.toPoints(props.getLIns());
setValue(val);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
// If this attribute is omitted, then a value of 0.1 inches is implied
return fetcher.getValue() == null ? 7.2 : fetcher.getValue();
}
/**
* Returns the distance (in points) between the right edge of the
* text frame and the right edge of the inscribed rectangle of the shape
* that contains the text.
*
* @return the right inset in points
*/
public double getRightInset(){
PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetRIns()){
double val = Units.toPoints(props.getRIns());
setValue(val);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
// If this attribute is omitted, then a value of 0.1 inches is implied
return fetcher.getValue() == null ? 7.2 : fetcher.getValue();
}
/**
* Returns the distance (in points) between the top of the text frame
* and the top of the inscribed rectangle of the shape that contains the text.
*
* @return the top inset in points
*/
public double getTopInset(){
PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetTIns()){
double val = Units.toPoints(props.getTIns());
setValue(val);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
// If this attribute is omitted, then a value of 0.05 inches is implied
return fetcher.getValue() == null ? 3.6 : fetcher.getValue();
}
/**
* Sets the bottom margin.
* @see #getBottomInset()
*
* @param margin the bottom margin
*/
public void setBottomInset(double margin){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(margin == -1) bodyPr.unsetBIns();
else bodyPr.setBIns(Units.toEMU(margin));
}
}
/**
* Sets the left margin.
* @see #getLeftInset()
*
* @param margin the left margin
*/
public void setLeftInset(double margin){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(margin == -1) bodyPr.unsetLIns();
else bodyPr.setLIns(Units.toEMU(margin));
}
}
/**
* Sets the right margin.
* @see #getRightInset()
*
* @param margin the right margin
*/
public void setRightInset(double margin){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(margin == -1) bodyPr.unsetRIns();
else bodyPr.setRIns(Units.toEMU(margin));
}
}
/**
* Sets the top margin.
* @see #getTopInset()
*
* @param margin the top margin
*/
public void setTopInset(double margin){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(margin == -1) bodyPr.unsetTIns();
else bodyPr.setTIns(Units.toEMU(margin));
}
}
@Override
public Insets2D getInsets() {
Insets2D insets = new Insets2D(getTopInset(), getLeftInset(), getBottomInset(), getRightInset());
return insets;
}
@Override
public void setInsets(Insets2D insets) {
setTopInset(insets.top);
setLeftInset(insets.left);
setBottomInset(insets.bottom);
setRightInset(insets.right);
}
@Override
public boolean getWordWrap(){
PropertyFetcher<Boolean> fetcher = new TextBodyPropertyFetcher<Boolean>(){
public boolean fetch(CTTextBodyProperties props){
if(props.isSetWrap()){
setValue(props.getWrap() == STTextWrappingType.SQUARE);
return true;
}
return false;
}
};
fetchShapeProperty(fetcher);
return fetcher.getValue() == null ? true : fetcher.getValue();
}
@Override
public void setWordWrap(boolean wrap){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
bodyPr.setWrap(wrap ? STTextWrappingType.SQUARE : STTextWrappingType.NONE);
}
}
/**
*
* Specifies that a shape should be auto-fit to fully contain the text described within it.
* Auto-fitting is when text within a shape is scaled in order to contain all the text inside
*
* @param value type of autofit
*/
public void setTextAutofit(TextAutofit value){
CTTextBodyProperties bodyPr = getTextBodyPr(true);
if (bodyPr != null) {
if(bodyPr.isSetSpAutoFit()) bodyPr.unsetSpAutoFit();
if(bodyPr.isSetNoAutofit()) bodyPr.unsetNoAutofit();
if(bodyPr.isSetNormAutofit()) bodyPr.unsetNormAutofit();
switch(value){
case NONE: bodyPr.addNewNoAutofit(); break;
case NORMAL: bodyPr.addNewNormAutofit(); break;
case SHAPE: bodyPr.addNewSpAutoFit(); break;
}
}
}
/**
*
* @return type of autofit
*/
public TextAutofit getTextAutofit(){
CTTextBodyProperties bodyPr = getTextBodyPr();
if (bodyPr != null) {
if(bodyPr.isSetNoAutofit()) return TextAutofit.NONE;
else if (bodyPr.isSetNormAutofit()) return TextAutofit.NORMAL;
else if (bodyPr.isSetSpAutoFit()) return TextAutofit.SHAPE;
}
return TextAutofit.NORMAL;
}
protected CTTextBodyProperties getTextBodyPr(){
return getTextBodyPr(false);
}
protected CTTextBodyProperties getTextBodyPr(boolean create) {
CTTextBody textBody = getTextBody(create);
if (textBody == null) {
return null;
}
CTTextBodyProperties textBodyPr = textBody.getBodyPr();
if (textBodyPr == null && create) {
textBodyPr = textBody.addNewBodyPr();
}
return textBodyPr;
}
protected abstract CTTextBody getTextBody(boolean create);
@Override
public void setPlaceholder(Placeholder placeholder) {
super.setPlaceholder(placeholder);
}
public Placeholder getTextType(){
CTPlaceholder ph = getCTPlaceholder();
if (ph == null) return null;
int val = ph.getType().intValue();
return Placeholder.lookupOoxml(val);
}
@Override
public double getTextHeight(){
DrawFactory drawFact = DrawFactory.getInstance(null);
DrawTextShape dts = drawFact.getDrawable(this);
return dts.getTextHeight();
}
/**
* Adjust the size of the shape so it encompasses the text inside it.
*
* @return a <code>Rectangle2D</code> that is the bounds of this shape.
*/
public Rectangle2D resizeToFitText(){
Rectangle2D anchor = getAnchor();
if(anchor.getWidth() == 0.) throw new POIXMLException(
"Anchor of the shape was not set.");
double height = getTextHeight();
height += 1; // add a pixel to compensate rounding errors
anchor.setRect(anchor.getX(), anchor.getY(), anchor.getWidth(), height);
setAnchor(anchor);
return anchor;
}
@Override
void copy(XSLFShape other){
super.copy(other);
XSLFTextShape otherTS = (XSLFTextShape)other;
CTTextBody otherTB = otherTS.getTextBody(false);
CTTextBody thisTB = getTextBody(true);
if (otherTB == null) {
return;
}
thisTB.setBodyPr((CTTextBodyProperties)otherTB.getBodyPr().copy());
if (thisTB.isSetLstStyle()) thisTB.unsetLstStyle();
if (otherTB.isSetLstStyle()) {
thisTB.setLstStyle((CTTextListStyle)otherTB.getLstStyle().copy());
}
boolean srcWordWrap = otherTS.getWordWrap();
if(srcWordWrap != getWordWrap()){
setWordWrap(srcWordWrap);
}
double leftInset = otherTS.getLeftInset();
if(leftInset != getLeftInset()) {
setLeftInset(leftInset);
}
double rightInset = otherTS.getRightInset();
if(rightInset != getRightInset()) {
setRightInset(rightInset);
}
double topInset = otherTS.getTopInset();
if(topInset != getTopInset()) {
setTopInset(topInset);
}
double bottomInset = otherTS.getBottomInset();
if(bottomInset != getBottomInset()) {
setBottomInset(bottomInset);
}
VerticalAlignment vAlign = otherTS.getVerticalAlignment();
if(vAlign != getVerticalAlignment()) {
setVerticalAlignment(vAlign);
}
clearText();
for (XSLFTextParagraph srcP : otherTS.getTextParagraphs()) {
XSLFTextParagraph tgtP = addNewTextParagraph();
tgtP.copy(srcP);
}
}
@Override
public void setTextPlaceholder(TextPlaceholder placeholder) {
switch (placeholder) {
default:
case NOTES:
case HALF_BODY:
case QUARTER_BODY:
case BODY:
setPlaceholder(Placeholder.BODY);
break;
case TITLE:
setPlaceholder(Placeholder.TITLE);
break;
case CENTER_BODY:
setPlaceholder(Placeholder.BODY);
setHorizontalCentered(true);
break;
case CENTER_TITLE:
setPlaceholder(Placeholder.CENTERED_TITLE);
break;
case OTHER:
setPlaceholder(Placeholder.CONTENT);
break;
}
}
@Override
public TextPlaceholder getTextPlaceholder() {
Placeholder ph = getTextType();
if (ph == null) return TextPlaceholder.BODY;
switch (ph) {
case BODY: return TextPlaceholder.BODY;
case TITLE: return TextPlaceholder.TITLE;
case CENTERED_TITLE: return TextPlaceholder.CENTER_TITLE;
default:
case CONTENT: return TextPlaceholder.OTHER;
}
}
/**
* Helper method to allow subclasses to provide their own text paragraph
*
* @param p the xml reference
*
* @return a new text paragraph
*
* @since POI 3.15-beta2
*/
protected XSLFTextParagraph newTextParagraph(CTTextParagraph p) {
return new XSLFTextParagraph(p, this);
}
}