/******************************************************************************* * Copyright (c) 2009 the CHISEL group and contributors. * All rights reserved. This program and the accompanying materials * are 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: * Del Myers -- initial API and implementation *******************************************************************************/ package org.eclipse.zest.custom.sequence.visuals; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.eclipse.draw2d.Animation; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.PointList; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.GC; import org.eclipse.zest.custom.sequence.internal.AbstractSimpleProgressRunnable; import org.eclipse.zest.custom.sequence.internal.SimpleProgressMonitor; import org.eclipse.zest.custom.sequence.widgets.Activation; import org.eclipse.zest.custom.sequence.widgets.Call; import org.eclipse.zest.custom.sequence.widgets.Lifeline; import org.eclipse.zest.custom.sequence.widgets.Message; import org.eclipse.zest.custom.sequence.widgets.MessageGroup; import org.eclipse.zest.custom.sequence.widgets.UMLItem; import org.eclipse.zest.custom.sequence.widgets.UMLSequenceChart; import org.eclipse.zest.custom.sequence.widgets.internal.IWidgetProperties; /** * A runnable for laying out sequence diagrams made to be forkable so that the * main bulk of the layout work doesn't have to happen inside the UI thread. * That way, a progress indicator can be used and the users can continue to * work. * * @author Del Myers * */ class SequenceLayoutRunnable extends AbstractSimpleProgressRunnable { /** * Message groups have their own "containment" heirarchy containing messages * and other message groups. This makes it difficult to keep track of the * size and location of the group until after the rest of the chart has been * layed-out. Instead, we keep a tree of message groups which can be * traversed at the end, and applied later. * * @author Del Myers * */ private class MessageGroupLayoutNode { List<MessageGroupLayoutNode> childGroups; List<Message> containedMessages; Rectangle layout; private MessageGroup group; private MessageGroupLayoutNode parent; MessageGroupLayoutNode(MessageGroupLayoutNode parent, MessageGroup group) { childGroups = new LinkedList<MessageGroupLayoutNode>(); containedMessages = new LinkedList<Message>(); layout = new Rectangle(); this.group = group; this.parent = parent; if (parent != null) { parent.childGroups.add(this); } } } /** * Defines the family for a layout runnable running on a given chart. * * @author Del Myers * */ private static final class RunnableFamily { private UMLSequenceChart chart; private RunnableFamily(UMLSequenceChart chart) { this.chart = chart; } public boolean equals(Object obj) { if (obj == null || !obj.getClass().equals(getClass())) { return false; } return ((RunnableFamily) obj).chart == this.chart; } } private class TempLayoutObject { /** * Temporary variable that is used to track the number of activations * being executed on a lifeline during they layout. */ int executions; /** * An identifier for the widget. Used for consistency checks. */ String id; /** * The actual layout object. For messages, the x and y represent the * source and destination coordinates respectively. For all others, the * rectangle represents the bounding box. * * Activations will have their layout relative to the bounding box of * the lifeline in which they appear. The same will be true for the * endpoints of the messages (relative to the lifeline in which the * endpoint is contained). Lifelines are relative to the screen. */ Rectangle layout; /** * Field for when applying the layout for messages. Indicates that the * opposite end of the message has already been layed-out. */ //private boolean oppositeVisited; TempLayoutObject(String id) { this.id = id; this.layout = new Rectangle(); this.executions = 0; //this.oppositeVisited = false; } } private static final int ACTIVATION_WIDTH = 7; private static final int ACTIVATION_HALF_WIDTH = ACTIVATION_WIDTH / 2; public static final int TIME_UNIT_HEIGHT = 18; private UMLSequenceChart chart; private GC gc; /** * Temporary data stored on the widgets to organize their layout. */ private final String LAYOUT_DECORATION = "lytdec"; /** * The lifelines as they appear in the layout. */ private ArrayList<Lifeline> lifelines; /** * Used to count the amount of work that must be done. */ private int messageCount; /** * Used for building the layout for the message groups. */ private MessageGroupLayoutNode messageGroupLayout; private MessageGroupLayoutNode messageGroupLayoutPointer; public SequenceLayoutRunnable(UMLSequenceChart chart) { this.chart = chart; } @Override public Object getFamily() { return getFamily(chart); } public static Object getFamily(UMLSequenceChart chart) { return new RunnableFamily(chart); } @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { readAndDispatch(); if (chart.isDisposed()) return; lifelines = new ArrayList<Lifeline>(); this.gc = new GC(chart); //needed to make sure that there is enough room when calculating //sizes. gc.setAntialias(SWT.ON); messageGroupLayout = new MessageGroupLayoutNode(null, null); messageGroupLayoutPointer = messageGroupLayout; Animation.markBegin(); monitor.beginTask("Laying out diagram", 4000); try { if (!monitor.isCancelled()) { initializeLayout(monitor.createSubMonitor( "Initializing layout", 1000)); } readAndDispatch(); if (!monitor.isCancelled()) { calculateLayout(monitor.createSubMonitor("Calculating Layout", 1000)); } readAndDispatch(); if (!monitor.isCancelled()) { // can't read and dispatch after this because that would mess up // the animation. applyLayout(); monitor.worked(1000); } } finally { // clear the memory used. lifelines = null; gc.dispose(); cleanWidgets(); monitor.done(); } Animation.run(250); // readAndDispatch(); } /** * @param createSubMonitor * @return */ private void initializeLayout(SimpleProgressMonitor monitor) { Activation root = chart.getRootActivation(); if (root == null || root.isDisposed() || root.isHidden()) { monitor.cancel(); return; } Lifeline line = root.getVisibleLifeline(); // store the index of the array with the lifeline layout, so that they // can be easily found in the list. TempLayoutObject lifelineLayout = new TempLayoutObject(lifelines.size() + ""); line.setData(LAYOUT_DECORATION, lifelineLayout); lifelines.add(line); Message[] messages = root.getMessages(); TempLayoutObject rootLayout = new TempLayoutObject("1"); monitor.beginTask("Initializing layout", messages.length); root.setData(LAYOUT_DECORATION, rootLayout); int sequence = 1; for (Message m : messages) { if (!m.isDisposed() && m.isVisible()) { initializeMessage(m, root, sequence); sequence++; } monitor.worked(1); if (monitor.isCancelled()) { monitor.done(); } } monitor.done(); } /** * Initializes all the life line layouts in the order that they appear in * the lifelines list. * * @param lifelineLayout */ private void initializeLifeLineLayout(Lifeline line, int index) { TempLayoutObject lifelineLayout = (TempLayoutObject) line .getData(LAYOUT_DECORATION); if (lifelineLayout == null) { lifelineLayout = new TempLayoutObject(index + ""); line.setData(LAYOUT_DECORATION, lifelineLayout); } int width = 25; if (line.getText() != null) { width = getExtents(line)+10; } int width2 = 0; if (line.getStereoType() != null) { width2 = getExtents(line.getStereoType())+10; } if (width2 > width) { width = width2; } width = width + 20; // a little padding. int x = 0; int y = 0; if (index > 0) { TempLayoutObject neighbor = (TempLayoutObject) lifelines.get( index - 1).getData(LAYOUT_DECORATION); x = neighbor.layout.x + neighbor.layout.width + neighbor.layout.y + 10; } lifelineLayout.layout.setBounds(new Rectangle(x, y, width, 0)); } /** * @param m */ private void initializeMessage(Message m, Activation source, int sequence) { Activation target = m.getTarget(); if (!m.isVisible()) { return; } // don't process in-coming messages. if (target == null || target == source) { return; } TempLayoutObject sourceLayout = (TempLayoutObject) source .getData(LAYOUT_DECORATION); if (sourceLayout == null) { throw new IllegalStateException("Message " + m.toString() + " cannot be initialized without a source."); } TempLayoutObject messageLayout = (TempLayoutObject) m .getData(LAYOUT_DECORATION); if (messageLayout != null) { throw new IllegalStateException("Message " + m.toString() + " cannot be called more than once."); } messageLayout = new TempLayoutObject(sourceLayout.id + "." + sequence); m.setData(LAYOUT_DECORATION, messageLayout); // update the message count so that we know how much work to do. messageCount++; Lifeline lifeline = target.getVisibleLifeline(); if (!lifeline.isVisible()) { return; } TempLayoutObject lifelineLayout = (TempLayoutObject) lifeline .getData(LAYOUT_DECORATION); if (lifelineLayout == null) { lifelineLayout = new TempLayoutObject("" + lifelines.size()); lifeline.setData(LAYOUT_DECORATION, lifelineLayout); lifelines.add(target.getVisibleLifeline()); } TempLayoutObject targetLayout = (TempLayoutObject) target .getData(LAYOUT_DECORATION); if (targetLayout == null) { if (source.isExpanded()) { // only work with targets that are expanded when the layout // is null. // other ones will be returns, and have to be processed // anyway. // make the creation path through this message. This will be // used to ensure // that messages don't return to a path that they didn't // originate from. targetLayout = new TempLayoutObject(messageLayout.id); target.setData(LAYOUT_DECORATION, targetLayout); int csequence = 1; for (Message cm : target.getMessages()) { if (!cm.isDisposed() && m.isVisible()) { initializeMessage(cm, m.getTarget(), csequence); csequence++; } } } } else { // make sure that this message was called from the same sequence // that created it // otherwise, we are sending a message to either a dead // activation, or one that // can't be reached yet. if (messageLayout.id.startsWith(sourceLayout.id)) { } else if (!messageLayout.id.startsWith(targetLayout.id)) { throw new IllegalStateException("Message " + m.toString() + " cannot call activation " + target.toString() + ": they are not on the same call path."); } } readAndDispatch(); } /** * @param createSubMonitor */ private void calculateLayout(SimpleProgressMonitor monitor) { Activation root = chart.getRootActivation(); monitor.beginTask("Calculating Layout", messageCount); // initialize the lifeline layouts. int index = 0; for (Lifeline l : lifelines) { initializeLifeLineLayout(l, index); index++; } recursiveCalculateLayout(root, 0, monitor); monitor.done(); } /** * Lays out the given activation, and all of its out-going calls and * lifelines. * * @param activation * the activation to layout * @param top * the top at which the activation should start * @param monitor * the monitor to give updates on progress. Should increment for * every processed message * @return the bottom coordinate of the layed-out activation. */ private int recursiveCalculateLayout(Activation a, int top, SimpleProgressMonitor monitor) { TempLayoutObject layout = (TempLayoutObject) a .getData(LAYOUT_DECORATION); TempLayoutObject lifelineLayout = (TempLayoutObject) a.getVisibleLifeline() .getData(LAYOUT_DECORATION); int centerx = lifelineLayout.layout.width / 2 + (ACTIVATION_HALF_WIDTH * lifelineLayout.executions); layout.layout.setBounds(new Rectangle(centerx - ACTIVATION_HALF_WIDTH, top, ACTIVATION_WIDTH, TIME_UNIT_HEIGHT)); lifelineLayout.executions++; layout.layout.y = top; // if no messages are processed, then we have to pad the bottom. boolean addToBottom = true; Message[] messages = a.getMessages(); MessageGroup[] groups = a.getMessageGroups(); int groupIndex = 0; for (int i = 0; i < messages.length; i++) { //move up the group tree, getting rid of groups that //are no longer in range. while (messageGroupLayoutPointer.group != null && messageGroupLayoutPointer.group.getActivation() == a && (messageGroupLayoutPointer.group.getOffset() + messageGroupLayoutPointer.group.getLength() <= i)) { messageGroupLayoutPointer = messageGroupLayoutPointer.parent; //top += TIME_UNIT_HEIGHT; top += 3; } if (messageGroupLayoutPointer == null) { messageGroupLayoutPointer = messageGroupLayout; } Message m = messages[i]; while (groupIndex < groups.length && groups[groupIndex].getOffset() <= i) { MessageGroup group = groups[groupIndex]; if (group.isVisible()) { MessageGroupLayoutNode node = new MessageGroupLayoutNode(messageGroupLayoutPointer, group); if (group.getLength() > 0) { messageGroupLayoutPointer = node; } //initialize the layout. The X position will be at the same position //as the activation. top += TIME_UNIT_HEIGHT; node.layout.y = top; node.layout.width = (group.isExpanded() ? 0 : 40); node.layout.height = TIME_UNIT_HEIGHT; node.layout.x = layout.layout.x; // add a little bit of time top += TIME_UNIT_HEIGHT; } groupIndex++; } if (!m.isVisible()) continue; if (messageGroupLayoutPointer.group != null) { messageGroupLayoutPointer.containedMessages.add(m); } // calculate for out-going messages. boolean messagePointsRight = messagePointsRight(m); TempLayoutObject messageLayout = (TempLayoutObject) m .getData(LAYOUT_DECORATION); if (m.getSource() == a) { top += TIME_UNIT_HEIGHT; nudgeLifelines(m); // check the target to see if it is a return. Activation target = m.getTarget(); // set-up the message layout messageLayout.layout.y = top; boolean isSelfCall = isSelfCall(m); if (isSelfCall(m) && m instanceof Call) { top += (TIME_UNIT_HEIGHT/3); } messageLayout.layout.height = top; if (m instanceof Call) { top = recursiveCalculateLayout(target, top, monitor); if (isSelfCall(m)) { top +=TIME_UNIT_HEIGHT/3; } } else { addToBottom = false; if (isSelfCall) { messageLayout.layout.height += (TIME_UNIT_HEIGHT/3); } } TempLayoutObject targetLayout = (TempLayoutObject) target .getData(LAYOUT_DECORATION); if (!target.isVisible() || targetLayout == null) { //account for targets that no longer exist. messageLayout.layout.x = centerx - ACTIVATION_HALF_WIDTH; messageLayout.layout.width = messageLayout.layout.x; } else { if (messagePointsRight) { messageLayout.layout.x = centerx + ACTIVATION_HALF_WIDTH; messageLayout.layout.width = targetLayout.layout.x + ((isSelfCall) ? targetLayout.layout.width : 0); } else { messageLayout.layout.x = centerx - ACTIVATION_HALF_WIDTH; messageLayout.layout.width = targetLayout.layout.x + ACTIVATION_WIDTH; } } monitor.worked(1); readAndDispatch(); } } if (addToBottom) top += TIME_UNIT_HEIGHT; layout.layout.height = top - layout.layout.y; //close all the groups. int groupPadding = 0; while (messageGroupLayoutPointer.group != null && messageGroupLayoutPointer.group.getActivation()==a) { messageGroupLayoutPointer = messageGroupLayoutPointer.parent; groupPadding += 80; } if (messageGroupLayoutPointer == null) { messageGroupLayoutPointer = messageGroupLayout; } if (layout.layout.y + layout.layout.height > lifelineLayout.layout.y + lifelineLayout.layout.height) { int bottom = layout.layout.y + layout.layout.height + 10; lifelineLayout.layout.height = bottom - lifelineLayout.layout.y; } lifelineLayout.executions--; return layout.layout.y + layout.layout.height; } /** * Adjusts the distance between lifelines to make room for messages. * * @param lifeline * @param targetLifeline * @param messageWidth */ private void nudgeLifelines(Message m) { Lifeline sourceLifeline = m.getSource().getVisibleLifeline(); Lifeline targetLifeline = m.getTarget().getVisibleLifeline(); if (!sourceLifeline.isVisible()) { return; } else if (!targetLifeline.isVisible()) { return; } int messageWidth = 0; if (m.getText() != null) { messageWidth = getExtents(m) + 10; } TempLayoutObject sourceLayout = (TempLayoutObject) sourceLifeline .getData(LAYOUT_DECORATION); TempLayoutObject targetLayout = (TempLayoutObject) targetLifeline .getData(LAYOUT_DECORATION); int sourceIndex = Integer.parseInt(sourceLayout.id); int targetIndex = -1; if (targetLayout != null) { // shouldn't happen right now, but may happen in the future that we // display returns to // parents that can't be seen (eg. in the case of a thrown // exception). targetIndex = Integer.parseInt(targetLayout.id); } boolean pointsRight = targetIndex >= sourceIndex; // check the distance between the source and its nearest neighbor, not // the source and target. TempLayoutObject first = sourceLayout; TempLayoutObject next = null; int firstIndex = sourceIndex; if (pointsRight) { if (sourceIndex < lifelines.size() - 1) { next = (TempLayoutObject) lifelines.get(sourceIndex + 1) .getData(LAYOUT_DECORATION); } else { // no need to do anything, just return. return; } } else { next = first; if (sourceIndex > 0) { first = (TempLayoutObject) lifelines.get(sourceIndex - 1) .getData(LAYOUT_DECORATION); firstIndex--; } else { firstIndex = 0; first = null; } } // make the right end-point to the center of the lifeline + the distance // needed for // embedded activations. int right = next.layout.getCenter().x + (next.executions - 1) * ACTIVATION_WIDTH + ((pointsRight) ? ACTIVATION_HALF_WIDTH : -ACTIVATION_HALF_WIDTH); int left = 0; if (first != null) { left = first.layout.getCenter().x + first.executions * ACTIVATION_WIDTH + ACTIVATION_HALF_WIDTH; } int distance = right - left; if (distance < messageWidth) { // adjust all of the lifelines to give room for the message. int nudge = messageWidth - distance; for (int i = firstIndex + 1; i < lifelines.size(); i++) { Lifeline line = lifelines.get(i); TempLayoutObject llayout = (TempLayoutObject) line .getData(LAYOUT_DECORATION); llayout.layout.x += nudge; } } } /** * Applies the layout that has been temporarily stored in the widgets. */ private void applyLayout() { // initialize the lifeline layouts. for (Lifeline l : lifelines) { TempLayoutObject lifelineLayout = (TempLayoutObject) l .getData(LAYOUT_DECORATION); l.setData(IWidgetProperties.LAYOUT, lifelineLayout.layout); } Activation root = chart.getRootActivation(); applyLayout(root); layoutMessageGroups(messageGroupLayout); } /** * Recursively visits the activations, applying their layouts and the * layouts of their messages. Assumes that all lifelines * have been previously layed out. * @param root */ private void applyLayout(Activation a) { //apply the layout for all the called activations first. for (Message m : a.getMessages()) { if (m.isVisible() && m instanceof Call) { applyLayout(m.getTarget()); } } TempLayoutObject activationLayout = (TempLayoutObject) a .getData(LAYOUT_DECORATION); Rectangle lifelineLayout = (Rectangle) a.getVisibleLifeline().getData(IWidgetProperties.LAYOUT); Rectangle activationBounds = new Rectangle( activationLayout.layout.x + lifelineLayout.x, activationLayout.layout.y + lifelineLayout.y, activationLayout.layout.width, activationLayout.layout.height ); for (Message m : a.getMessages()) { if (m.isVisible()) { Rectangle targetLifelineBounds = (Rectangle)m.getTarget().getVisibleLifeline().getData(IWidgetProperties.LAYOUT); TempLayoutObject messageLayout = (TempLayoutObject) m.getData(LAYOUT_DECORATION); PointList points = new PointList(); //add the start point. points.addPoint(messageLayout.layout.x + lifelineLayout.x, messageLayout.layout.y + lifelineLayout.y); //add end point Point end = null; if (targetLifelineBounds == null) { end = new Point(0, messageLayout.layout.height + lifelineLayout.y); } else { end = new Point(messageLayout.layout.width + targetLifelineBounds.x, messageLayout.layout.height + targetLifelineBounds.y); } if (m.getTarget().getVisibleLifeline() == m.getSource().getVisibleLifeline()) { //self-call, add two extra points. gc.setFont(m.getFont()); int extents = getExtents(m) + 10; Point start = points.getFirstPoint(); points.addPoint(start.x + extents, start.y); points.addPoint(start.x + extents, end.y); } points.addPoint(end); m.setData(IWidgetProperties.LAYOUT, points); } } a.setData(IWidgetProperties.LAYOUT, activationBounds); } /** * */ private void layoutMessageGroups(MessageGroupLayoutNode node) { //run a depth-first search on the message group layout tree, making each group //surround the messages that are contained within it. //the root has no group, so we must check this int originX = 0; if (node.group != null) { //add all of the visible messages within the group. Rectangle activationLayout = (Rectangle) node.group.getActivation().getData(IWidgetProperties.LAYOUT); node.layout.x = originX = activationLayout.x; } for (MessageGroupLayoutNode child : node.childGroups) { layoutMessageGroups(child); //reset the x position to the final x position of the activation if (node.group != null) { //get the child's layout and expand it by a little, just to add some padding. Rectangle childLayout = child.layout.getCopy().expand(2, 2); node.layout.union(childLayout); } } //the root has no group, so we must check this if (node.group != null) { for (Message m : node.containedMessages) { PointList points = (PointList) m.getData(IWidgetProperties.LAYOUT); node.layout.union(points.getBounds().getCopy().expand(0, 5)); if (m instanceof Call) { Activation target = m.getTarget(); Rectangle tLayout = (Rectangle) target.getData(IWidgetProperties.LAYOUT); if (tLayout != null) { node.layout.union(tLayout.getCopy().expand(2, 2)); } } } if (node.group.isExpanded()) { int textWidth = getExtents(node.group); int d = node.layout.width - (originX - node.layout.x); if (d < textWidth) { node.layout.width += (textWidth-d) + 14; } } node.group.setData(IWidgetProperties.LAYOUT, node.layout); } } /** * */ private void cleanWidgets() { LinkedList<UMLItem> items = new LinkedList<UMLItem>(); items.add(chart.getRootActivation()); while (items.size() != 0) { UMLItem item = items.removeFirst(); // just a convenience check for when the root doesn't exist. if (item == null || item.isDisposed()) { continue; } item.setData(LAYOUT_DECORATION, null); if (item instanceof Message) { Message m = (Message) item; Activation target = m.getTarget(); if (target != null && !target.isDisposed() && target.getData(LAYOUT_DECORATION) != null) { items.add(target); } } else if (item instanceof Activation) { Activation a = (Activation) item; Lifeline l = a.getVisibleLifeline(); if (l.getData(LAYOUT_DECORATION) != null) { l.setData(LAYOUT_DECORATION, null); } for (Message m : a.getMessages()) { if (m.getSource() == a) { if (!m.isDisposed() && m.getData(LAYOUT_DECORATION) != null) { items.add(m); } } } } } } /** * Calculates the bounding box of the given text for the diagram. No tab * expansions or carriage returns are calculated. * * @param text * the text to calculate. * @return the width of the text. */ private int getExtents(UMLItem widget) { Label tempLabel = new Label(); tempLabel.setText(widget.getText()); tempLabel.setFont(widget.getFont()); tempLabel.setIcon(widget.getImage()); return tempLabel.getPreferredSize().width; } /** * @param stereoType * @return */ private int getExtents(String text) { Label tempLabel = new Label(); tempLabel.setText(text); tempLabel.setFont(chart.getFont()); return tempLabel.getPreferredSize().width; } /** * Returns true if the given message returns has the same lifeline on its * source and target. * * @param m * the message to check. * @return */ private boolean isSelfCall(Message m) { return m.getSource().getVisibleLifeline() == m.getTarget().getVisibleLifeline(); } /** * @return true if the given message points right in this layout. */ private boolean messagePointsRight(Message m) { if (!m.isVisible()) { return false; } Lifeline sourceLifeline = m.getSource().getVisibleLifeline(); Lifeline targetLifeline = m.getTarget().getVisibleLifeline(); TempLayoutObject sourceLayout = (TempLayoutObject) sourceLifeline .getData(LAYOUT_DECORATION); TempLayoutObject targetLayout = (TempLayoutObject) targetLifeline .getData(LAYOUT_DECORATION); if (sourceLayout == null || targetLayout == null) { return false; } int sourceIndex = Integer.parseInt(sourceLayout.id); int targetIndex = Integer.parseInt(targetLayout.id); return sourceIndex <= targetIndex; } }