ExportDiagramDialog.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.dialogs;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.services.DiagramExportService;
/**
* Dialog shown when exporting the diagram as an image.
*
*/
public class ExportDiagramDialog {
private static final Dimension MAX_IMAGE_DIMENSIONS = new Dimension(8000, 8000);
private static final Dimension MIN_IMAGE_DIMENSIONS = new Dimension(16, 16);
private static final double MAX_SCALE_FACTOR = 5.0;
private static final double MIN_SCALE_FACTOR = 0.1;
private static final String[] WIDTHS = new String[] { "320", "640", "800", "1024", "1280", "1600", "1920" };
private static final String[] HEIGHTS = new String[] { "200", "400", "600", "768", "1024", "1280" };
private static final double[] SCALE_FACTORS = new double[] { 0.5, 1.0, 2.0, 4.0 };
public static enum ImageFormat {
JPEG("jpg", ".jpg"), PNG("png", ".png"), SVG("svg", ".svg");
private final String exporterFormat;
private final String extension;
ImageFormat(final String exporterFormat, final String extension) {
this.exporterFormat = exporterFormat;
this.extension = extension;
}
/**
* Returns the format that should be passed to {@link DiagramExportService}.
* @return the format that should be passed to the exporter
*/
public String getExporterFormat() {
return this.exporterFormat;
}
/**
* Returns the file extension used for the image format. For example, for {@link #SVG}, ".svg" will be returned.
* @return
*/
public String getDotExtension() {
return this.extension;
}
}
public static class Result {
private final boolean all;
private final ImageFormat format;
private final double scaling;
Result(final boolean all, final ImageFormat format, final double scaling) {
this.all = all;
this.format = format;
this.scaling = scaling;
}
/**
* Returns whether to export the entire figure.
* @return true if the user selected to export all the diagram. False if the user selected to export just selected elements.
*/
public boolean getAll() {
return all;
}
public ImageFormat getFormat() {
return format;
}
public double getScaling() {
return scaling;
}
}
private static class InnerDialog extends Dialog {
private final Dimension rootElement;
private final Dimension selectedElement;
private Dimension figure;
private Result result;
private Button allFigureButton;
private Combo scaleFactorCombo;
private Combo widthCombo;
private Combo heightCombo;
private Combo formatCombo;
private double scaleFactor = 1.0; // default value for scaling
private boolean internalModification = false; // flag so listeners are not executed during modifications
private InnerDialog(final Shell parentShell, final Dimension rootElement, final Dimension selectedElement) {
super(parentShell);
this.rootElement = rootElement;
this.selectedElement = selectedElement;
setShellStyle(SWT.CLOSE | SWT.PRIMARY_MODAL | SWT.BORDER | SWT.TITLE);
determinePossibleFigures();
}
private void determinePossibleFigures() {
figure = selectedElement == null ? rootElement : selectedElement;
}
@Override
protected void configureShell(final Shell newShell) {
super.configureShell(newShell);
newShell.setText("Export Diagram");
}
@Override
protected Control createDialogArea(final Composite parent) {
final Composite container = (Composite) super.createDialogArea(parent);
container.setLayout(new GridLayout(2, false));
createFigureGroup(container);
createScaleGroup(container);
createFormatGroup(container);
return container;
}
private void createLabel(final Group parent, final String text) {
final Label label = new Label(parent, SWT.NONE);
label.setText(text);
}
private Combo createCombo(final Group parent, final String[] items) {
final Combo combo = new Combo(parent, SWT.DROP_DOWN);
combo.setItems(items);
combo.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
return combo;
}
@Override
public void create() {
super.create();
updateControls((int) Math.ceil(figure.width), (int) Math.ceil(figure.height));
// Create modify and verify listeners
final ModifyListener modifyListener = e -> {
if (internalModification) {
return;
}
try {
final Combo combo = (Combo) e.getSource();
if (combo.getText().isEmpty() || combo.getText().equals(".")) {
setOkBtnEnabled(false);
return;
}
internalModification = true;
final int width, height;
if (combo == widthCombo) {
// Get new width value, update scale factor and height
width = Integer.valueOf(combo.getText());
scaleFactor = width / figure.width;
height = (int) Math.ceil(scaleFactor * figure.height);
} else if (combo == heightCombo) {
// Get new height value, update scale factor and width
height = Integer.valueOf(combo.getText());
scaleFactor = height / figure.height;
width = (int) Math.ceil(scaleFactor * figure.width);
} else {
// Update width and height for new scale factor
width = (int) Math.ceil(scaleFactor * figure.width);
height = (int) Math.ceil(scaleFactor * figure.height);
}
updateControls(width, height);
} finally {
internalModification = false;
}
};
final VerifyListener verifyListener = e -> {
// Do not allow letters
if (Character.isLetter(e.character)) {
e.doit = false;
return;
}
final Combo combo = (Combo) e.getSource();
final String originalText = combo.getText();
final String updatedText = originalText.substring(0, e.start) + e.text + originalText.substring(e.end);
if (combo == scaleFactorCombo) {
try {
// Allow for empty or a period to be placed in scaled combo text
if (updatedText.isEmpty() || updatedText.equals(".")) {
scaleFactor = 0;
} else {
scaleFactor = Double.parseDouble(updatedText);
}
} catch (final Exception ex) {
e.doit = false;
}
} else {
// Verify Width and Height combos
e.doit = isValidInteger(updatedText);
}
};
// Add listeners here so initialization does not trigger listeners
widthCombo.addVerifyListener(verifyListener);
widthCombo.addModifyListener(modifyListener);
heightCombo.addVerifyListener(verifyListener);
heightCombo.addModifyListener(modifyListener);
scaleFactorCombo.addVerifyListener(verifyListener);
scaleFactorCombo.addModifyListener(modifyListener);
}
private void createFigureGroup(final Composite parent) {
final Group figureGroup = createGroup(parent, "Figure");
figureGroup.setLayout(new GridLayout(1, false));
figureGroup.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
final boolean isSingleSelection = selectedElement != null;
allFigureButton = createButton(figureGroup, "All", !isSingleSelection, rootElement != null);
createButton(figureGroup, "Selected (only single-selection)", isSingleSelection, isSingleSelection);
}
private Button createButton(final Group group, final String text, final boolean isSelected,
final boolean isEnabled) {
final Button btn = new Button(group, SWT.RADIO);
btn.setText(text);
btn.setSelection(isSelected);
btn.setEnabled(isEnabled);
btn.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
btn.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
// Set figure to root or selection and update dialog controls
figure = allFigureButton.getSelection() ? rootElement : selectedElement;
updateControls((int) Math.ceil(figure.width), (int) Math.ceil(figure.height));
}
});
return btn;
}
private Group createScaleGroup(final Composite parent) {
final Group scaleGroup = createGroup(parent, "Size");
scaleGroup.setLayoutData(GridDataFactory.fillDefaults().span(1, 2).grab(true, true).create());
scaleGroup.setLayout(new GridLayout(2, false));
createLabel(scaleGroup, "Scale-factor:");
// Scale combo
scaleFactorCombo = createCombo(scaleGroup, SWT.DROP_DOWN,
DoubleStream.of(SCALE_FACTORS).mapToObj(Double::toString).toArray(String[]::new), "1.0");
// Create width and height combos
createLabel(scaleGroup, "Width:");
widthCombo = createCombo(scaleGroup, WIDTHS);
createLabel(scaleGroup, "Height:");
heightCombo = createCombo(scaleGroup, HEIGHTS);
return scaleGroup;
}
private void createFormatGroup(final Composite parent) {
final Group formatGroup = createGroup(parent, "Image Format");
formatGroup.setLayout(new GridLayout(2, false));
formatGroup.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
formatCombo = createCombo(formatGroup, SWT.DROP_DOWN | SWT.READ_ONLY,
Stream.of(ImageFormat.values()).map(ImageFormat::toString).toArray(String[]::new),
ImageFormat.SVG.toString());
}
private Group createGroup(final Composite parent, final String text) {
final Group group = new Group(parent, SWT.NONE);
group.setText(text);
return group;
}
private Combo createCombo(final Group parent, final int style, final String[] items,
final String defaultValue) {
final Combo combo = new Combo(parent, style);
combo.setItems(items);
// Set default selection
final int defaultIndex = getIndexOfItem(combo, defaultValue);
combo.select(defaultIndex);
combo.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
return combo;
}
private int getIndexOfItem(final Combo combo, final String item) {
return Math.max(0, Arrays.asList(combo.getItems()).indexOf(item));
}
@Override
protected void okPressed() {
// Set value for return
result = new Result(allFigureButton.getSelection(), ImageFormat.valueOf(formatCombo.getText()),
scaleFactor);
super.okPressed();
}
public Result getResult() {
return result;
}
// Update controls based on scale factor
private void updateControls(final int width, final int height) {
updateDimensionCombo(widthCombo, width);
updateDimensionCombo(heightCombo, height);
updateScaleFactorCombo();
updateOkBtn(width, height);
}
private static void updateDimensionCombo(final Combo dimCombo, final int dim) {
final String newText = Integer.toString(dim);
if (!dimCombo.getText().equals(newText)) {
// Don't update if identical, otherwise cursor will move to the
// first character
dimCombo.setText(newText);
}
}
private boolean isValidScaleFactor() {
return scaleFactor >= MIN_SCALE_FACTOR && scaleFactor <= MAX_SCALE_FACTOR;
}
private static boolean isValidWidth(final int width) {
return width >= MIN_IMAGE_DIMENSIONS.width && width <= MAX_IMAGE_DIMENSIONS.width;
}
private static boolean isValidHeight(final int height) {
return height >= MIN_IMAGE_DIMENSIONS.height && height <= MAX_IMAGE_DIMENSIONS.height;
}
private void updateScaleFactorCombo() {
try {
final double oldValue = Double.parseDouble(scaleFactorCombo.getText());
if (scaleFactor != oldValue) {
scaleFactorCombo.setText(Double.toString(scaleFactor));
}
} catch (final NumberFormatException e) {
scaleFactorCombo.setText(Double.toString(scaleFactor));
}
}
private void updateOkBtn(final int width, final int height) {
final boolean isEnable = isValidWidth(width) && isValidHeight(height) && isValidScaleFactor();
setOkBtnEnabled(isEnable);
}
private void setOkBtnEnabled(final boolean isEnabled) {
final Button okBtn = getButton(OK);
okBtn.setEnabled(isEnabled);
}
private static boolean isValidInteger(final String input) {
try {
// Allows for erasing width or height in combo text
Integer.parseInt(input.isEmpty() ? "0" : input);
} catch (final Exception e) {
return false;
}
return true;
}
}
/**
* Open a modal dialog.
* @param parentShell the parent shell for the modal dialog.
* @param root the size of the entire diagram when the scale factor is 1.0
* @param selected the size of the selected element when the scale factor is 1.0.
* If null, then option to export the selected figure will be disabled.
* @return the result of the dialog. An empty value is returned if the dialog is canceled.
*/
public static Optional<Result> open(final Shell parentShell, final Dimension root, final Dimension selected) {
final InnerDialog dlg = new InnerDialog(parentShell, root, selected);
dlg.open();
return Optional.ofNullable(dlg.getResult());
}
public static void main(final String[] args) {
open(new Shell(), new Dimension(700, 800), new Dimension(300, 400)).ifPresent(result -> {
System.err.println("All: " + result.getAll());
System.err.println("Format: " + result.getFormat());
System.err.println("Scaling: " + result.getScaling());
});
}
}