NewDiagramWizard.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.ge.aadl2.ui.internal.wizards;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.INewWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.osate.aadl2.Aadl2Package;
import org.osate.aadl2.Element;
import org.osate.ge.DiagramType;
import org.osate.ge.aadl2.ui.AadlModelAccessUtil;
import org.osate.ge.aadl2.ui.internal.dialogs.ElementLabelProvider;
import org.osate.ge.internal.DiagramTypeProvider;
import org.osate.ge.internal.services.DiagramService;
import org.osate.ge.internal.ui.dialogs.CreateDiagramComposite;
import org.osate.ge.internal.ui.dialogs.DefaultCreateDiagramModel;
import org.osate.ge.internal.ui.util.EditorUtil;
import org.osate.ge.internal.util.AgeEmfUtil;
import org.osgi.framework.FrameworkUtil;

public class NewDiagramWizard extends Wizard implements INewWizard {
	// What type of context will be used for the new diagram
	enum ContextType {
		NEW_PACKAGE, EXISTING_PACKAGE, EXISTING_CLASSIFIER, CONTEXTLESS,
	}

	/**
	 * Page for selecting the type of context to be selected.
	 */
	private static class SelectContextTypePage extends WizardPage {
		private Composite container;
		private final List<Button> selectionBtns = new ArrayList<>();
		private final SelectionListener updateOnSelect = new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				update();
			}
		};

		SelectContextTypePage() {
			super("Select Context Type");
			setTitle("Select Context Type");
			setDescription("Select Context Type");
		}

		@Override
		public void createControl(final Composite parent) {
			container = new Composite(parent, SWT.NONE);
			final GridLayout layout = new GridLayout();
			container.setLayout(layout);
			layout.numColumns = 1;

			addButton("New Package", ContextType.NEW_PACKAGE);
			addButton("Existing Package", ContextType.EXISTING_PACKAGE);
			addButton("Existing Classifier", ContextType.EXISTING_CLASSIFIER);
			addButton("Contextless", ContextType.CONTEXTLESS);

			setControl(container);

			update();
		}

		private void addButton(final String label, final ContextType contextType) {
			final Button btn = new Button(container, SWT.RADIO);
			btn.setText(label);
			btn.setData(contextType);
			btn.addSelectionListener(updateOnSelect);
			selectionBtns.add(btn);
		}

		private void update() {
			setPageComplete(getContextType() != null);
			setDescription(getDescription(getContextType()));
		}

		private static String getDescription(final ContextType type) {
			if (type == null) {
				return "Create a new AADL diagram.";
			}

			switch (type) {
			case NEW_PACKAGE:
				return "Select Finish to open the AADL Package wizard.";

			case CONTEXTLESS:
				return "Create a new diagram which is not associated with a model element.";

			case EXISTING_CLASSIFIER:
				return "Create a diagram which uses an existing classifier as its context.";

			case EXISTING_PACKAGE:
				return "Create a diagram which uses an existing package as its context.";
			}

			throw new RuntimeException("Unhandled case: " + type);
		}

		private ContextType getContextType() {
			for (final Button selectionBtn : selectionBtns) {
				if (selectionBtn.getSelection()) {
					return (ContextType) selectionBtn.getData();
				}
			}

			return null;
		}
	}

	/**
	 * Page for selecting the project in which to create the diagram.
	 */
	private static class SelectProjectPage extends WizardPage {
		private Composite container;
		private TreeViewer projectViewer;

		SelectProjectPage() {
			super("Select Project");
			setTitle("Select Project");
			setDescription("Select the project in which to create the diagram.");
		}

		@Override
		public void createControl(final Composite parent) {
			container = new Composite(parent, SWT.NONE);
			final GridLayout layout = new GridLayout();
			container.setLayout(layout);
			layout.numColumns = 1;

			projectViewer = new TreeViewer(container, SWT.SINGLE | SWT.BORDER);
			GridDataFactory.fillDefaults().grab(true, true).applyTo(projectViewer.getControl());
			projectViewer.setContentProvider(new WorkbenchContentProvider() {
				@Override
				public Object[] getChildren(Object element) {
					return Arrays.stream(super.getChildren(element)).filter(r -> r instanceof IProject)
							.map(r -> (IProject) r).filter(p -> p.isOpen()).toArray();
				}
			});
			projectViewer.setLabelProvider(new WorkbenchLabelProvider());
			projectViewer.setComparator(new ViewerComparator());
			projectViewer.addSelectionChangedListener(event -> update());
			projectViewer.setInput(ResourcesPlugin.getWorkspace());

			setControl(container);

			update();
		}

		private void update() {
			setPageComplete(getProject() != null);
		}

		public IProject getProject() {
			return (IProject) ((StructuredSelection) projectViewer.getSelection()).getFirstElement();
		}

		public void setProject(final IProject value) {
			projectViewer.setSelection(value == null ? StructuredSelection.EMPTY : new StructuredSelection(value));
			update();
		}
	}

	/**
	 * Page for selecting the context for the diagram
	 */
	private static class SelectContextPage extends WizardPage {
		private Composite container;
		private ListViewer contextViewer;
		private IProject project;

		SelectContextPage() {
			super("Select Context");
			refresh(null, null);
		}

		@Override
		public void createControl(final Composite parent) {
			container = new Composite(parent, SWT.NONE);
			final GridLayout layout = new GridLayout();
			container.setLayout(layout);
			layout.numColumns = 1;

			contextViewer = new ListViewer(container, SWT.SINGLE | SWT.BORDER | SWT.V_SCROLL);
			GridDataFactory.fillDefaults().grab(true, true).applyTo(contextViewer.getControl());
			contextViewer.setContentProvider(new ArrayContentProvider());
			contextViewer.setLabelProvider(new ElementLabelProvider());
			contextViewer.setComparator(new ViewerComparator());
			contextViewer.addSelectionChangedListener(event -> update());

			setControl(container);
			setPageComplete(false);
			update();
		}

		private void update() {
			setPageComplete(getContext() != null);
		}

		public EObject getContext() {
			if (project == null) {
				return null;
			}

			final IEObjectDescription contextDescription = (IEObjectDescription) ((StructuredSelection) contextViewer
					.getSelection()).getFirstElement();

			if (contextDescription == null) {
				return null;
			}

			// Get a live resource set and use it to load the actual context business object.
			final ResourceSet liveResourceSet = AadlModelAccessUtil.getLiveResourceSet(project);

			return AgeEmfUtil.resolveOrNull(contextDescription, Element.class, liveResourceSet);
		}

		/**
		 * Refreshes the page based on an updated project and context type
		 * @param project
		 * @param contextType
		 */
		public void refresh(final IProject project, final ContextType contextType) {
			setTitle(determineTitle(contextType));
			setDescription(determineDescription(contextType));

			this.project = project;

			if (contextViewer != null) {
				// Determine the EClass of the context
				final EClass contextEclass;
				if (contextType == ContextType.EXISTING_PACKAGE) {
					contextEclass = Aadl2Package.eINSTANCE.getAadlPackage();
				} else if (contextType == ContextType.EXISTING_CLASSIFIER) {
					contextEclass = Aadl2Package.eINSTANCE.getClassifier();
				} else {
					contextEclass = null;
				}

				// Update the context viewer with valid contexts
				if (contextEclass == null || project == null) {
					contextViewer.setInput(Collections.emptyList());
				} else {
					contextViewer.setInput(AadlModelAccessUtil.getContainedEObjectsByType(project, contextEclass));
				}
			}
		}

		private static String determineTitle(final ContextType contextType) {
			if (contextType == ContextType.EXISTING_PACKAGE) {
				return "Select Package";
			} else if (contextType == ContextType.EXISTING_CLASSIFIER) {
				return "Select Classifier";
			} else {
				return "Select Context";
			}
		}

		private static String determineDescription(final ContextType contextType) {
			if (contextType == ContextType.EXISTING_PACKAGE) {
				return "Select a package to use as the context for the new diagram.";
			} else if (contextType == ContextType.EXISTING_CLASSIFIER) {
				return "Select a classifier to use as the context for the new diagram.";
			} else {
				return "Select a context for the new diagram.";
			}
		}
	}

	/**
	 * Page for selecting the name and type for the diagram.
	 */
	private static class SelectNameAndTypePage extends WizardPage {
		private CreateDiagramComposite<DiagramType> composite;
		private final DiagramTypeProvider diagramTypeProvider;
		private IProject project;
		private Object context;

		SelectNameAndTypePage(final DiagramTypeProvider diagramTypeProvider) {
			super("Select Name and Type");
			this.diagramTypeProvider = Objects.requireNonNull(diagramTypeProvider,
					"diagramTypeProvider must not be null");

			setTitle("Select Diagram Name and Type");
			setDescription("Select Finish to create the diagram.");
		}

		@Override
		public void createControl(final Composite parent) {
			composite = new CreateDiagramComposite<>(parent, createModel());
			setControl(composite);
			setPageComplete(false);

			composite.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					setErrorMessage(composite.getErrorMessage());
					setPageComplete(composite.getErrorMessage() == null);
				}
			});
		}

		public CreateDiagramComposite.Value<DiagramType> getValue() {
			return composite.getValue();
		}

		public void setProject(final IProject project) {
			this.project = project;
			this.composite.setModel(createModel());
		}

		public void setContext(final Object context) {
			this.context = context;
			this.composite.setModel(createModel());
		}

		// Create a new model based on the current state
		private DefaultCreateDiagramModel createModel() {
			return new DefaultCreateDiagramModel(diagramTypeProvider, project, context);
		}
	}

	private final DiagramService diagramService;
	private IProject initialProject = null;
	private SelectContextTypePage contextTypePage = new SelectContextTypePage();
	private SelectProjectPage selectProjectPage = new SelectProjectPage();
	private SelectContextPage contextPage = new SelectContextPage();
	private SelectNameAndTypePage diagramNameAndTypePage;

	public NewDiagramWizard(final DiagramTypeProvider diagramTypeProvider, final DiagramService diagramService) {
		setWindowTitle("New AADL Diagram");
		this.diagramService = Objects.requireNonNull(diagramService, "diagramService must not be null");
		diagramNameAndTypePage = new SelectNameAndTypePage(diagramTypeProvider);
	}

	@Override
	public void init(final IWorkbench workbench, final IStructuredSelection selection) {
		if (selection.getFirstElement() instanceof IResource) {
			initialProject = ((IResource) selection.getFirstElement()).getProject();
		}
	}

	@Override
	public void addPages() {
		addPage(contextTypePage);
		addPage(selectProjectPage);
		addPage(contextPage);
		addPage(diagramNameAndTypePage);
	}

	@Override
	public IWizardPage getNextPage(final IWizardPage currentPage) {
		if (currentPage == contextTypePage) {
			if (contextTypePage.getContextType() == ContextType.NEW_PACKAGE) {
				return null;
			}

			// Update the select project page's project. This is not done during initialization because the method must be called after the page itself is
			// initialized.
			selectProjectPage.setProject(initialProject);
		}

		if (currentPage == selectProjectPage) {
			diagramNameAndTypePage.setProject(selectProjectPage.getProject());
			contextPage.refresh(selectProjectPage.getProject(), contextTypePage.getContextType());

			if (contextTypePage.getContextType() == ContextType.CONTEXTLESS) {
				diagramNameAndTypePage.setContext(null);
				return diagramNameAndTypePage;
			}
		}

		if (currentPage == contextPage) {
			diagramNameAndTypePage.setContext(contextPage.getContext());
		}

		return super.getNextPage(currentPage);
	}

	@Override
	public boolean canFinish() {
		if (contextTypePage.getContextType() == ContextType.NEW_PACKAGE) {
			return true;
		}

		// Ignore context page is contextless is selected.
		if (contextTypePage.getContextType() == ContextType.CONTEXTLESS) {
			return selectProjectPage.isPageComplete() && diagramNameAndTypePage.isPageComplete();
		}

		return super.canFinish();
	}

	@Override
	public boolean performFinish() {
		try {
			if (contextTypePage.getContextType() == ContextType.NEW_PACKAGE) {
				// Use async exec so the current wizard will be closed before the new wizard opens.
				Display.getDefault().asyncExec(() -> {
					// Ideally, this would open the wizard and select the option to open it in the diagram editor. The wizard class can't be used directly at
					// this time because that would require a circular dependency.
					final IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
					final IHandlerService handlerService = (IHandlerService) window.getService(IHandlerService.class);
					try {
						handlerService.executeCommand("org.osate.ui.wizards.packageWizard", null);
					} catch (final Exception e) {
						final Status status = new Status(IStatus.ERROR,
								FrameworkUtil.getBundle(getClass()).getSymbolicName(), "New Diagram Error", e);
						StatusManager.getManager().handle(status, StatusManager.LOG | StatusManager.SHOW);
						throw new RuntimeException(e);
					}
				});

				return true;
			} else {
				final Object context = getContext();
				final CreateDiagramComposite.Value<DiagramType> diagramNameAndType = diagramNameAndTypePage.getValue();
				diagramService.createDiagram(diagramNameAndType.getFile(), diagramNameAndType.getDiagramType(),
						context);

				EditorUtil.openEditor(diagramNameAndType.getFile(), false);

				return true;
			}
		} catch (final RuntimeException ex) {
			final Status status = new Status(IStatus.ERROR, FrameworkUtil.getBundle(getClass()).getSymbolicName(),
					"New Diagram Error", ex);
			StatusManager.getManager().handle(status, StatusManager.LOG | StatusManager.SHOW);
			return false;
		}
	}

	/**
	 * Helper method for getting the proper context by considering whether the context page is being used or not.
	 * @return
	 */
	private Object getContext() {
		if (contextTypePage.getContextType() == ContextType.CONTEXTLESS) {
			return null;
		} else {
			return contextPage.getContext();
		}
	}

}