AbstractDependencyVisualizationView.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file). 
 * All Rights Reserved.
 * 
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 * 
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 * 
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ui.dependencyvisualization;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.IFigure;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.zest.core.viewers.EntityConnectionData;
import org.eclipse.zest.core.viewers.GraphViewer;
import org.eclipse.zest.core.viewers.IConnectionStyleProvider;
import org.eclipse.zest.core.viewers.IEntityStyleProvider;
import org.eclipse.zest.core.viewers.IGraphEntityContentProvider;
import org.eclipse.zest.core.widgets.ZestStyles;
import org.eclipse.zest.layouts.LayoutAlgorithm;
import org.eclipse.zest.layouts.LayoutStyles;
import org.eclipse.zest.layouts.algorithms.CompositeLayoutAlgorithm;
import org.eclipse.zest.layouts.algorithms.DirectedGraphLayoutAlgorithm;
import org.eclipse.zest.layouts.algorithms.HorizontalShift;
import org.osate.ui.OsateUiPlugin;

/**
 * This class contains the code common to {@link ProjectDependencyVisualizationView} and
 * {@link ModelUnitDependencyVisualizationView}. Each view has a graph, control group, context menu, and refresh
 * button. The input object for the graph is an {@link IVisualizationInput}. This is the model object which handles all
 * the non-UI information necessary to build a directed graph.
 * 
 * The graph has a concept of which elements are in the scope and which are out of scope. The scope is specified when
 * setting the input to the graph. For example, a scope could be all projects in a working set. When building the
 * graph, the elements in the scope serve as the starting point and all dependencies are calculated from the scope.
 * This means that the final graph may contain elements that are outside of the scope. The elements in the scope have a
 * light blue background while the elements outside of the scope have a white and gray background. This is handled by
 * the graph's label provider which queries {@link IVisualizationInput#isInScope(Object)}.
 * 
 * If the user selects a node, that node will be highlighted yellow and all directly connected nodes will be
 * highlighted orange. The direct connection lines will also be highlighted red and become dashed. This is handled by
 * the graph's label provider which queries the graph selection and
 * {@link IVisualizationInput#getConnectedToBothDirections(Object)}.
 */
abstract class AbstractDependencyVisualizationView extends ViewPart {
	private IVisualizationInput input;

	private GraphViewer graph;

	@Override
	public void createPartControl(Composite parent) {
		parent.setLayout(new GridLayout(2, false));

		graph = new GraphViewer(parent, SWT.NONE);
		graph.getGraphControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		graph.setContentProvider(new IGraphEntityContentProvider() {
			@Override
			public Object[] getElements(Object inputElement) {
				return input.getGraphElements();
			}

			@Override
			public Object[] getConnectedTo(Object entity) {
				return input.getConnectedTo(entity);
			}
		});
		graph.setLabelProvider(new VisualizationLabelProvider());
		graph.setLayoutAlgorithm(new CompositeLayoutAlgorithm(
				new LayoutAlgorithm[] { new DirectedGraphLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING),
						new HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING) }));

		MenuManager menuManager = new MenuManager();
		menuManager.setRemoveAllWhenShown(true);
		menuManager.addMenuListener(this::menuAboutToShow);
		graph.getGraphControl().setMenu(menuManager.createContextMenu(graph.getGraphControl()));

		graph.addSelectionChangedListener(event -> {
			graph.update(graph.getNodeElements(), null);
			graph.update(graph.getConnectionElements(), null);
		});

		Group controlGroup = new Group(parent, SWT.SHADOW_NONE);
		controlGroup.setText("Graph Scope:");
		controlGroup.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
		fillControlComposite(controlGroup);

		IAction refreshAction = new Action("Refresh", IAction.AS_PUSH_BUTTON) {
			@Override
			public void run() {
				refresh();
			}
		};
		refreshAction.setImageDescriptor(OsateUiPlugin.getImageDescriptor("icons/refresh.png"));
		getViewSite().getActionBars().getToolBarManager().add(refreshAction);

		setScopeToWorkspace();
	}

	@Override
	public void setFocus() {
		graph.getGraphControl().setFocus();
	}

	/**
	 * This is called when the user opens the graph's context menu. Subclasses should fill the manager with
	 * appropriate actions.
	 */
	protected abstract void menuAboutToShow(IMenuManager manager);

	/**
	 * Subclasses should fill the control composite with radio buttons and combo boxes for controlling the scope of the
	 * graph.
	 */
	protected abstract void fillControlComposite(Composite parent);

	/**
	 * Called by the graph's label provider.
	 */
	protected abstract Image getImage(Object element);

	/**
	 * Called by the graph's label provider.
	 */
	protected abstract String getText(Object element);

	/**
	 * When called, subclasses should refresh the graph input based on the current scope.
	 */
	protected abstract void refresh();

	/**
	 * Sets the initial scope for the graph.
	 */
	protected abstract void setScopeToWorkspace();

	protected IStructuredSelection getGraphSelection() {
		return graph.getStructuredSelection();
	}

	/**
	 * Call to update the content of the graph.
	 */
	protected void setInput(IVisualizationInput input) {
		this.input = input;
		graph.setInput(input);
	}

	private class VisualizationLabelProvider extends LabelProvider
			implements IEntityStyleProvider, IConnectionStyleProvider {

		@Override
		public Image getImage(Object element) {
			return AbstractDependencyVisualizationView.this.getImage(element);
		}

		@Override
		public String getText(Object element) {
			return AbstractDependencyVisualizationView.this.getText(element);
		}

		@Override
		public Color getNodeHighlightColor(Object entity) {
			return null;
		}

		@Override
		public Color getBorderColor(Object entity) {
			return null;
		}

		@Override
		public Color getBorderHighlightColor(Object entity) {
			return null;
		}

		@Override
		public int getBorderWidth(Object entity) {
			return -1;
		}

		@Override
		public Color getBackgroundColour(Object entity) {
			Stream<?> selected = graph.getStructuredSelection().toList().stream();
			Set<?> connectedToSelected = selected.flatMap(input::getConnectedToBothDirections)
					.collect(Collectors.toSet());
			if (connectedToSelected.contains(entity)) {
				return ColorConstants.orange;
			} else if (!input.isInScope(entity)) {
				return ColorConstants.white;
			} else {
				return graph.getGraphControl().LIGHT_BLUE;
			}
		}

		@Override
		public Color getForegroundColour(Object entity) {
			return null;
		}

		@Override
		public IFigure getTooltip(Object entity) {
			return null;
		}

		@Override
		public boolean fisheyeNode(Object entity) {
			return false;
		}

		@Override
		public int getConnectionStyle(Object rel) {
			if (isEndPointSelected(rel)) {
				return ZestStyles.CONNECTIONS_DIRECTED | ZestStyles.CONNECTIONS_DASH;
			} else {
				return ZestStyles.CONNECTIONS_DIRECTED;
			}
		}

		@Override
		public Color getColor(Object rel) {
			if (isEndPointSelected(rel)) {
				return ColorConstants.red;
			} else {
				return ColorConstants.lightGray;
			}
		}

		@Override
		public Color getHighlightColor(Object rel) {
			return null;
		}

		@Override
		public int getLineWidth(Object rel) {
			if (isEndPointSelected(rel)) {
				return 3;
			} else {
				return 1;
			}
		}

		private boolean isEndPointSelected(Object rel) {
			EntityConnectionData connection = (EntityConnectionData) rel;
			List<?> selection = graph.getStructuredSelection().toList();
			return selection.contains(connection.source) || selection.contains(connection.dest);
		}
	}
}