/******************************************************************************* * 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.uml.viewers; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.IColorProvider; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Widget; import org.eclipse.zest.custom.sequence.events.SequenceEvent; import org.eclipse.zest.custom.sequence.events.SequenceListener; import org.eclipse.zest.custom.sequence.events.internal.ListenerList; import org.eclipse.zest.custom.sequence.internal.AbstractSimpleProgressRunnable; import org.eclipse.zest.custom.sequence.internal.IUIProgressService; import org.eclipse.zest.custom.sequence.internal.SimpleProgressMonitor; import org.eclipse.zest.custom.sequence.internal.UIJobProcessor; import org.eclipse.zest.custom.sequence.widgets.Activation; import org.eclipse.zest.custom.sequence.widgets.Call; import org.eclipse.zest.custom.sequence.widgets.IExpandableItem; 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.Return; import org.eclipse.zest.custom.sequence.widgets.UMLColoredItem; import org.eclipse.zest.custom.sequence.widgets.UMLItem; import org.eclipse.zest.custom.sequence.widgets.UMLSequenceChart; import org.eclipse.zest.custom.sequence.widgets.UMLTextColoredItem; /** * A viewer on top of a UMLSequenceChart. * @author Del Myers * */ public class UMLSequenceViewer extends StructuredViewer { private UMLSequenceChart chart; private IMessageGrouper grouper; private ListenerList sequenceListeners; //used in runnables to limit the execution time and memory needed for the chart private final int TIME_LIMIT = 30000; private final int CHILD_LIMIT = 1000; private class InternalSequenceListener implements SequenceListener { /* (non-Javadoc) * @see org.eclipse.zest.custom.sequence.events.SequenceListener#itemCollapsed(org.eclipse.zest.custom.sequence.events.SequenceEvent) */ public void itemCollapsed(SequenceEvent event) { if (event.item instanceof MessageGroup) { SequenceViewerGroupEvent sEvent = new SequenceViewerGroupEvent(UMLSequenceViewer.this, (IMessageGrouping) event.item.getData()); fireCollapseEvent(sEvent); return; } SequenceViewerEvent sEvent = new SequenceViewerEvent(UMLSequenceViewer.this, event.item.getData()); fireCollapseEvent(sEvent); } /* (non-Javadoc) * @see org.eclipse.zest.custom.sequence.events.SequenceListener#itemExpanded(org.eclipse.zest.custom.sequence.events.SequenceEvent) */ public void itemExpanded(SequenceEvent event) { if (event.item instanceof Activation) { Activation a = (Activation) event.item; if (a.hasChildren() && a.getMessages().length == 0) { //we have to load the messages. //could possibly do this in a runnable. internalRefreshActivation(a, null); } } if (event.item instanceof MessageGroup) { SequenceViewerGroupEvent sEvent = new SequenceViewerGroupEvent(UMLSequenceViewer.this, (IMessageGrouping) event.item.getData()); fireExpandEvent(sEvent); return; } SequenceViewerEvent sEvent = new SequenceViewerEvent(UMLSequenceViewer.this, event.item.getData()); fireExpandEvent(sEvent); } /* (non-Javadoc) * @see org.eclipse.zest.custom.sequence.events.SequenceListener#rootChanged(org.eclipse.zest.custom.sequence.events.SequenceEvent) */ public void rootChanged(SequenceEvent event) { SequenceViewerRootEvent sEvent = new SequenceViewerRootEvent(UMLSequenceViewer.this); fireRootEvent(sEvent); } } private final class ExpandActivationsUnderRunnable extends AbstractSimpleProgressRunnable { private final Object activationElement; //used to count the number of children that have been loaded or //displayed for this runnable. //private int childCount; //private long startTime; private boolean limit; /** * Creates a new runnable for expanding children. * @param activationElement element to expand. * @param limit true if there should be a limit on the number of children / time it takes to expand. */ private ExpandActivationsUnderRunnable(Object activationElement, boolean limit) { this.activationElement = activationElement; this.limit = limit; } @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { try { getChart().setRedraw(false); monitor.beginTask("Expanding Activations", IUIProgressService.UNKNOWN_WORK); Object root = activationElement; Widget[] results = findItems(root); Widget item = null; for (int i = 0; i < results.length; i++) { if(results[i] instanceof Activation){ item = results[i]; break; } } if (item instanceof Activation) { //startTime = System.currentTimeMillis(); recursiveExpandActivations((Activation)item, monitor); } readAndDispatch(); } finally { if (!monitor.isCancelled()) { monitor.done(); } getChart().setRedraw(true); } } private void recursiveExpandActivations(Activation activation, SimpleProgressMonitor monitor) { readAndDispatch(); if (monitor.isCancelled()) { return; } monitor.beginTask("Expanding children", IUIProgressService.UNKNOWN_WORK); if (activation.hasChildren() && activation.getMessages().length == 0) { //we have to load the messages. //could possibly do this in a runnable. internalRefreshActivation(activation, null); } long startTime = System.currentTimeMillis(); int childCount = 0; LinkedList<Activation> activationStack = new LinkedList<Activation>(); activationStack.add(activation); while (activationStack.size() > 0) { readAndDispatch(); long elapsedTime = (limit) ? System.currentTimeMillis() - startTime : 0; if (childCount > CHILD_LIMIT || elapsedTime > TIME_LIMIT) { boolean result = MessageDialog.openQuestion( getChart().getShell(), "Large Sequence Diagram Warning", "You are trying to display an extremely large sequence diagram which " + "will likely degrade system performance. Are you sure you would like to continue?" ); if (result) { childCount = 0; startTime = System.currentTimeMillis(); } else { monitor.cancel(); } } if (monitor.isCancelled()) { break; } Activation a = activationStack.removeFirst(); monitor.setSubTask(a.getText()); boolean wasExpanded = a.isExpanded(); a.setExpanded(true); for (Message m : a.getMessages()) { if (m instanceof Call) { Activation target = m.getTarget(); if (target != null && !target.isDisposed()) { activationStack.add(target); if (limit && !wasExpanded) ++childCount; } } } readAndDispatch(); } } } /** * Collapses all activations under the activation element given in the constructor. * @author Del Myers * */ private final class CollapseActivationsUnderRunnable extends AbstractSimpleProgressRunnable { private final Object activationElement; private CollapseActivationsUnderRunnable(Object activationElement) { this.activationElement = activationElement; } private void recursiveCollapseActivations(Activation activation, SimpleProgressMonitor monitor) { for (Message m : activation.getMessages()) { if (!(m instanceof Call)) continue; Call call = (Call) m; if (monitor.isCancelled()) return; monitor.setSubTask(call.getText()); readAndDispatch(); recursiveCollapseActivations(call.getTarget(), monitor); } activation.setExpanded(false); } @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { try { getChart().setRedraw(false); Object root = activationElement; Widget[] results = findItems(root); Widget item = null; for (int i = 0; i < results.length; i++) { if(results[i] instanceof Activation){ item = results[i]; break; } } if (item instanceof Activation) { monitor.beginTask("Collapsing Activations Under " + ((Activation)item).getText(), IUIProgressService.UNKNOWN_WORK); recursiveCollapseActivations((Activation)item, monitor); } } finally { readAndDispatch(); monitor.done(); getChart().setRedraw(true); } } } /** * Runnable for expanding activations that are within a lifeline. Only expands those that are * currently reachable on the lifeline, and that become reachable after the current activations * are expanded. * @author Del Myers * */ private final class ExpandActivationsInRunnable extends AbstractSimpleProgressRunnable { private Object lifelineElement; private boolean limit; public ExpandActivationsInRunnable(Object lifelineElement, boolean limit) { this.lifelineElement = lifelineElement; this.limit = limit; } /* (non-Javadoc) * @see org.eclipse.zest.custom.sequence.internal.AbstractSimpleProgressRunnable#doRunInUIThread(org.eclipse.zest.custom.sequence.internal.SimpleProgressMonitor) */ @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { try { getChart().setRedraw(false); Widget item = findItem(lifelineElement); if (!(item instanceof Lifeline)) { return; } Lifeline ll = (Lifeline) item; monitor.beginTask("Expading Activations In " + ll.getText(), IUIProgressService.UNKNOWN_WORK); Activation[] activationArray = ll.getAllActivations(); List<Activation> activationList = new LinkedList<Activation>(Arrays.asList(activationArray)); int childCount = 0; long startTime = System.currentTimeMillis(); while (activationList.size() > 0) { readAndDispatch(); Activation a = activationList.remove(0); monitor.setSubTask(a.getText()); if (!a.isExpanded()) { a.setExpanded(true); //search children for messages that land on the current lifeline. LinkedList<Message> messages = new LinkedList<Message>(); messages.addAll(Arrays.asList(a.getMessages())); while (messages.size() > 0) { readAndDispatch(); if (monitor.isCancelled()) { break; } Message m = messages.removeFirst(); if (m instanceof Return) continue; Activation target = m.getTarget(); if (target != null && !target.isDisposed()) { if (!target.isExpanded() && target.getLifeline() == ll) { childCount++; activationList.add(target); } if (target.isExpanded()) { //search the children messages.addAll(Arrays.asList(target.getMessages())); } } } for (Message m : a.getMessages()) { if (m.getTarget() != null && !m.getTarget().isDisposed() && m.getTarget().getLifeline() == ll) { childCount++; activationList.add(m.getTarget()); } } } if (limit && (childCount > CHILD_LIMIT || (System.currentTimeMillis() - startTime > TIME_LIMIT))) { boolean result = MessageDialog.openQuestion( getChart().getShell(), "Large Sequence Diagram Warning", "You are trying to display an extremely large sequence diagram which " + "will likely degrade system performance. Are you sure you would like to continue?" ); if (result) { childCount = 0; startTime = System.currentTimeMillis(); } else { monitor.cancel(); } } if (monitor.isCancelled()) { break; } } } finally { readAndDispatch(); monitor.done(); getChart().setRedraw(true); } } } private final class CollapseActivationsInRunnable extends AbstractSimpleProgressRunnable { private Object lifelineElement; public CollapseActivationsInRunnable(Object lifelineElement) { this.lifelineElement = lifelineElement; } /* (non-Javadoc) * @see org.eclipse.zest.custom.sequence.internal.AbstractSimpleProgressRunnable#doRunInUIThread(org.eclipse.zest.custom.sequence.internal.SimpleProgressMonitor) */ @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { try { getChart().setRedraw(false); Widget item = findItem(lifelineElement); if (!(item instanceof Lifeline)) { return; } Lifeline ll = (Lifeline) item; monitor.beginTask("Expanding Activations In " + ll.getText(), IUIProgressService.UNKNOWN_WORK); Activation[] activationArray = ll.getAllActivations(); for (Activation a : activationArray) { a.setExpanded(false); } } finally { readAndDispatch(); monitor.done(); getChart().setRedraw(true); } } } /** * Creates a sequence viewer on the given parent, using the given style. * No layout is set on the chart. Clients should call {@link #getChart()} or * {@link #getControl()} to set its layout or layout data. */ public UMLSequenceViewer(Composite parent, int style) { this.chart = new UMLSequenceChart(parent, style); sequenceListeners = new ListenerList(); setUseHashlookup(true); chart.addSequenceListener(new InternalSequenceListener()); hookControl(chart); } /** * Fires the given event object based on its type. * @param event */ protected void fireCollapseEvent(Object event) { for (Object o : sequenceListeners.getListeners()) { ISequenceViewerListener l = (ISequenceViewerListener) o; if (event instanceof SequenceViewerGroupEvent) { l.groupCollapsed((SequenceViewerGroupEvent)event); } else { l.elementCollapsed((SequenceViewerEvent) event); } } } /** * Fires the given event object based on its type. * @param event */ protected void fireExpandEvent(Object event) { for (Object o : sequenceListeners.getListeners()) { ISequenceViewerListener l = (ISequenceViewerListener) o; if (event instanceof SequenceViewerGroupEvent) { l.groupExpanded((SequenceViewerGroupEvent)event); } else { l.elementExpanded((SequenceViewerEvent) event); } } } /** * Fires the given event object based on its type. * @param event */ protected void fireRootEvent(Object event) { for (Object o : sequenceListeners.getListeners()) { ISequenceViewerListener l = (ISequenceViewerListener) o; l.rootChanged((SequenceViewerRootEvent) event); } } /** * Creates a sequence viewer using the given chart. Note that the chart will * be cleared of all widgets when used in this constructor. */ public UMLSequenceViewer(UMLSequenceChart chart) { chart.setRedraw(false); chart.clearChart(); chart.setRedraw(true); this.chart = chart; sequenceListeners = new ListenerList(); setUseHashlookup(true); chart.addSequenceListener(new InternalSequenceListener()); hookControl(chart); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object) */ @Override protected Widget doFindInputItem(Object element) { if (getInput() != null) { if (getInput().equals(element)) { return getChart(); } } return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object) */ @Override protected Widget doFindItem(Object element) { if (element == null) { return null; } if (getChart() != null && !getChart().isDisposed()) { UMLItem[] items = getChart().getItems(); for (UMLItem item : items) { if (!item.isDisposed() && element.equals(item.getData())) { return item; } } } return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doUpdateItem(org.eclipse.swt.widgets.Widget, java.lang.Object, boolean) */ @Override protected void doUpdateItem(Widget widget, Object element, boolean fullMap) { if (widget.isDisposed()) return; if (widget instanceof UMLItem) { UMLItem item = (UMLItem) widget; if (fullMap) { associate(element, item); } else { Object data = item.getData(); if (data != null) { unmapElement(data, item); } item.setData(element); mapElement(element, item); } IBaseLabelProvider provider = getLabelProvider(); if (widget instanceof MessageGroup) { MessageGroup group = (MessageGroup) widget; if (provider instanceof ItemLabelProvider) { ItemLabelProvider.ItemLabel label = new ItemLabelProvider.ItemLabel(); label.background = group.getBackground(); label.foreground = group.getForeground(); label.text = group.getText(); ((ItemLabelProvider)provider).style(label, element); } return; } if (provider instanceof ILabelProvider) { String text = ((ILabelProvider)provider).getText(element); item.setText((text != null) ? text : ""); Image image = ((ILabelProvider)provider).getImage(element); item.setImage(image); } if (provider instanceof IColorProvider) { Color fc = ((IColorProvider)provider).getForeground(element); Color bc = ((IColorProvider)provider).getBackground(element); if (item instanceof UMLColoredItem) { ((UMLColoredItem)item).setForeground(fc); ((UMLColoredItem)item).setBackground(bc); } } if (provider instanceof ITextColorProvider) { Color fc = ((ITextColorProvider)provider).getTextForeground(element); Color bc = ((ITextColorProvider)provider).getTextBackground(element); if (item instanceof UMLTextColoredItem) { ((UMLTextColoredItem)item).setTextForeground(fc); ((UMLTextColoredItem)item).setTextBackground(bc); } } if (widget instanceof Lifeline) { if (provider instanceof ISequenceLabelProvider) { String stereotype = ((ISequenceLabelProvider)provider).getStereoType(element); ((Lifeline)widget).setStereotype((stereotype != null) ? stereotype : ""); if (provider instanceof IStylingSequenceLabelProvider) { int lifelineStyle = ((IStylingSequenceLabelProvider)provider).getLifelineStyle(element); if (lifelineStyle == -1) { lifelineStyle = Lifeline.CLASS; } ((Lifeline)widget).setClassStyle(lifelineStyle); } } } else if (widget instanceof Message) { int lineStyle = SWT.LINE_SOLID; int targetStyle = Message.CLOSED_ARROW | Message.FILL_MASK; int sourceStyle = Message.NONE; Message m = (Message) widget; if (widget instanceof Return) { lineStyle = SWT.LINE_DASH; targetStyle = Message.OPEN_ARROW; } if (provider instanceof IStylingSequenceLabelProvider) { IStylingSequenceLabelProvider sslp = (IStylingSequenceLabelProvider) provider; int temp = sslp.getMessageLineStyle(element); if (temp != -1) { lineStyle = temp; } temp = sslp.getMessageSourceStyle(element); if (temp != -1) { sourceStyle = temp; } temp = sslp.getMessageTargetStyle(element); if (temp != -1) { targetStyle = temp; } } m.setLineStyle(lineStyle); m.setSourceStyle(sourceStyle); m.setTargetStyle(targetStyle); } } } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget() */ @SuppressWarnings("unchecked") @Override protected List getSelectionFromWidget() { UMLItem[] items = getChart().getSelection(); HashSet<Object> elementSelection = new HashSet<Object>(); for (UMLItem item : items) { if (!item.isDisposed()) { elementSelection.add(item.getData()); } } List<?> list = new ArrayList<Object>(elementSelection); return list; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object) */ @Override protected void internalRefresh(Object element) { try { getChart().setRedraw(false); if (element == null && getInput() == null) { getChart().clearChart(); unmapAllElements(); return; } if (element == null) { return; } if (element.equals(getInput())) { LinkedList<Object> expanded = new LinkedList<Object>(); LinkedList<Activation> activations = new LinkedList<Activation>(); ISequenceChartContentProvider provider = (ISequenceChartContentProvider) getContentProvider(); Object[] roots = provider.getElements(getInput()); Activation rootActivation = null; if (roots != null && roots.length == 1) { Widget w = findItem(roots[0]); if (w instanceof Activation) { rootActivation = (Activation) w; } } if (rootActivation == null) { rootActivation = getChart().getRootActivation(); } if (rootActivation != null && rootActivation.isExpanded()) { activations.addFirst(rootActivation); } //store the expanded activations while (activations.size() > 0) { Activation a = activations.removeFirst(); expanded.add(a.getData()); for (Message m :a.getMessages()) { if (m instanceof Call) { if (m.getTarget() != null && m.getTarget().isExpanded()) { activations.add(m.getTarget()); } } } } //store the root Object root = getRootActivation(); //clear the chart getChart().clearChart(); unmapAllElements(); //create a new activation for the root. Object[] elements = provider.getElements(getInput()); if (elements.length == 1) { Activation a = new Activation(getChart()); updateItem(a, elements[0]); internalRefreshActivation(a, null); getChart().setRootActivation(a); } while (expanded.size() > 0) { Object next = expanded.removeFirst(); Widget[] items = findItems(next); for (Widget item : items) { if (item instanceof Activation) { ((Activation)item).setExpanded(true); } } } //try to restore the root if (root != null) { Widget rootA = findItem(root); if (rootA instanceof Activation) { getChart().setRootActivation((Activation) rootA); } } } else { //refresh for the different kinds of elements. Widget[] items = findItems(element); if (items != null) { for (Widget item : items) { if (item instanceof Activation) { internalRefreshActivation((Activation)item, null); } } } } } catch (Exception e) { e.printStackTrace(); } finally { getChart().setRedraw(true); } } /** * Refreshes from the given activation, rebuilding lifelines and messages as it goes. * @param activationElement * @param callStack a list of parent activations where the first element is the most * recent parent. */ private void internalRefreshActivation(Activation a, LinkedList<Activation> callStack) { try { getChart().setRedraw(false); if (callStack == null) { callStack = new LinkedList<Activation>(); } if (callStack.isEmpty()) { if (a.getSourceCall() != null && a.getSourceCall().getTarget() != null) { Activation parent = a.getSourceCall().getTarget(); while (parent != null) { callStack.addLast(parent); if (parent.getSourceCall() != null) { parent = parent.getSourceCall().getSource(); } else { parent = null; } } } } if (a == null || a.isDisposed()) return; //make sure that the activation should be shown if (!select(a.getData())) { //it shouldn't be. So, delete the sub tree deleteSubTree(a); return; } ISequenceChartContentProvider provider = (ISequenceChartContentProvider) getContentProvider(); internalRefreashActivationLifeline(a); //the activation may have been disposed in the above process. if (a.isDisposed()) { return; } //update the messages Message[] oldMessages = a.getMessages(); Object[] filteredMessages = filter(provider.getMessages(a.getData())); //run through a filtering to make sure that the targets of the messages will be visible as well List<Object> filteredMessageList = new LinkedList<Object>(); for (Object m : filteredMessages) { if (selectActivation(provider.getTarget(m))) { filteredMessageList.add(m); } } Object[] newMessageElements = filteredMessageList.toArray(); ArrayList<Message> newMessages = new ArrayList<Message>(); newMessages.addAll(Arrays.asList(oldMessages)); HashSet<Object> mappedElements = new HashSet<Object>(); LinkedList<Activation> activationsToLoad = new LinkedList<Activation>(); for (Message m : newMessages) { if (!m.isDisposed()) { mappedElements.add(m.getData()); } } int i = 0; for (; i < newMessageElements.length; i++) { Object messageElement = newMessageElements[i]; Object target = provider.getTarget(messageElement); Message oldMessage = null; if (mappedElements.contains(messageElement)) { for (int j = i; j < oldMessages.length; j++) { Message temp = newMessages.get(j); if (!temp.isDisposed()) { if (temp.getData().equals(messageElement)) { if (i != j) { //swap the messages to put them in order. Message replacement = newMessages.get(i); newMessages.set(i, temp); newMessages.set(j, replacement); } oldMessage = temp; break; } } } } Message newMessage = null; Activation newTarget = null; if (oldMessage != null) { newMessage = oldMessage; if (provider.isCall(messageElement)) { if (!(oldMessage instanceof Call)) { //remove this message from the current list and throw it //at the end as trash. newMessages.remove(i); newMessages.add(oldMessage); newMessage = null; } } else if (!(oldMessage instanceof Return)) { //remove this message from the current list and throw it //at the end as trash. newMessages.remove(i); newMessages.add(oldMessage); newMessage = null; } if (newMessage != null) { Activation oldTarget = newMessage.getTarget(); if (oldTarget != null && !oldTarget.isDisposed()) { if (oldTarget.getData().equals(target)) { newTarget = oldTarget; } else { deleteSubTree(oldTarget); } } } } if (newMessage == null) { //have to create a new message. if (provider.isCall(messageElement)) { newMessage = new Call(getChart()); } else { newMessage = new Return(getChart()); } updateItem(newMessage, messageElement); if (i == newMessages.size()) { newMessages.add(newMessage); } else { newMessages.add(i, newMessage); } } if (newTarget == null) { if (newMessage instanceof Return) { //try and use the fastest ways to get a match. //first, check the immediate parent. if (a.getSourceCall() != null && target.equals(a.getSourceCall().getSource().getData())) { //found a match, no need to continue. newTarget = a.getSourceCall().getSource(); } else { if (usingElementMap()) { //otherwise, check the map for a single match Widget[] widgets = findItems(target); if (widgets.length == 1 && widgets[0] instanceof Activation) { newTarget = (Activation)widgets[0]; } } if (newTarget == null) { //finally, try and find a previous match on the call stack. Iterator<Activation> stackIterator = callStack.iterator(); while (stackIterator.hasNext()) { Activation parent = stackIterator.next(); if (parent.getData().equals(target)) { if (parent != a) { newTarget = parent; } break; } } } } } else { //create a new target to call. newTarget = new Activation(getChart()); updateItem(newTarget, target); if (!isLazyLoading()) { activationsToLoad.add(newTarget); } } } if (newTarget != null) { //make sure that the lifeline is correct. // if (newTarget.getLifeline() == null) { // Object lifelineElement = provider.getLifeline(target); // Lifeline lifeline = (Lifeline) findItem(lifelineElement); // if (lifeline == null) { // lifeline = new Lifeline(getChart()); // updateItem(lifeline, lifelineElement); // } // newTarget.setLifeline(lifeline); // } internalRefreashActivationLifeline(newTarget); } if (newMessage != null && newTarget != null) { if (!newMessage.isDisposed() && !newTarget.isDisposed()) { a.addMessage(i, newMessage, newTarget); } } } if (a.isDisposed()) { System.out.println("disposed here"); } //remove the trash for (; i < newMessages.size(); i++) { Message m = newMessages.get(i); if (!m.isDisposed()) { if (m instanceof Call) { deleteSubTree(m.getTarget()); } m.dispose(); } } if (a.isDisposed()) { System.out.println("disposed here!"); } refreshGroups(a); if (activationsToLoad.size() > 0) { callStack.addFirst(a); while (activationsToLoad.size() > 0) { internalRefreshActivation(activationsToLoad.removeFirst(), callStack); } callStack.removeFirst(); } } finally { getChart().setRedraw(true); } } /** * Checks the activation to see if it passes the filters. * @param target * @return */ private boolean selectActivation(Object target) { if (target == null) return false; ISequenceChartContentProvider provider = (ISequenceChartContentProvider) getContentProvider(); if (select(target)) { return selectLifeline(provider.getLifeline(target)); } return false; } /** * @param lifeline * @return */ private boolean selectLifeline(Object lifeline) { if (select(lifeline)) { if (getContentProvider() instanceof ISequenceContentExtension) { ISequenceContentExtension provider = (ISequenceContentExtension) getContentProvider(); Object current = lifeline; while (provider.hasContainingGroup(current)) { if (select(current)) { current = provider.getContainingGroup(current); } else { return false; } } return true; } return true; } return false; } /** * @param a */ private void internalRefreashActivationLifeline(Activation a) { ISequenceChartContentProvider provider = (ISequenceChartContentProvider) getContentProvider(); //update the lifeline Object llElement = provider.getLifeline(a.getData()); //make sure that the lifeline should be shown. if (!select(llElement)) { //delete this subtree, and all of the subtrees on the lifeline Widget w = findItem(llElement); //deleting all of the subtrees on the lifeline will result //in the deletion of the lifeline itself. if (!a.isDisposed()) { deleteSubTree(a); } if (w instanceof Lifeline && !w.isDisposed()) { Lifeline ll = (Lifeline) w; for (Activation a2 : ll.getAllActivations()) { if (!a2.isDisposed()) { deleteSubTree(a2); } } } return; } Lifeline ll = (Lifeline) findItem(llElement); if (ll == null) { ll = new Lifeline(getChart()); updateItem(ll, llElement); } if (a.getLifeline() != ll) { if (a.getLifeline() != null) { a.setLifeline(null); if (ll.getAllActivations().length == 0) { //dispose the lifeline disassociate(a.getLifeline()); a.getLifeline().dispose(); } } a.setLifeline(ll); } if (provider instanceof ISequenceContentExtension) { internalRefreshLifelineGroup(a.getLifeline()); } } /** * Walks up the parent three of the given lifeline to refresh its groups * @param lifeline */ private void internalRefreshLifelineGroup(Lifeline lifeline) { ISequenceContentExtension provider = (ISequenceContentExtension) getContentProvider(); Object element = lifeline.getData(); if (provider.hasContainingGroup(element)) { Object parentElement = provider.getContainingGroup(element); if (parentElement != null) { if (lifeline.getParent() == null || !lifeline.getParent().getData().equals(parentElement)) { //create a new lifeline group for this lifeline Lifeline oldParent = lifeline.getParent(); Lifeline group = (Lifeline) findItem(parentElement); if (group == null) { group = new Lifeline(getChart()); } group.addChild(lifeline); if (oldParent != null && oldParent.getAllActivations().length == 0) { //delete the group disassociate(group); group.dispose(); } updateItem(group, parentElement); internalRefreshLifelineGroup(group); } } } } /** * Disposes the given activation, all of its messages, and all of the activations called by it. * If the activation's lifeline is empty after this operation, the lifeline is disposed as well. * @param a */ private void deleteSubTree(Activation a) { if (a == null || a.isDisposed()) return; Lifeline ll = a.getLifeline(); Message[] messages = a.getMessages(); for (Message m : messages) { if (m instanceof Call) { deleteSubTree(m.getTarget()); } if (!m.isDisposed()) { disassociate(m); m.dispose(); } } //delete the calling message if (a.getSourceCall() != null && !a.getSourceCall().isDisposed()) { disassociate(a.getSourceCall()); a.getSourceCall().dispose(); } disassociate(a); a.dispose(); if (ll != null && !ll.isDisposed() && ll.getAllActivations().length == 0) { disassociate(ll); ll.dispose(); } } /** * Uses the IMessageGrouper set on this viewer to refresh the groups on the given activation. * It is expected that the activation is refreshed and has all of its messages * set. * @param a the activation to refresh. */ private void refreshGroups(Activation a) { try { getChart().setRedraw(false); IMessageGrouper grouper = getMessageGrouper(); if (grouper == null) { return; } Object element = a.getData(); Message[] messages = a.getMessages(); Object[] messageElements = new Object[messages.length]; for (int i = 0; i < messages.length; i++) { messageElements[i] = messages[i].getData(); } MessageGroup[] oldGroups = a.getMessageGroups(); HashMap<IMessageGrouping, MessageGroup> groupingWidgetMap = new HashMap<IMessageGrouping, MessageGroup>(); for (int i = 0; i < oldGroups.length; i++) { Object groupData = oldGroups[i].getData(); if (!(groupData instanceof IMessageGrouping)) { throw new IllegalArgumentException("Message grouping set outside of the viewer"); } else { groupingWidgetMap.put((IMessageGrouping)groupData, oldGroups[i]); } } IMessageGrouping[] newGroupings = getMessageGrouper().calculateGroups(this,element,messageElements); for (int i = 0; i < newGroupings.length; i++) { IMessageGrouping newGrouping = newGroupings[i]; MessageGroup groupWidget = groupingWidgetMap.get(newGrouping); if (groupWidget == null) { groupWidget = new MessageGroup(getChart()); associate(newGrouping, groupWidget); } else { groupingWidgetMap.remove(newGrouping); } groupWidget.setRange(a, newGrouping.getOffset(), newGrouping.getLength()); groupWidget.setText(newGrouping.getName()); groupWidget.setForeground(newGrouping.getForeground()); groupWidget.setBackground(newGrouping.getBackground()); } //delete all the unused groups for (Iterator<IMessageGrouping> it = groupingWidgetMap.keySet().iterator(); it.hasNext();) { IMessageGrouping key = it.next(); MessageGroup widget = groupingWidgetMap.get(key); disassociate(widget); widget.dispose(); } } finally { chart.setRedraw(true); } } /** * Returns the message grouper set on this viewer. * @return the message grouper set on this viewer. */ public IMessageGrouper getMessageGrouper() { return grouper; } /** * Sets the message grouper on this viewer. To save on execution time, it is * suggested that the message grouper be set before the input is set on the viewer. * Setting the message grouper will force a full refresh of all of the activation * groups currently on the chart. The earlier the grouper is set, the quicker this * operation will be. * @param grouper the new grouper. */ public void setMessageGrouper(IMessageGrouper grouper) { if (this.grouper != null) { this.grouper.dispose(); } this.grouper = grouper; if (getInput() != null) { //need to refresh. try { getChart().setRedraw(false); UMLItem[] items = getChart().getItems(); for (int i = 0; i < items.length; i++) { if (items[i] instanceof Activation) { refreshGroups((Activation)items[i]); } } } finally { getChart().setRedraw(true); } } } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#reveal(java.lang.Object) */ @Override public void reveal(final Object element) { //put it on a work queue to ensure that any layout, etc. is done first. getJobProcessor().runInUIThread(new AbstractSimpleProgressRunnable() { @Override protected void doRunInUIThread(SimpleProgressMonitor monitor) throws InvocationTargetException { monitor.beginTask("Revealing item", IUIProgressService.UNKNOWN_WORK); Widget item = findItem(element); if (item instanceof UMLItem) { getChart().reveal((UMLItem)item); } monitor.done(); } }, getChart().getDisplay(), false); } /** * Checks to see if the given element currently exists and is visible in * the viewer. * @param element the element to check * @return true if the given element exists and is visible in the viewer. */ public boolean isVisible(Object element) { Widget item = findItem(element); if (item instanceof UMLItem) { UMLItem umlItem = (UMLItem) item; return umlItem.isVisible() && !umlItem.isHidden(); } return false; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, boolean) */ @SuppressWarnings("unchecked") @Override protected void setSelectionToWidget(List l, boolean reveal) { List<UMLItem> items = new ArrayList<UMLItem>(); for (Object o : l) { Widget[] widgets = findItems(o); for (Widget widget : widgets) { if (widget instanceof UMLItem) { items.add((UMLItem)widget); } } } if (reveal && l.size() > 0) { getChart().reveal(items.get(0)); } getChart().setSelection(items.toArray(new UMLItem[items.size()])); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.Viewer#getControl() */ @Override public Control getControl() { return chart; } public UMLSequenceChart getChart() { return chart; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#assertContentProviderType(org.eclipse.jface.viewers.IContentProvider) */ @Override protected void assertContentProviderType(IContentProvider provider) { assert(provider instanceof ISequenceChartContentProvider); } private boolean isLazyLoading() { return (chart.getStyle() & SWT.VIRTUAL) != 0; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object) */ @Override protected void inputChanged(Object input, Object oldInput) { refresh(); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#handleDispose(org.eclipse.swt.events.DisposeEvent) */ @Override protected void handleDispose(DisposeEvent event) { super.handleDispose(event); if (getMessageGrouper() != null) { getMessageGrouper().dispose(); } sequenceListeners.clear(); } /** * Returns an array of elements that passed the viewer's filters. * @param elements the elements to filter. * @return an array of elements that passed the viewer's filters. */ protected Object[] filter(Object[] elements) { LinkedList<Object> filtered = new LinkedList<Object>(); for (Object e : elements) { if (select(e)) { filtered.add(e); } } return filtered.toArray(); } /** * Checks the filters to see if the given object has passed. Returns true if there * are no filters. * @param e the element to check * @return true if there are no filters on this viewer, or the element passes the filters. */ private boolean select(Object e) { ViewerFilter[] filters = getFilters(); Object root = getRoot(); for (int j = 0; j < filters.length; j++) { if (!((ViewerFilter) filters[j]).select(this, root, e)) { return false; } } return true; } /** * Sets the expanded states for all items that represent the given element to * the expanded state (if they are expandable). This method is effective for * activations returned by the content provider and message groups returned by * the supplied message grouper. * @param element the element to set the expanded state to * @param expanded the new expanded state * @see setMessageGrouper */ public void setExpanded(Object element, boolean expanded) { Widget[] items = findItems(element); try { chart.setRedraw(false); for (Widget item : items) { if (item instanceof IExpandableItem) { ((IExpandableItem)item).setExpanded(expanded); } } } finally { chart.setRedraw(true); } } /** * Returns the element found at the given point in the chart. * @param x the x location of the point. * @param y the y location of the point. * @return the element found at the point, or null if none. */ public Object elementAt(int x, int y) { Widget w = chart.getItemAt(x,y); if (w != null && !w.isDisposed()) { return w.getData(); } return null; } /** * Sets the focus to the activation represented by the given object. If the object * doesn't represent an activation, this method does nothing. * @param activation the element to focus on. */ public void setRootActivation(Object activation) { if (activation == null) return; Widget[] items = findItems(activation); for (Widget item : items) { if (item instanceof Activation) { if (item.isDisposed()) { unmapElement(activation, item); } else { getChart().setRootActivation((Activation)item); return; } } } } /** * Returns the element representing the root activation, or null if not present. * @return the element representing the root activation, or null if not present. */ public Object getRootActivation() { Activation root = getChart().getRootActivation(); if (root != null && !root.isDisposed()) { return root.getData(); } return null; } /** * Adds the given listener to the list of sequence listeners. * @param listener the listener to add. */ public void addSequenceListener(ISequenceViewerListener listener) { sequenceListeners.add(listener); } /** * Removes the given listener from the list of sequence listeners. * @param listener the listener to remove. */ public void removeSequenceListener(ISequenceViewerListener listener) { sequenceListeners.remove(listener); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#setContentProvider(org.eclipse.jface.viewers.IContentProvider) */ @Override public void setContentProvider(IContentProvider provider) { super.setContentProvider(provider); if (!(provider instanceof ISequenceContentExtension)) { getChart().setLifelineGroupsVisible(false); } else { getChart().setLifelineGroupsVisible(true); } } /** * Expands all activations in the chart. Updates on the chart will * be dispabled during the expansion, so that the user isn't bothered by unnessary re-layouts. * @param enforceLimit set to true if there should be a limit to the amount of time that is used to expand * children. This is highly recommended, as some sequence charts may have infinite recursions and not * enforcing a limit may never terminate. */ public void expandAllActivations(boolean enforceLimit) { expandActivationsUnder(((ISequenceChartContentProvider)getContentProvider()).getElements(getInput())[0], enforceLimit); } /** * Expands the given activations, and all of its sub-activations. * @param activationElement the activation to expand. * @param enforceLimit set to true if there should be a limit to the amount of time that is used to expand * children. This is highly recommended, as some sequence charts may have infinite recursions and not * enforcing a limit may never terminate. */ public void expandActivationsUnder(final Object activationElement, boolean enforceLimit) { getJobProcessor().runInUIThread( new ExpandActivationsUnderRunnable(activationElement, enforceLimit), getChart().getDisplay(), true ); } /** * Expands all of the activations within the given lifeline. Only activations that are currently * reachable from the root are expanded. No attempt is made to search the contents for new * activations that may be on the lifeline given different expansion states. Such an effort may * produce an infinite loop, and so is avoided. If #enforceLimit is set to true, then the process * will warn the user after a period of time, or a large number of new activations have been reached. * It is recommended that limit is always set to true in order to avoid large graphs. * @param lifelineElement the lifeline to expand in. * @param enforceLimit set to true to warn users when the chart might be getting too large to display efficiently. */ public void expandActivationsIn(Object lifelineElement, boolean enforceLimit) { getJobProcessor().runInUIThread( new ExpandActivationsInRunnable(lifelineElement, enforceLimit), getChart().getDisplay(), true ); } /** * Collapses all of the activations within the givin lifeline. Only activations that are currently * reachable from the root are collapsed. No attempt is made to search the contents for * activations that may be reacable given different expansion states. * @param lifelineElement */ public void collapseActivationsIn(Object lifelineElement) { getJobProcessor().runInUIThread( new CollapseActivationsInRunnable(lifelineElement), getChart().getDisplay(), true ); } /** * Collapses all activations in the chart. Updates on the chart will * be disabled during the collapse, so that the user isn't bothered by unnecessary re-layouts. * */ public void collapseAllActivations() { collapseActivationsUnder(((ISequenceChartContentProvider)getContentProvider()).getElements(getInput())[0]); } /** * Collapses the given activations, and all of its sub-activations. * @param activationElement the activation to collapse. */ public void collapseActivationsUnder(final Object activationElement) { getJobProcessor().runInUIThread(new CollapseActivationsUnderRunnable( activationElement), getChart().getDisplay(), true); } /** * Returns the expanded state of the given element. Always returns false for elements that * are not expandable (eg. messages). * @param element the expanded state of the given element. Always returns false for elements that * are not expandable (eg. messages). * @return */ public boolean getExpanded(Object element) { Widget[] items = findItems(element); if (items != null && items.length > 0) { //just choose the first item. There is really no better way to do it. if (!items[0].isDisposed() && items[0] instanceof IExpandableItem) { return ((IExpandableItem)items[0]).isExpanded(); } } return false; } /** * Sets the expansion state for the given message grouping. If the grouping doesn't currently exist * int the chart, no state is changed. * @param group the grouping to set. * @param expanded the new expanded state */ public void setGroupingExpanded(IMessageGrouping grouping, boolean expanded) { Widget[] items = findItems(grouping); if (items != null) { for (Widget item : items) { if (item instanceof MessageGroup) { ((MessageGroup)item).setExpanded(expanded); } } } } /** * Returns the filtered message groupings for the given activation. If the activation element * doesn't currently exist in the chart, the grouper supplied to the viewer is queried to see * what the groupings would be if the activation element did exist in the chart. For this reason, * clients should not expect that the groupings returned will have object identity with the groupings * that may or may not exist in the chart. * @param activationElement the element to check. * @return the groupings on the element. */ public IMessageGrouping[] getMessageGroupingsFor(Object activationElement) { Widget[] items = findItems(activationElement); List<IMessageGrouping> groupings = null; if (items != null && items.length > 0) { for (Widget item : items) { if (item instanceof Activation) { groupings = new LinkedList<IMessageGrouping>(); for (MessageGroup group : ((Activation)item).getMessageGroups()) { groupings.add((IMessageGrouping)group.getData()); } break; } } } if (groupings == null) { IMessageGrouper grouper = getMessageGrouper(); Object[] messages = ((ISequenceChartContentProvider)getContentProvider()).getMessages(activationElement); if (messages != null) { messages = filter(messages); return grouper.calculateGroups(this, activationElement, messages); } } return new IMessageGrouping[0]; } private UIJobProcessor getJobProcessor() { return (UIJobProcessor) chart.getData("jobs"); } public void updateAll() { getChart().setRedraw(false); UMLItem[] items = getChart().getItems(); for (UMLItem item : items) { updateItem(item, item.getData()); } getChart().setRedraw(true); } }