AppearancePropertySection.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.internal.ui.properties;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.DeviceResourceManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.IFilter;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.ColorDialog;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
import org.eclipse.ui.model.BaseWorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import org.osate.ge.StringUtil;
import org.osate.ge.graphics.Style;
import org.osate.ge.graphics.StyleBuilder;
import org.osate.ge.graphics.internal.AgeConnection;
import org.osate.ge.graphics.internal.Label;
import org.osate.ge.internal.Activator;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.services.ProjectProvider;
import org.osate.ge.internal.ui.editor.InternalDiagramEditor;
import org.osate.ge.internal.ui.util.InternalPropertySectionUtil;
import org.osate.ge.internal.ui.util.UiUtil;
import org.osate.ge.swt.SwtUtil;

import com.google.common.collect.Lists;

/**
 * The appearance property section. Allows customizing the style of the selected objects what can be adapted to {@link DiagramElement}
 *
 */
public class AppearancePropertySection extends AbstractPropertySection {
	/**
	 * Filter which determines if the property section is compatible with an object.
	 */
	public static class SelectionFilter implements IFilter {
		@Override
		public boolean select(final Object o) {
			return Adapters.adapt(o, DiagramElement.class) != null;
		}
	}

	@Override
	public void createControls(final Composite parent, final TabbedPropertySheetPage aTabbedPropertySheetPage) {
		super.createControls(parent, aTabbedPropertySheetPage);
		resourceMgr = new DeviceResourceManager(Display.getCurrent());
		final Composite containerComposite = getWidgetFactory().createFlatFormComposite(parent);
		createComboViewerSection(containerComposite);
		createImageSection(containerComposite);
		createButtonSection(containerComposite);
		InternalPropertySectionUtil.setPropertiesHelp(aTabbedPropertySheetPage.getControl());
	}

	@Override
	public void dispose() {
		ageDiagram = null;
		selectedDiagramElements.clear();

		if (resourceMgr != null) {
			resourceMgr.dispose();
		}
	}

	private void createComboViewerSection(final Composite parent) {
		FormData fd;

		primaryLabelVisibleLabel = createLabel(parent, "Label Visibility:");
		fd = new FormData();
		fd.top = new FormAttachment(5, 0);
		fd.left = new FormAttachment(0, 10);
		primaryLabelVisibleLabel.setLayoutData(fd);

		primaryLabelVisibleViewer = createComboViewer(parent, LabelVisibility.values());
		fd = new FormData();
		fd.top = new FormAttachment(primaryLabelVisibleLabel, 0, SWT.TOP);
		fd.left = new FormAttachment(primaryLabelVisibleLabel, 10);
		primaryLabelVisibleViewer.getCombo().setLayoutData(fd);
		SwtUtil.setTestingId(primaryLabelVisibleViewer.getControl(), WIDGET_ID_PRIMARY_LABEL_VISIBILITY_COMBO);

		fontSizeLabel = createLabel(parent, "Font Size:");
		fd = new FormData();
		fd.top = new FormAttachment(primaryLabelVisibleLabel, 10);
		fd.left = new FormAttachment(0, 10);
		fontSizeLabel.setLayoutData(fd);

		fontSizeComboViewer = createComboViewer(parent, FontSize.values());
		fd = new FormData();
		fd.top = new FormAttachment(fontSizeLabel, 0, SWT.TOP);
		fd.left = new FormAttachment(primaryLabelVisibleViewer.getControl(), 0, SWT.LEFT);
		fontSizeComboViewer.getCombo().setLayoutData(fd);
		SwtUtil.setTestingId(fontSizeComboViewer.getControl(), WIDGET_ID_FONT_SIZE_COMBO);

		lineWidthLabel = createLabel(parent, "Line Width:");
		fd = new FormData();
		fd.top = new FormAttachment(fontSizeLabel, 10);
		fd.left = new FormAttachment(0, 10);
		lineWidthLabel.setLayoutData(fd);

		lineWidthComboViewer = createComboViewer(parent, LineWidth.values());
		fd = new FormData();
		fd.top = new FormAttachment(lineWidthLabel, 0, SWT.TOP);
		fd.left = new FormAttachment(primaryLabelVisibleViewer.getControl(), 0, SWT.LEFT);
		lineWidthComboViewer.getCombo().setLayoutData(fd);
		SwtUtil.setTestingId(lineWidthComboViewer.getControl(), WIDGET_ID_LINE_WIDTH_COMBO);
	}

