EditEmbeddedTextDialog.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.ba.ui;
import java.util.Objects;
import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ExtendedModifyEvent;
import org.eclipse.swt.custom.ExtendedModifyListener;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.osate.ge.ba.ui.properties.EditableEmbeddedTextValue;
import org.osate.ge.swt.SwtUtil;
/**
* Dialog for editing AADL models using {@link EditableEmbeddedTextValue}
* @since 2.0
*/
public class EditEmbeddedTextDialog extends MessageDialog {
private static String WIDGET_ID = "org.osate.ge.ba.behaviortransition.editdialog";
private static String MODIFIED_SOURCE_KEY = WIDGET_ID + ".modifiedsource";
/**
* Widget ID for StyledText
*/
public static String WIDGET_ID_TEXT = WIDGET_ID + ".text";
/**
* Widget ID for OK Button
*/
public static String WIDGET_ID_CONFIRM = WIDGET_ID + ".confirmation";
private final ValidationTask validationTask = new ValidationTask();
private final EmbeddedStyledTextXtextAdapter xtextAdapter;
private final ExtendedModifyListener textValidator;
private final IHandlerService service;
private final int styledTextStyle;
private final GridData styledTextLayoutData;
private IHandlerActivation undoHandler;
private IHandlerActivation redoHandler;
private StyledText styledText;
private Result result;
/**
* Creates a new instance
* @param parentShell the parent shell for the dialog
* @param editableTextValue is the text information for editing embedded AADL source
* @param styledTextStyle is the style for the {@link StyledText}
* @param styledTextLayoutData is the layout data for the {@link StyledText}
* @since 2.0
*/
public EditEmbeddedTextDialog(final Shell parentShell,
final EditableEmbeddedTextValue editableTextValue,
final int styledTextStyle,
final GridData styledTextLayoutData) {
super(parentShell, editableTextValue.getEditDialogTitle(), null, editableTextValue.getEditDialogMessage(), MessageDialog.NONE,
0,
"OK",
"Cancel");
// Create new xtext adapter for the edit dialog
this.xtextAdapter = Objects.requireNonNull(
new EmbeddedStyledTextXtextAdapter(editableTextValue),
"xtextAdapter cannot be null");
this.styledTextStyle = styledTextStyle;
this.styledTextLayoutData = Objects.requireNonNull(styledTextLayoutData, "styledTextLayoutData cannot be null");
this.textValidator = Objects.requireNonNull(
createTextValidator(),
"textValidator cannot be null");
service = PlatformUI.getWorkbench().getService(IHandlerService.class);
setShellStyle(SWT.CLOSE | SWT.PRIMARY_MODAL | SWT.BORDER | SWT.TITLE | SWT.RESIZE);
}
@Override
protected Control createCustomArea(final Composite parent) {
final Composite composite = new Composite(parent, SWT.NONE);
final GridLayout layout = getGridLayout();
composite.setLayout(layout);
composite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
composite.setFont(parent.getFont());
styledText = new StyledText(composite, styledTextStyle);
styledText.setLayoutData(styledTextLayoutData);
styledText.addExtendedModifyListener(textValidator);
SwtUtil.setTestingId(styledText, WIDGET_ID_TEXT);
xtextAdapter.adapt(styledText);
final UndoRedoHelper undoRedoHelper = new UndoRedoHelper();
undoHandler = service.activateHandler("org.eclipse.ui.edit.undo", new AbstractHandler() {
@Override
public Object execute(final ExecutionEvent event) throws ExecutionException {
undoRedoHelper.undo();
return null;
}
});
redoHandler = service.activateHandler("org.eclipse.ui.edit.redo", new AbstractHandler() {
@Override
public Object execute(final ExecutionEvent event) throws ExecutionException {
undoRedoHelper.redo();
return null;
}
});
return composite;
}
private GridLayout getGridLayout() {
final GridLayout layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.verticalSpacing = 0;
layout.horizontalSpacing = 0;
layout.numColumns = 2;
return layout;
}
@Override
public void create() {
super.create();
final Button okBtn = getButton(IDialogConstants.OK_ID);
okBtn.setEnabled(false);
SwtUtil.setTestingId(okBtn, WIDGET_ID_CONFIRM);
}
// Text modification listener that sets the OK button as enabled
// or disabled based on if the new text is valid
private ExtendedModifyListener createTextValidator() {
return event -> {
// Disable button until validation occurs
final Button okBtn = getButton(IDialogConstants.OK_ID);
okBtn.setEnabled(false);
validationTask.schedule(okBtn, styledText.getText().trim());
};
}
private class ValidationTask {
private Timer validationTimer;
public void schedule(final Button okBtn, final String newText) {
cancelTimer();
validationTimer = new Timer();
validationTimer.schedule(new TimerTask() {
@Override
public void run() {
Display.getDefault().asyncExec(() -> {
if(!styledText.isDisposed()) {
// Disable ok button if text has not changed
if (newText.equals(xtextAdapter.getEmbeddedTextValue().getEditableText())) {
okBtn.setEnabled(false);
return;
}
// Source text to load
final String modifiedSrc = xtextAdapter.getValidModifiedSource(newText).orElse(null);
// Set modified source text data
styledText.setData(MODIFIED_SOURCE_KEY, modifiedSrc);
okBtn.setEnabled(modifiedSrc != null);
}
});
}
}, 1000);
}
public void cancelTimer() {
if (validationTimer != null) {
validationTimer.cancel();
validationTimer.purge();
}
}
}
@Override
protected void buttonPressed(final int buttonId) {
if (buttonId == IDialogConstants.OK_ID) {
// Set return result
result = new Result(styledText.getData(MODIFIED_SOURCE_KEY).toString(),
styledText.getText().trim());
}
super.buttonPressed(buttonId);
}
@Override
public boolean close() {
xtextAdapter.dispose();
service.deactivateHandler(undoHandler);
service.deactivateHandler(redoHandler);
validationTask.cancelTimer();
return super.close();
}
/**
* Returns the result of the {@link EditEmbeddedTextDialog} that contains the full and partial modified AADL source
* @return the result of the {@link EditEmbeddedTextDialog}
*/
public Result getResult() {
return result;
}
/**
* Result that contains the full and partial modified AADL source
*/
public class Result {
private final String fullSource;
private final String partialSource;
/**
* Instantiates the dialog result
* @param fullSource the modified source for the full AADL resource/document
* @param partialSource the region of the AADL source edited by the dialog
*/
public Result(final String fullSource, final String partialSource) {
this.fullSource = fullSource;
this.partialSource = partialSource;
}
/**
* Returns the modified source for the full AADL resource/document.
* @return the modified source for the full AADL resource/document.
*/
public String getFullSource() {
return fullSource;
}
/**
* Returns the modified source for the region of the AADL resource edited by the dialog.
* @return the modified source for the region of the AADL resource edited by the dialog.
*/
public String getPartialSource() {
return partialSource;
}
}
private class UndoRedoHelper implements ExtendedModifyListener {
private final UndoRedoStack undoRedoStack = new UndoRedoStack();
private boolean isUndo;
private boolean isRedo;
private UndoRedoHelper() {
styledText.addExtendedModifyListener(this);
}
/**
* Creates a corresponding Undo or Redo step from the given event and pushes
* it to the stack. The Redo stack is emptied if the event comes
* from key input.
*/
@Override
public void modifyText(final ExtendedModifyEvent event) {
if (isUndo) {
undoRedoStack.pushRedo(event);
} else { // is redo or key input
undoRedoStack.pushUndo(event);
if (!isRedo) {
undoRedoStack.clearRedo();
}
}
}
private void undo() {
if (undoRedoStack.hasUndo()) {
isUndo = true;
revertEvent(undoRedoStack.popUndo());
isUndo = false;
}
}
private void redo() {
if (undoRedoStack.hasRedo()) {
isRedo = true;
revertEvent(undoRedoStack.popRedo());
isRedo = false;
}
}
/**
* Reverts the given modify event
*/
private void revertEvent(final ExtendedModifyEvent event) {
styledText.replaceTextRange(event.start, event.length, event.replacedText);
styledText.setSelectionRange(event.start, event.replacedText.length());
}
private class UndoRedoStack {
private final static int MAX_STACK_SIZE = 50;
private final Stack<ExtendedModifyEvent> undo;
private final Stack<ExtendedModifyEvent> redo;
public UndoRedoStack() {
undo = new Stack<ExtendedModifyEvent>();
redo = new Stack<ExtendedModifyEvent>();
}
public void pushUndo(final ExtendedModifyEvent undoEvent) {
if (undo.size() > MAX_STACK_SIZE) {
undo.remove(0);
}
undo.add(undoEvent);
}
public void pushRedo(final ExtendedModifyEvent redoEvent) {
if (redo.size() > MAX_STACK_SIZE) {
redo.remove(0);
}
redo.add(redoEvent);
}
public ExtendedModifyEvent popUndo() {
return undo.pop();
}
public ExtendedModifyEvent popRedo() {
return redo.pop();
}
public void clearRedo() {
redo.clear();
}
public boolean hasUndo() {
return !undo.isEmpty();
}
public boolean hasRedo() {
return !redo.isEmpty();
}
}
}
}