/*******************************************************************************
* Copyright (c) 2011, 2012 Red Hat, Inc.
* All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*
* @author Ivar Meikas
******************************************************************************/
package org.eclipse.bpmn2.modeler.core.features.label;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.bpmn2.BaseElement;
import org.eclipse.bpmn2.DataState;
import org.eclipse.bpmn2.ItemAwareElement;
import org.eclipse.bpmn2.di.BPMNDiagram;
import org.eclipse.bpmn2.di.BPMNEdge;
import org.eclipse.bpmn2.di.BPMNLabel;
import org.eclipse.bpmn2.di.BPMNShape;
import org.eclipse.bpmn2.modeler.core.di.DIUtils;
import org.eclipse.bpmn2.modeler.core.features.AbstractBpmn2UpdateFeature;
import org.eclipse.bpmn2.modeler.core.features.GraphitiConstants;
import org.eclipse.bpmn2.modeler.core.preferences.ShapeStyle;
import org.eclipse.bpmn2.modeler.core.preferences.ShapeStyle.LabelPosition;
import org.eclipse.bpmn2.modeler.core.utils.BusinessObjectUtil;
import org.eclipse.bpmn2.modeler.core.utils.FeatureSupport;
import org.eclipse.bpmn2.modeler.core.utils.GraphicsUtil;
import org.eclipse.bpmn2.modeler.core.utils.ModelUtil;
import org.eclipse.dd.dc.Bounds;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.graphiti.datatypes.IDimension;
import org.eclipse.graphiti.datatypes.ILocation;
import org.eclipse.graphiti.features.IFeatureProvider;
import org.eclipse.graphiti.features.IReason;
import org.eclipse.graphiti.features.context.IContext;
import org.eclipse.graphiti.features.context.IUpdateContext;
import org.eclipse.graphiti.features.impl.Reason;
import org.eclipse.graphiti.mm.algorithms.AbstractText;
import org.eclipse.graphiti.mm.algorithms.GraphicsAlgorithm;
import org.eclipse.graphiti.mm.algorithms.styles.Font;
import org.eclipse.graphiti.mm.algorithms.styles.Point;
import org.eclipse.graphiti.mm.pictograms.Connection;
import org.eclipse.graphiti.mm.pictograms.ContainerShape;
import org.eclipse.graphiti.mm.pictograms.PictogramElement;
import org.eclipse.graphiti.mm.pictograms.Shape;
import org.eclipse.graphiti.services.Graphiti;
import org.eclipse.graphiti.ui.services.GraphitiUi;
public class UpdateLabelFeature extends AbstractBpmn2UpdateFeature {
public UpdateLabelFeature(IFeatureProvider fp) {
super(fp);
}
@Override
public boolean canUpdate(IUpdateContext context) {
Object bo = getBusinessObjectForPictogramElement(context.getPictogramElement());
if (bo instanceof BaseElement) {
return hasLabel((BaseElement)bo);
}
return false;
}
@Override
public IReason updateNeeded(IUpdateContext context) {
IReason reason = super.updateNeeded(context);
if (reason.toBoolean())
return reason;
PictogramElement ownerPE = FeatureSupport.getLabelOwner(context);
BaseElement element = (BaseElement) BusinessObjectUtil.getFirstElementOfType(ownerPE, BaseElement.class);
Shape labelShape = FeatureSupport.getLabelShape(ownerPE);
if (labelShape != null) {
if (FeatureSupport.getPropertyValue(labelShape, GraphitiConstants.LABEL_CHANGED) != null) {
return Reason.createTrueReason(Messages.UpdateLabelFeature_LabelChanged);
}
String newLabel = getLabelString(element);
if (newLabel == null || newLabel.isEmpty())
newLabel = ""; //$NON-NLS-1$
AbstractText text = (AbstractText) labelShape.getGraphicsAlgorithm();
String oldLabel = text.getValue();
if (oldLabel == null || oldLabel.isEmpty())
oldLabel = ""; //$NON-NLS-1$
if (!newLabel.equals(oldLabel))
return Reason.createTrueReason(Messages.UpdateLabelFeature_TextChanged);
// Workaround for Bug 440796
GraphicsAlgorithm labelGA = labelShape.getGraphicsAlgorithm();
int x = labelGA.getX();
if (x==0 || x==-90) {
Rectangle bounds = getLabelBounds(labelShape, false, null);
labelGA.eSetDeliver(false);
labelGA.setX(bounds.x);
labelGA.eSetDeliver(true);
}
}
return Reason.createFalseReason();
}
@Override
public boolean update(IUpdateContext context) {
PictogramElement pe = FeatureSupport.getLabelOwner(context);
Point offset = (Point) context.getProperty(GraphitiConstants.LABEL_OFFSET);
boolean isAdding = isAddingLabel(context);
adjustLabelLocation(pe, isAdding, offset);
return true;
}
protected boolean isAddingLabel(IContext context) {
return context.getProperty(GraphitiConstants.PICTOGRAM_ELEMENTS) != null
|| context.getProperty(GraphitiConstants.PICTOGRAM_ELEMENT) != null
|| context.getProperty(GraphitiConstants.IMPORT_PROPERTY) != null;
}
protected boolean hasLabel(BaseElement element) {
return ModelUtil.hasName(element);
}
protected String getLabelString(BaseElement element) {
/*
* Unfortunately this needs to be aware of ItemAwareElements, which have
* a Data State (the Data State needs to appear below the element's
* label in []) The UpdateLabelFeature is checked in
* BPMN2FeatureProvider AFTER the Update Feature for Data Objects is
* executed - this wipes out the Label provided by
* ItemAwareElementUpdateFeature.
*/
String label = ModelUtil.getName(element);
if (element instanceof ItemAwareElement) {
DataState state = ((ItemAwareElement) element).getDataState();
if (state != null && state.getName() != null) {
return label + "\n[" + state.getName() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
}
return label;
}
protected int getLabelWrapWidth(PictogramElement ownerPE) {
int w = GraphicsUtil.calculateSize(ownerPE).getWidth();
return w>=80 ? w : 80;
}
protected int[] wrapText(AbstractText ga, String text, int wrapWidth) {
Font font = ga.getFont();
// If the text is camel case, break on the lower to upper case change.
String normalizedText = ""; //$NON-NLS-1$
boolean first = true;
char[] chars = text.toCharArray();
for (int i=0; i<chars.length; ++i) {
char c = chars[i];
if (Character.isUpperCase(c)) {
if (normalizedText.length()>0 && i+1<chars.length && !Character.isUpperCase(chars[i+1]))
normalizedText += " "; //$NON-NLS-1$
}
if (first) {
c = Character.toUpperCase(c);
first = false;
}
if (!Character.isLetterOrDigit(c))
c = ' ';
if (c==' ')
first = true;
normalizedText += c;
}
List<String> ss = new ArrayList<String>();
int start = 0;
for (int end=0; end<normalizedText.length(); ++end) {
char c = normalizedText.charAt(end);
if (c==' ') {
ss.add(normalizedText.substring(start, end+1));
start = end+1;
}
else if (c=='\n') {
ss.add(normalizedText.substring(start, end+1));
start = end+1;
}
}
if (start<normalizedText.length())
ss.add(normalizedText.substring(start));
String words[] = ss.toArray(new String[ss.size()]);
IDimension dim = calculateTextSize(normalizedText, font);
int totalHeight = dim.getHeight();
int totalWidth = dim.getWidth();
int height = totalHeight;
int width = 0;
String line = ""; //$NON-NLS-1$
String nextword = ""; //$NON-NLS-1$
for (int i=0; i<words.length; ++i) {
line += words[i];
if (i<words.length-1)
nextword = words[i+1];
else
nextword = ""; //$NON-NLS-1$
dim = calculateTextSize(line + nextword, font);
if (dim.getWidth()>wrapWidth) {
height += dim.getHeight();
dim = calculateTextSize(line, font);
if (dim.getWidth()>width)
width = dim.getWidth();
line = ""; //$NON-NLS-1$
}
else if (dim.getWidth()>width)
width = dim.getWidth();
}
if (width==0)
width = totalWidth;
return new int[] {height, width};
}
private IDimension calculateTextSize(String text, Font font) {
IDimension dim = GraphitiUi.getUiLayoutService().calculateTextSize(text, font);
if (text.endsWith("\n")) //$NON-NLS-1$
dim.setHeight(2*dim.getHeight());
return dim;
}
protected Rectangle getLabelBounds(PictogramElement pe, boolean isAddingLabel, Point offset) {
PictogramElement ownerPE = FeatureSupport.getLabelOwner(pe);
Shape labelShape = FeatureSupport.getLabelShape(pe);
if (labelShape != null) {
AbstractText labelGA = (AbstractText) labelShape.getGraphicsAlgorithm();
BaseElement element = (BaseElement) BusinessObjectUtil.getFirstElementOfType(ownerPE, BaseElement.class);
String text = getLabelString(element);
if (text == null) {
text = ""; //$NON-NLS-1$
}
// Get the absolute location of the owner. If the owner is a
// Connection use the Connection midpoint.
ILocation ownerLoc = ownerPE instanceof Connection ? Graphiti.getPeLayoutService().getConnectionMidpoint(
(Connection) ownerPE, 0.5) : Graphiti.getPeService().getLocationRelativeToDiagram((Shape) ownerPE);
IDimension ownerSize = GraphicsUtil.calculateSize(ownerPE);
ILocation labelLoc = Graphiti.getPeService().getLocationRelativeToDiagram(labelShape);
int x = 0;
int y = 0;
int w = getLabelWidth(labelGA);
int h = getLabelHeight(labelGA);
int wrapWidth = getLabelWrapWidth(ownerPE);
if (wrapWidth>0 && w > wrapWidth) {
int hw[] = wrapText(labelGA, text, wrapWidth);
h = hw[0];
w = hw[1];
}
LabelPosition hpos = getHorizontalLabelPosition(labelGA);
LabelPosition vpos = getVerticalLabelPosition(labelGA);
if (isAddingLabel) {
BPMNDiagram bpmnDiagram = DIUtils.findBPMNDiagram(ownerPE);
BPMNLabel bpmnLabel = null;
if (ownerPE instanceof Connection) {
BPMNEdge bpmnEdge = DIUtils.findBPMNEdge(bpmnDiagram, element);
if (bpmnEdge!=null)
bpmnLabel = bpmnEdge.getLabel();
} else {
BPMNShape bpmnShape = DIUtils.findBPMNShape(bpmnDiagram, element);
if (bpmnShape!=null)
bpmnLabel = bpmnShape.getLabel();
}
Bounds bounds = bpmnLabel == null ? null : bpmnLabel.getBounds();
if (bounds == null) {
/*
* The edge or shape does not have a BPMNLabel so treat the
* label normally, that is adjust its location according to
* the User Preferences. In this case force the relative
* location of the label to be below the shape or connection
* in case User Preferences allow labels to be moved
* manually.
*/
isAddingLabel = false;
if (hpos == LabelPosition.MOVABLE) {
vpos = hpos = LabelPosition.SOUTH;
}
} else {
int bw = (int) bounds.getWidth();
int bh = (int) bounds.getHeight();
/*
* The size provided in BPMNLabel for this Label shape is
* not sufficient to hold all of the text at the selected
* Font. Recalculate the Label bounds using the selected
* preferences.
*/
if (bw < w || bh < h) {
isAddingLabel = false;
}
else {
x = (int) bounds.getX();
y = (int) bounds.getY();
w = bw;
h = bh;
}
}
}
if (!isAddingLabel && !text.isEmpty()) {
// calculate X coordinate
switch (hpos) {
case NORTH:
case SOUTH:
case TOP:
case CENTER:
case BOTTOM:
// X coordinate for these positions are all the same
x = ownerLoc.getX() + (ownerSize.getWidth() - w)/2;
break;
case WEST:
x = ownerLoc.getX() - w - LabelFeatureContainer.LABEL_MARGIN;
break;
case EAST:
x = ownerLoc.getX() + ownerSize.getWidth() + LabelFeatureContainer.LABEL_MARGIN;
break;
case LEFT:
x = ownerLoc.getX() + LabelFeatureContainer.LABEL_MARGIN;
break;
case RIGHT:
x = ownerLoc.getX() + ownerSize.getWidth() - w - LabelFeatureContainer.LABEL_MARGIN;
break;
case MOVABLE:
x = (int) labelLoc.getX();
y = (int) labelLoc.getY();
if (offset != null) {
x += offset.getX();
y += offset.getY();
}
break;
}
// calculate Y coordinate
switch (vpos) {
case NORTH:
y = ownerLoc.getY() - h - LabelFeatureContainer.LABEL_MARGIN/2;
break;
case SOUTH:
y = ownerLoc.getY() + ownerSize.getHeight();
break;
case TOP:
y = ownerLoc.getY() + LabelFeatureContainer.LABEL_MARGIN / 2;
break;
case CENTER:
y = ownerLoc.getY() + (ownerSize.getHeight() - h)/2;
break;
case BOTTOM:
y = ownerLoc.getY() + ownerSize.getHeight() - h - LabelFeatureContainer.LABEL_MARGIN / 2;
break;
case WEST:
case EAST:
case LEFT:
case RIGHT:
// Y coordinate for these positions are all the same
y = ownerLoc.getY() + (ownerSize.getHeight() - h)/2;
break;
case MOVABLE:
break;
}
}
if (ownerPE instanceof Connection) {
x -= ownerLoc.getX();
y -= ownerLoc.getY();
}
return new Rectangle(x,y,w,h);
}
return null;
}
protected void adjustLabelLocation(PictogramElement pe, boolean isAddingLabel, Point offset) {
PictogramElement ownerPE = FeatureSupport.getLabelOwner(pe);
Shape labelShape = FeatureSupport.getLabelShape(pe);
if (labelShape != null) {
AbstractText labelGA = (AbstractText) labelShape.getGraphicsAlgorithm();
BaseElement element = (BaseElement) BusinessObjectUtil.getFirstElementOfType(pe, BaseElement.class);
String text = getLabelString(element);
if (text == null) {
text = ""; //$NON-NLS-1$
}
if (!text.equals(labelGA.getValue()))
labelGA.setValue(text);
Rectangle bounds = getLabelBounds(labelShape, isAddingLabel, offset);
int x = bounds.x;
int y = bounds.y;
int w = bounds.width;
int h = bounds.height;
// move and resize the label shape and set the new size of the Text GA
// make sure the label shape is a child of the same ContainerShape
// as its owner.
if (!(ownerPE instanceof Connection)) {
// this is only valid if the owner is not a Connection;
// Connection labels are always contained in the Diagram
// just like the Connection itself.
ContainerShape container = getTargetContainer(ownerPE);
if (labelShape.eContainer() != container) {
container.getChildren().add(labelShape);
}
}
GraphicsUtil.setLocationRelativeToDiagram(labelShape, x, y);
Graphiti.getGaService().setSize(labelGA, w, h);
if (ownerPE instanceof Shape) {
// Note that it's not necessary to send Connection Labels
// to the front because they are children of Connections
// which are in the Connection Layer, which is always on
// top of the Figure Layer.
Graphiti.getPeService().sendToFront(labelShape);
}
// if the label is owned by a connection, its location will always be
// relative to the connection midpoint so we have to get the absolute
// location for the BPMNLabel coordinates.
ILocation absloc = Graphiti.getPeService().getLocationRelativeToDiagram(labelShape);
DIUtils.updateDILabel(ownerPE, absloc.getX(), absloc.getY(), w, h);
if (!FeatureSupport.isHidden(labelShape))
labelShape.setVisible(!text.isEmpty());
Graphiti.getPeService().removeProperty(labelShape, GraphitiConstants.LABEL_CHANGED);
}
}
protected ContainerShape getTargetContainer(PictogramElement ownerPE) {
return (ContainerShape) ownerPE.eContainer();
}
protected int getLabelWidth(AbstractText text) {
return getLabelSize(text).width;
}
protected int getLabelHeight(AbstractText text) {
return getLabelSize(text).height;
}
protected Dimension getLabelSize(AbstractText text) {
int width = 0;
int height = 0;
if (text.getValue() != null && !text.getValue().isEmpty()) {
String[] strings = text.getValue().split(LabelFeatureContainer.LINE_BREAK);
for (String string : strings) {
IDimension dim = GraphitiUi.getUiLayoutService().calculateTextSize(string, text.getFont());
if (dim.getWidth() > width) {
width = dim.getWidth();
}
height += dim.getHeight();
}
}
// TODO: the zoom level influences the effective font size which determines the text extents.
// Need to figure this stuff out because labels are truncated at high zoom levels.
// The org.eclipse.graphiti.ui.internal.parts.directedit.GFDirectEditManager#updateScaledFont()
// does this for SWT Text widgets.
// GraphicalViewer viewer = (GraphicalViewer) ((IAdaptable)getDiagramEditor()).getAdapter(GraphicalViewer.class);
// ZoomManager zoomMgr = (ZoomManager) viewer.getProperty(ZoomManager.class.toString());
// if (zoomMgr!=null) {
// width = (int)(width * zoomMgr.getZoom());
// height = (int)(height * zoomMgr.getZoom());
// }
return new Dimension(width, height);
}
/**
* Get the position of the label relative to its owning figure for the given
* BaseElement as defined in the User Preferences.
*
* Overrides will provide their own relative positions for, e.g. Tasks and
* TextAnnotations.
* @param text the BaseElement that is represented by the graphical
* figure.
*
* @return a ShapeStyle LabelPosition relative location indicator.
*/
protected LabelPosition getLabelPosition(AbstractText text) {
PictogramElement pe = FeatureSupport.getLabelOwner(text);
BaseElement element = BusinessObjectUtil.getFirstBaseElement(pe);
ShapeStyle ss = ShapeStyle.getShapeStyle(element);
return ss.getLabelPosition();
}
protected LabelPosition getHorizontalLabelPosition(AbstractText text) {
return getLabelPosition(text);
}
protected LabelPosition getVerticalLabelPosition(AbstractText text) {
return getLabelPosition(text);
}
}