	private void createButtonSection(final Composite parent) {
		final Button outlineButton = createButton(parent, OUTLINE_ICON, "Outline Color",
				WIDGET_ID_OUTLINE_COLOR_BUTTON);
		FormData fd = new FormData();
		fd.top = new FormAttachment(imageLabel, 10);
		fd.left = new FormAttachment(0, 30);
		outlineButton.setLayoutData(fd);
		outlinePaintListener = new StylePaintListener(outlineButton,
				new StyleCommand("Set Outline", (diagramElement, sb, value) -> {
					if (supportsOutline(diagramElement)) {
						final org.osate.ge.graphics.Color outline = (org.osate.ge.graphics.Color) value;
						sb.outlineColor(outline);
					}
				}));

		final Button fontColorButton = createButton(parent, FONT_COLOR_ICON, "Font Color", WIDGET_ID_FONT_COLOR_BUTTON);
		fd = new FormData();
		fd.top = new FormAttachment(outlineButton, 0, SWT.TOP);
		fd.left = new FormAttachment(outlineButton, 0);
		fontColorButton.setLayoutData(fd);
		fontColorPaintListener = new StylePaintListener(fontColorButton,
				new StyleCommand("Set Font Color", (diagramElement, sb, value) -> {
					if (supportsFontOptions(diagramElement)) {
						final org.osate.ge.graphics.Color fontColor = (org.osate.ge.graphics.Color) value;
						sb.fontColor(fontColor);
					}
				}));

		final Button backgroundButton = createButton(parent, BACKGROUND_ICON, "Background Color",
				WIDGET_ID_BACKGROUND_COLOR_BUTTON);
		fd = new FormData();
		fd.top = new FormAttachment(fontColorButton, 0, SWT.TOP);
		fd.left = new FormAttachment(fontColorButton, 0);
		backgroundButton.setLayoutData(fd);
		backgroundPaintListener = new StylePaintListener(backgroundButton,
				new StyleCommand("Set Background", (diagramElement, sb, value) -> {
					if (supportsBackground(diagramElement)) {
						final org.osate.ge.graphics.Color background = (org.osate.ge.graphics.Color) value;
						sb.backgroundColor(background);
					}
				}));
	}

	private void createImageSection(final Composite parent) {
		// Create controls
		imageLabel = createLabel(parent, "Show as Image:");
		toggleShowImage = new Button(parent, SWT.CHECK);
		toggleShowImage.setToolTipText("Show Image");
		setImageButton = createButton(parent, IMAGE_ICON, "Set Image", WIDGET_ID_SET_IMAGE_BUTTON);

		// Set layout
		FormData fd = new FormData();
		fd.top = new FormAttachment(lineWidthLabel, 10);
		fd.left = new FormAttachment(0, 10);
		imageLabel.setLayoutData(fd);

		fd = new FormData();
		fd.top = new FormAttachment(imageLabel, 0, SWT.TOP);
		fd.left = new FormAttachment(toggleShowImage, 0);
		setImageButton.setLayoutData(fd);

		fd = new FormData();
		fd.top = new FormAttachment(setImageButton, 0, SWT.CENTER);
		fd.left = new FormAttachment(primaryLabelVisibleViewer.getControl(), 0, SWT.LEFT);
		toggleShowImage.setLayoutData(fd);

		toggleShowImage.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(final SelectionEvent e) {
				final boolean isVisible = toggleShowImage.getSelection();
				runStyleCommand(isVisible, showAsImageStyleCmd);
			}
		});

		setImageButton.addSelectionListener(imageSelectionAdapter);
	}

	// Determine if the file is an image and supported type
	private boolean isImageFile(final IFile file) {
		final String ext = file.getFileExtension();
		for (final String suf : supportedImageTypes) {
			if (suf.equalsIgnoreCase(ext)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public void setInput(final IWorkbenchPart part, final ISelection selection) {
		super.setInput(part, selection);
		selectedDiagramElements.clear();
		ageDiagram = null;

		if (selection instanceof IStructuredSelection) {
			for (final Object o : (IStructuredSelection) selection) {
				final DiagramElement diagramElement = Adapters.adapt(o, DiagramElement.class);
				if (diagramElement != null) {
					selectedDiagramElements.add(diagramElement);
				}
			}
		}
	}

	@Override
	public void refresh() {
		final RGB disableColor = lightGray.rgb;
		boolean enableFontOptions = false;
		boolean enableLineWidth = false;
		boolean enableBackground = false;
		boolean enableOutlineOption = false;
		boolean enablePrimaryLabelVisibleOption = false;
		boolean enableImage = false;
		boolean enableShowAsImage = false;
		boolean isShowAsImageEnabled = false;
		for (final DiagramElement diagramElement : selectedDiagramElements) {
			// Font options
			if (supportsFontOptions(diagramElement)) {
				enableFontOptions = true;
			}

			// Shape/Connection options
			if (supportsLineWidth(diagramElement)) {
				enableLineWidth = true;
			}

			if (supportsBackground(diagramElement)) {
				enableBackground = true;
			}

			if (supportsOutline(diagramElement)) {
				enableOutlineOption = true;
			}

			if (supportsPrimaryLabelVisible(diagramElement)) {
				enablePrimaryLabelVisibleOption = true;
			}

			// Image options
			if (DiagramElementPredicates.supportsImage(diagramElement)) {
				enableImage = true;
				final Style style = diagramElement.getStyle();
				// If any elements have an image style
				if (style.getImagePath() != null) {
					enableShowAsImage = true;

					// If any elements are images
					if (Boolean.TRUE.equals(style.getShowAsImage())) {
						isShowAsImageEnabled = true;
					}
				}
			}
		}

		// Get the diagram from the selected diagram elements
		ageDiagram = Objects.requireNonNull(UiUtil.getDiagram(selectedDiagramElements), "Unable to retrieve diagram");

		// Set image controls
		imageLabel.setEnabled(enableImage);
		setImageButton.setEnabled(enableImage);
		toggleShowImage.setEnabled(enableShowAsImage);
		toggleShowImage.setSelection(isShowAsImageEnabled);

		// Set options to match last selected element
		final DiagramElement diagramElement = selectedDiagramElements.get(selectedDiagramElements.size() - 1);
		final Style currentStyle = diagramElement.getStyle();

		final FontSize lastFontSizeSelected = FontSize.getByValue(currentStyle.getFontSize());
		final LineWidth lastLineWidthSelected = LineWidth.getByValue(currentStyle.getLineWidth());
		final LabelVisibility lastLabelVisibilitySelected = LabelVisibility
				.getByValue(currentStyle.getPrimaryLabelVisible());

		final Button backgroundButton = backgroundPaintListener.getButton();
		backgroundButton.setEnabled(enableBackground);

		final Button fontColorButton = fontColorPaintListener.getButton();
		fontColorButton.setEnabled(enableFontOptions);

		final Button outlineButton = outlinePaintListener.getButton();
		outlineButton.setEnabled(enableOutlineOption);

		final Style defaultStyle = StyleBuilder
				.create(diagramElement.getGraphicalConfiguration().getStyle(), Style.DEFAULT)
				.build();
		backgroundPaintListener.setDefaultColor(toRGB(defaultStyle.getBackgroundColor()));
		fontColorPaintListener.setDefaultColor(toRGB(defaultStyle.getFontColor()));
		outlinePaintListener.setDefaultColor(toRGB(defaultStyle.getOutlineColor()));

		final RGB background;
		if (enableBackground) {
			background = currentStyle.getBackgroundColor() == null ? null : toRGB(currentStyle.getBackgroundColor());
		} else {
			background = disableColor;
		}

		final RGB fontColor;
		if (enableFontOptions) {
			fontColor = currentStyle.getFontColor() == null ? null : toRGB(currentStyle.getFontColor());
		} else {
			fontColor = disableColor;
		}

		final RGB outline;
		if (enableOutlineOption) {
			outline = currentStyle.getOutlineColor() == null ? null : toRGB(currentStyle.getOutlineColor());
		} else {
			outline = disableColor;
		}

		// Update UI
		setComboViewersEnabled(enableFontOptions, enableLineWidth, enablePrimaryLabelVisibleOption);
		setStructuredSelection(lastFontSizeSelected, lastLineWidthSelected, lastLabelVisibilitySelected);
		setPaintListenerColors(background, fontColor, outline);
	}

	private static boolean supportsFontOptions(final DiagramElement de) {
		return de.getLabelName() != null || de.getGraphic() instanceof Label;
	}

	private static boolean supportsLineWidth(final DiagramElement de) {
		return de.getGraphic() instanceof AgeConnection;
	}

	private static boolean supportsBackground(final DiagramElement de) {
		return !(de.getGraphic() instanceof Label);
	}

	private static boolean supportsOutline(final DiagramElement de) {
		return !(de.getGraphic() instanceof Label);
	}

	private static boolean supportsPrimaryLabelVisible(final DiagramElement de) {
		return !(de.getGraphic() instanceof Label);
	}

	private static RGB toRGB(final org.osate.ge.graphics.Color color) {
		return new RGB(color.getRed(), color.getGreen(), color.getBlue());
	}

	private static ComboViewer createComboViewer(final Composite parent, final Object[] input) {
		final ComboViewer comboViewer = new ComboViewer(parent, SWT.DROP_DOWN | SWT.READ_ONLY | SWT.BORDER);
		comboViewer.setContentProvider(ArrayContentProvider.getInstance());
		comboViewer.setInput(input);
		return comboViewer;
	}

	private static org.eclipse.swt.widgets.Label createLabel(final Composite parent, final String text) {
		final org.eclipse.swt.widgets.Label label = new org.eclipse.swt.widgets.Label(parent, SWT.NONE);
		label.setText(text);
		label.setBackground(parent.getBackground());

		return label;
	}

	private static Button createButton(final Composite parent, final ImageDescriptor imageDescriptor,
			final String toolTipText, final String id) {
		final Button button = new Button(parent, SWT.PUSH);
		button.setImage(imageDescriptor.createImage());
		button.setToolTipText(toolTipText);
		SwtUtil.setTestingId(button, id);

		return button;
	}

	private void setComboViewersEnabled(final boolean showFontSize, final boolean showLineWidth,
			final boolean enablePrimaryLabelVisibleOption) {
		fontSizeLabel.setEnabled(showFontSize);
		fontSizeComboViewer.getControl().setEnabled(showFontSize);
		lineWidthLabel.setEnabled(showLineWidth);
		lineWidthComboViewer.getControl().setEnabled(showLineWidth);
		primaryLabelVisibleLabel.setEnabled(enablePrimaryLabelVisibleOption);
		primaryLabelVisibleViewer.getControl().setEnabled(enablePrimaryLabelVisibleOption);
	}

	private void setPaintListenerColors(final RGB background, final RGB fontColor, final RGB outline) {
		backgroundPaintListener.setColor(background);
		fontColorPaintListener.setColor(fontColor);
		outlinePaintListener.setColor(outline);
	}

	private void setStructuredSelection(final FontSize fontSize, final LineWidth lineWidth,
			final LabelVisibility labelVisibility) {
		fontSizeComboViewer.removeSelectionChangedListener(fontSizeSelectionListener);
		fontSizeComboViewer
		.setSelection(fontSize == null ? StructuredSelection.EMPTY : new StructuredSelection(fontSize));
		fontSizeComboViewer.addSelectionChangedListener(fontSizeSelectionListener);

		lineWidthComboViewer.removeSelectionChangedListener(lineWidthSelectionListener);
		lineWidthComboViewer
		.setSelection(lineWidth == null ? StructuredSelection.EMPTY : new StructuredSelection(lineWidth));
		lineWidthComboViewer.addSelectionChangedListener(lineWidthSelectionListener);

		primaryLabelVisibleViewer.removeSelectionChangedListener(primaryLabelVisibleSelectionListener);
		primaryLabelVisibleViewer.setSelection(
				labelVisibility == null ? StructuredSelection.EMPTY : new StructuredSelection(labelVisibility));
		primaryLabelVisibleViewer.addSelectionChangedListener(primaryLabelVisibleSelectionListener);
	}

	private class ColorSelectionListener extends SelectionAdapter {
		private ColorSelectionListener(final StylePaintListener paintListener, final StyleCommand styleCmd) {
			this.paintListener = paintListener;
			this.styleCmd = styleCmd;
		}

		private Button createColorButton(final Composite parent, final ImageDescriptor imgDescriptor) {
			final Button btn = new Button(parent, SWT.PUSH);
			btn.setImage(imgDescriptor.createImage());
			btn.addDisposeListener(e -> {
				btn.getImage().dispose();
			});
			return btn;
		}

		@Override
		public void widgetSelected(final SelectionEvent e) {
			final Shell shell = new Shell(Display.getCurrent(), SWT.NO_TRIM);
			GridLayoutFactory.fillDefaults().spacing(0, 0).numColumns(6).applyTo(shell);

			// Create preset color buttons
			for (final PresetColor pc : colors) {
				final Button btn = createColorButton(shell, pc.imageDescriptor);
				btn.addSelectionListener(new ColorSelectionAdapter(shell, paintListener, pc.rgb, styleCmd));
			}

			// Create custom color button
			final boolean hasCustomColor = customPC != null;
			final PresetColor customPresetColor = hasCustomColor ? customPC : white;
			final Button customColorBtn = createButton(shell, customPresetColor.imageDescriptor, "Custom...",
					WIDGET_ID_CUSTOM_COLOR_BUTTON);
			customColorBtn.setEnabled(hasCustomColor);
			customColorBtn.addSelectionListener(
					new ColorSelectionAdapter(shell, paintListener, customPresetColor.rgb, styleCmd));

			// Custom color dialog
			final Button colorDlgBtn = new Button(shell, SWT.PUSH);
			colorDlgBtn.setText("Custom...");
			GridDataFactory.fillDefaults().grab(true, true).span(5, 0).applyTo(colorDlgBtn);
			colorDlgBtn.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(final SelectionEvent e) {
					shell.dispose();
					final Shell dlgShell = new Shell(Display.getCurrent().getActiveShell());
					final Rectangle rect = Display.getCurrent().getActiveShell().getBounds();
					// Center color dialog on screen
					dlgShell.setLocation(rect.x + rect.width / 2, rect.y + rect.height / 2);
					final ColorDialog dlg = new ColorDialog(dlgShell);
					final RGB rgb = dlg.open();
					if (rgb != null) {
						if (customPC == null) {
							customPC = new PresetColor(rgb);
						} else {
							customPC.setColor(rgb);
						}
						paintListener.setColor(customPC.rgb);
						runStyleCommand(createGeColor(customPC.rgb), styleCmd);
					}
				}
			});

			// Default button
			final Button defaultColorBtn = new Button(shell, SWT.PUSH);
			defaultColorBtn.setText("Default");
			GridDataFactory.fillDefaults().grab(true, true).span(6, 0).applyTo(defaultColorBtn);
			defaultColorBtn.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(final SelectionEvent e) {
					shell.dispose();
					paintListener.setColor(null);
					runStyleCommand(null, styleCmd);
				}
			});

			// Close shell if user clicks off of the shell
			shell.addListener(SWT.Deactivate, e1 -> shell.setVisible(false));

			shell.pack();

			shell.setLocation(getShellPosition(shell.getSize(), (Button) e.getSource(), 50));

			shell.open();
			shell.setFocus();

			final Display display = shell.getDisplay();
			while (!shell.isDisposed() && shell.isVisible()) {
				if (!display.readAndDispatch()) {
					display.sleep();
				}
			}
		}

		private StylePaintListener paintListener;
		private PresetColor customPC;
		private StyleCommand styleCmd;
	}

	private class StylePaintListener implements PaintListener {
		private final Button button;
		private RGB rgb;
		private RGB defaultRgb;
		private Color color;

		private StylePaintListener(final Button button, final StyleCommand styleCmd) {
			this.button = button;
			this.button.addPaintListener(this);
			this.button.addSelectionListener(new ColorSelectionListener(this, styleCmd));
		}

		@Override
		public void paintControl(final PaintEvent e) {
			// Paint color bar on button
			if (color != null) {
				final Rectangle bounds = button.getBounds();
				if (color != null) {
					e.gc.setBackground(color);
				}

				e.gc.fillRectangle(4, bounds.height - 7, bounds.width - 8, 4);
				e.gc.dispose();
			}
		}

		private void setDefaultColor(final RGB defaultRgb) {
			this.defaultRgb = defaultRgb;
		}

		private void setColor(final RGB newRgb) {
			if (rgb != null) {
				resourceMgr.destroyColor(rgb);
			}

			this.rgb = newRgb == null ? defaultRgb : newRgb;
			this.color = resourceMgr.createColor(rgb);
			button.redraw();
		}

		private Button getButton() {
			return button;
		}

	}

	private static interface CreateStyle {
		void createStyle(final DiagramElement diagramElement, final StyleBuilder sb, final Object value);
	}

	private static class StyleCommand {
		public StyleCommand(final String desc, final CreateStyle createStyle) {
			this.desc = desc;
			this.createStyle = createStyle;
		}

		private String getDescription() {
			return desc;
		}

		private Style getStyle(final DiagramElement diagramElement, final Object value) {
			final StyleBuilder sb = StyleBuilder.create(diagramElement.getStyle());
			createStyle.createStyle(diagramElement, sb, value);
			return sb.build();
		}

		private final String desc;
		private final CreateStyle createStyle;
	}

	private class ComboViewerSelection implements ISelectionChangedListener {
		private ComboViewerSelection(final StyleCommand styleCmd) {
			this.styleCmd = styleCmd;
		}

		@Override
		public void selectionChanged(final SelectionChangedEvent event) {
			if (!selectedDiagramElements.isEmpty()) {
				final IStructuredSelection selection = (IStructuredSelection) event.getSelection();
				runStyleCommand(selection.getFirstElement(), styleCmd);
			}
		}

		private final StyleCommand styleCmd;
	}

	private static org.osate.ge.graphics.Color createGeColor(final RGB color) {
		return new org.osate.ge.graphics.Color(color.red, color.green, color.blue);
	}

	private Point getShellPosition(final Point widgetSize, final Button button,
			final int minSpacingFromDisplayRightAndBottom) {
		// Position the shell
		final Point unclampedShellPosition = Display.getCurrent()
				.map(button.getParent(), null, button.getLocation().x, button.getLocation().y + button.getSize().y);
		final Rectangle clientArea = Display.getCurrent().getClientArea();
		final Point shellPosition = new Point(
				Math.min(unclampedShellPosition.x,
						clientArea.width - widgetSize.x - minSpacingFromDisplayRightAndBottom),
				Math.min(unclampedShellPosition.y,
						clientArea.height - widgetSize.y - minSpacingFromDisplayRightAndBottom));

		return shellPosition;
	}

	private void runStyleCommand(final Object value, final StyleCommand styleCmd) {
		ageDiagram.modify(styleCmd.getDescription(), m -> {
			for (final DiagramElement diagramElement : selectedDiagramElements) {
				m.setStyle(diagramElement, styleCmd.getStyle(diagramElement, value));
			}
		});
	}

	private class ColorSelectionAdapter extends SelectionAdapter {
		public ColorSelectionAdapter(final Shell shell, final StylePaintListener paintListener, final RGB color,
				final StyleCommand styleCmd) {
			this.shell = shell;
			this.paintListener = paintListener;
			this.color = color;
			this.styleCmd = styleCmd;
		}

		@Override
		public void widgetSelected(final SelectionEvent e) {
			paintListener.setColor(color);
			shell.dispose();
			runStyleCommand(createGeColor(color), styleCmd);
		}

		private final RGB color;
		private final StylePaintListener paintListener;
		private final StyleCommand styleCmd;
		private final Shell shell;
	}

	private static final Point COLOR_ICON_SIZE = new Point(18, 18);

	private static class PresetColor {
		private RGB rgb;
		private ImageDescriptor imageDescriptor;

		private PresetColor(final RGB rgb) {
			setColor(rgb);
		}

		private void setColor(final RGB newRgb) {
			this.rgb = newRgb;
			final ImageData data = getImageData(rgb);
			this.imageDescriptor = ImageDescriptor.createFromImageDataProvider(zoom -> zoom == 100 ? data : null);
		}

		private static ImageData getImageData(final RGB rgb) {
			final PaletteData paletteData = new PaletteData(new RGB[] { rgb, new RGB(0, 0, 0) });

			final ImageData data = new ImageData(COLOR_ICON_SIZE.x, COLOR_ICON_SIZE.y, 1, paletteData);

			for (int i = 0; i < COLOR_ICON_SIZE.y; i++) {
				data.setPixel(0, i, 1);
			}

			for (int i = 0; i < COLOR_ICON_SIZE.y; i++) {
				data.setPixel(COLOR_ICON_SIZE.x - 1, i, 1);
			}

			for (int i = 0; i < COLOR_ICON_SIZE.x; i++) {
				data.setPixel(i, 0, 1);
			}

			for (int i = 0; i < COLOR_ICON_SIZE.x; i++) {
				data.setPixel(i, COLOR_ICON_SIZE.y - 1, 1);
			}

			return data;
		}
	}

	private ComboViewerSelection lineWidthSelectionListener = new ComboViewerSelection(
			new StyleCommand("Set Line Width", (diagramElement, sb, value) -> {
				if (supportsLineWidth(diagramElement) && value != null) {
					sb.lineWidth(((LineWidth) value).getValue());
				}
			}));

	private ComboViewerSelection fontSizeSelectionListener = new ComboViewerSelection(
			new StyleCommand("Set Font Size", (diagramElement, sb, value) -> {
				if (supportsFontOptions(diagramElement) && value != null) {
					sb.fontSize(((FontSize) value).getValue());
				}
			}));

	private ComboViewerSelection primaryLabelVisibleSelectionListener = new ComboViewerSelection(
			new StyleCommand("Set Primary Label Visibility", (diagramElement, sb, value) -> {
				if (supportsPrimaryLabelVisible(diagramElement) && value != null) {
					sb.primaryLabelVisible(((LabelVisibility) value).getValue());
				}
			}));

	private SelectionAdapter imageSelectionAdapter = new SelectionAdapter() {
		@Override
		public void widgetSelected(final SelectionEvent e) {
			// Open menu that lets user choose new image or remove image
			final Menu popupMenu = new Menu(setImageButton);
			// Add menu item to launch choose new image dialog
			final MenuItem chooseImgMenuItem = new MenuItem(popupMenu, SWT.NONE);
			chooseImgMenuItem.setText("Select...");
			chooseImgMenuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(final SelectionEvent e) {
					final InternalDiagramEditor editor = UiUtil.getActiveDiagramEditor();
					if (editor != null) {
						final ElementTreeSelectionDialog dialog = createSelectionDialog(editor);
						if (dialog.open() == Window.OK) {
							final IFile iFile = (IFile) dialog.getResult()[0];
							runStyleCommand(iFile.getFullPath(), imageStyleCommand);
						}
					}
				}
			});

			if (toggleShowImage.isEnabled()) {
				// Menu item to remove image from diagram element
				final MenuItem removeImgMenuItem = new MenuItem(popupMenu, SWT.NONE);
				removeImgMenuItem.setText("Remove");
				removeImgMenuItem.addSelectionListener(new SelectionAdapter() {
					@Override
					public void widgetSelected(final SelectionEvent e) {
						runStyleCommand(null, imageStyleCommand);
					}
				});
			}
			// Set menu location
			popupMenu.setLocation(getShellPosition(setImageButton.getSize(), setImageButton, 5));
			// Show menu
			popupMenu.setVisible(true);
		}

		private ElementTreeSelectionDialog createSelectionDialog(final InternalDiagramEditor editor) {
			final ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(
					Display.getCurrent().getActiveShell(), new WorkbenchLabelProvider(),
					new BaseWorkbenchContentProvider());
			try {
				final ProjectProvider projectProvider = Objects.requireNonNull(
						Adapters.adapt(editor, ProjectProvider.class), "unable to retrieve project provider");
				// Configure selection dialog
				dialog.setTitle("Select an Image");
				dialog.setMessage("Select an image.");
				dialog.setAllowMultiple(false);
				dialog.setHelpAvailable(false);
				final IProject project = projectProvider.getProject();
				final IProject[] referencedProjects = project.getReferencedProjects();
				// Filter Resources
				dialog.addFilter(new ImageSelectionViewerFilter(Lists.asList(project, referencedProjects)));
				dialog.setValidator(selection -> {
					if (selection.length > 0 && selection[0] instanceof IFile) {
						return new Status(IStatus.OK, Activator.PLUGIN_ID, "");
					}
					return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "No image selected.");
				});

				// Set initial selection
				final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
				// Allow selection of project resources
				dialog.setInput(workspaceRoot);

				final IFile diagramFile = editor.getDiagramFile();
				if (diagramFile != null) {
					dialog.setInitialSelection(diagramFile.getParent());
				}
			} catch (final CoreException e) {
				throw new RuntimeException("unable to get referenced projects");
			}

			return dialog;
		}

		// Set image and visibility
		private final StyleCommand imageStyleCommand = new StyleCommand("Set Image", (diagramElement, sb, value) -> {
			if (DiagramElementPredicates.supportsImage(diagramElement)) {
				final IPath imagePath = (IPath) value;
				sb.imagePath(imagePath);
				sb.showAsImage(imagePath != null);
			}
		});
	};

	// Set image visibility
	private final StyleCommand showAsImageStyleCmd = new StyleCommand("Show as Image", (diagramElement, sb, value) -> {
		if (DiagramElementPredicates.supportsImage(diagramElement) && value != null
				&& diagramElement.getStyle().getImagePath() != null) {
			sb.showAsImage((Boolean) value);
		}
	});

	// Only allow resources that are in the current project or referenced projects to be in dialog
	private class ImageSelectionViewerFilter extends ViewerFilter {
		private final List<IProject> projects;

		private ImageSelectionViewerFilter(final List<IProject> projects) {
			this.projects = projects;
		}

		@Override
		public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
			return isReferencedProject(element) || element instanceof IFolder
					|| (element instanceof IFile && isImageFile((IFile) element));
		}

		private boolean isReferencedProject(final Object element) {
			for (final IProject project : projects) {
				if (project == element) {
					return true;
				}
			}
			return false;
		}
	}

	/**
	 * Testing ID for the label visibility combo
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_PRIMARY_LABEL_VISIBILITY_COMBO = "org.osate.ge.properties.PrimaryLabelVisibility";

	/**
	 * Testing ID for the font size combo
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_FONT_SIZE_COMBO = "org.osate.ge.properties.FontSize";

	/**
	 * Testing ID for the line width combo
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_LINE_WIDTH_COMBO = "org.osate.ge.properties.LineWidth";

	/**
	 * Testing ID for the background color button
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_BACKGROUND_COLOR_BUTTON = "org.osate.ge.properties.BackgroundColor";

	/**
	 * Testing ID for the font color button
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_FONT_COLOR_BUTTON = "org.osate.ge.properties.FontColor";

	/**
	 * Testing ID for the outline color button
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_OUTLINE_COLOR_BUTTON = "org.osate.ge.properties.OutlineColor";

	/**
	 * Testing ID for the custom color button
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_CUSTOM_COLOR_BUTTON = "org.osate.ge.properties.CustomColor";

	/**
	 * Testing ID for the set image button
	 * @see SwtUtil#getTestingId(org.eclipse.swt.widgets.Widget)
	 */
	public static final String WIDGET_ID_SET_IMAGE_BUTTON = "org.osate.ge.properties.SetImage";

	private static final ImageDescriptor OUTLINE_ICON = Activator.getImageDescriptor("icons/Outline.gif");
	private static final ImageDescriptor BACKGROUND_ICON = Activator.getImageDescriptor("icons/Background.gif");
	private static final ImageDescriptor FONT_COLOR_ICON = Activator.getImageDescriptor("icons/FontColor.gif");
	private static final ImageDescriptor IMAGE_ICON = Activator.getImageDescriptor("icons/BackgroundImage.gif");

	private final String[] supportedImageTypes = { "bmp", "png", "jpg", "jpeg", "gif" };
	private AgeDiagram ageDiagram;
	private ResourceManager resourceMgr;
	private final List<DiagramElement> selectedDiagramElements = new ArrayList<>();
	private Button setImageButton;
	private Button toggleShowImage;
	private org.eclipse.swt.widgets.Label fontSizeLabel;
	private org.eclipse.swt.widgets.Label lineWidthLabel;
	private org.eclipse.swt.widgets.Label imageLabel;
	private ComboViewer fontSizeComboViewer;
	private ComboViewer lineWidthComboViewer;
	private org.eclipse.swt.widgets.Label primaryLabelVisibleLabel;
	private ComboViewer primaryLabelVisibleViewer;

	private StylePaintListener backgroundPaintListener;
	private StylePaintListener fontColorPaintListener;
	private StylePaintListener outlinePaintListener;

	// Red Column
	private final PresetColor lighterRed = new PresetColor(new RGB(255, 204, 204));
	private final PresetColor lightRed = new PresetColor(new RGB(255, 102, 102));
	private final PresetColor red = new PresetColor(new RGB(255, 0, 0));
	private final PresetColor darkRed = new PresetColor(new RGB(153, 0, 0));

	// Orange Column
	private final PresetColor lighterOrange = new PresetColor(new RGB(255, 229, 204));
	private final PresetColor lightOrange = new PresetColor(new RGB(255, 178, 102));
	private final PresetColor orange = new PresetColor(new RGB(255, 128, 0));
	private final PresetColor darkOrange = new PresetColor(new RGB(153, 76, 0));

	// Blue Column
	private final PresetColor lighterBlue = new PresetColor(new RGB(204, 204, 255));
	private final PresetColor lightBlue = new PresetColor(new RGB(102, 102, 255));
	private final PresetColor blue = new PresetColor(new RGB(0, 0, 255));
	private final PresetColor darkBlue = new PresetColor(new RGB(0, 0, 153));

	// Yellow Column
	private final PresetColor lighterYellow = new PresetColor(new RGB(255, 255, 204));
	private final PresetColor lightYellow = new PresetColor(new RGB(255, 255, 102));
	private final PresetColor yellow = new PresetColor(new RGB(255, 255, 0));
	private final PresetColor darkYellow = new PresetColor(new RGB(153, 153, 0));

	// Green Column
	private final PresetColor lighterGreen = new PresetColor(new RGB(204, 255, 204));
	private final PresetColor lightGreen = new PresetColor(new RGB(102, 255, 102));
	private final PresetColor green = new PresetColor(new RGB(0, 255, 0));
	private final PresetColor darkGreen = new PresetColor(new RGB(0, 153, 0));

	// Shade Column
	private final PresetColor white = new PresetColor(new RGB(255, 255, 255));
	private final PresetColor lightGray = new PresetColor(new RGB(224, 224, 224));
	private final PresetColor gray = new PresetColor(new RGB(160, 160, 160));
	private final PresetColor black = new PresetColor(new RGB(0, 0, 0));

	private final List<PresetColor> colors = new ArrayList<>();
	{
		// Lighter Colors
		colors.add(lighterRed);
		colors.add(lighterBlue);
		colors.add(lighterOrange);
		colors.add(lighterYellow);
		colors.add(lighterGreen);
		colors.add(white);

		// Light Colors
		colors.add(lightRed);
		colors.add(lightBlue);
		colors.add(lightOrange);
		colors.add(lightYellow);
		colors.add(lightGreen);
		colors.add(lightGray);

		// Normal Colors
		colors.add(red);
		colors.add(blue);
		colors.add(orange);
		colors.add(yellow);
		colors.add(green);
		colors.add(gray);

		// Dark Colors
		colors.add(darkRed);
		colors.add(darkBlue);
		colors.add(darkOrange);
		colors.add(darkYellow);
		colors.add(darkGreen);
		colors.add(black);
	}

	private static enum LineWidth {
		Default(null), Small(2.0), Medium(4.0), Large(6.0);

		private LineWidth(final Double value) {
			this.value = value;
		}

		public Double getValue() {
			return value;
		}

		public static LineWidth getByValue(final Double value) {
			return Stream.of(values()).filter(v -> Objects.equals(v.value, value)).findAny().orElse(null);
		}

		private final Double value;
	}

	private static enum FontSize {
		DEFAULT(null), SMALL(8.0), MEDIUM(10.0), LARGE(16.0), EXTRA_LARGE(20.0);

		private FontSize(final Double value) {
			this.value = value;
		}

		@Override
		public String toString() {
			return StringUtil.snakeCaseToTitleCase(super.toString());
		}

		public Double getValue() {
			return value;
		}

		public static FontSize getByValue(final Double value) {
			return Stream.of(values()).filter(v -> Objects.equals(v.value, value)).findAny().orElse(null);
		}

		private final Double value;
	}

	private static enum LabelVisibility {
		DEFAULT(null), SHOW(true), HIDE(false);

		private LabelVisibility(final Boolean value) {
			this.value = value;
		}

		@Override
		public String toString() {
			return StringUtil.snakeCaseToTitleCase(super.toString());
		}

		public Boolean getValue() {
			return value;
		}

		public static LabelVisibility getByValue(final Boolean value) {
			return Stream.of(values()).filter(v -> Objects.equals(v.value, value)).findAny().orElse(null);
		}

		private final Boolean value;
	}
}