blob: 7932de2e743397141f051b8b815968fb7e04b002 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.internal.ui.editor.context;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import org.eclipse.core.filebuffers.IDocumentSetupParticipant;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.*;
import org.eclipse.pde.internal.core.text.IEditingModel;
import org.eclipse.pde.internal.core.util.PropertiesUtil;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.editor.PDEFormEditor;
import org.eclipse.pde.internal.ui.editor.PDEStorageDocumentProvider;
import org.eclipse.pde.internal.ui.util.TextUtil;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.text.edits.*;
import org.eclipse.ui.*;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.editors.text.ForwardingDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.IElementStateListener;
/**
* This class maintains objects associated with a single editor input.
*/
public abstract class InputContext {
private PDEFormEditor fEditor;
private IEditorInput fEditorInput;
private IBaseModel fModel;
private IModelChangedListener fModelListener;
private IDocumentProvider fDocumentProvider;
private IElementStateListener fElementListener;
protected ArrayList<TextEdit> fEditOperations = new ArrayList<>();
private boolean fValidated;
private boolean fPrimary;
private boolean fIsSourceMode;
private boolean fMustSynchronize;
class ElementListener implements IElementStateListener {
@Override
public void elementContentAboutToBeReplaced(Object element) {
}
@Override
public void elementContentReplaced(Object element) {
if (element != null && element.equals(fEditorInput))
doRevert();
}
@Override
public void elementDeleted(Object element) {
if (element != null && element.equals(fEditorInput))
dispose();
}
@Override
public void elementDirtyStateChanged(Object element, boolean isDirty) {
if (element != null && element.equals(fEditorInput))
fMustSynchronize = true;
}
@Override
public void elementMoved(Object originalElement, Object movedElement) {
if (originalElement != null && originalElement.equals(fEditorInput)) {
dispose();
fEditor.close(true);
}
}
}
public InputContext(PDEFormEditor editor, IEditorInput input, boolean primary) {
this.fEditor = editor;
this.fEditorInput = input;
setPrimary(primary);
}
public abstract String getId();
public IEditorInput getInput() {
return fEditorInput;
}
public PDEFormEditor getEditor() {
return fEditor;
}
public IBaseModel getModel() {
return fModel;
}
public IDocumentProvider getDocumentProvider() {
return fDocumentProvider;
}
private IDocumentProvider createDocumentProvider(IEditorInput input) {
if (input instanceof IFileEditorInput) {
return new ForwardingDocumentProvider(getPartitionName(), getDocumentSetupParticipant(), PDEPlugin.getDefault().getTextFileDocumentProvider());
}
return new PDEStorageDocumentProvider(getDocumentSetupParticipant());
}
protected IDocumentSetupParticipant getDocumentSetupParticipant() {
return document -> {
};
}
protected abstract String getPartitionName();
protected abstract Charset getDefaultCharset();
protected abstract IBaseModel createModel(IEditorInput input) throws CoreException;
protected void create() {
fElementListener = new ElementListener();
fDocumentProvider = createDocumentProvider(fEditorInput);
try {
fDocumentProvider.connect(fEditorInput);
fModel = createModel(fEditorInput);
if (fModel instanceof IModelChangeProvider) {
fModelListener = e -> {
if (e.getChangeType() != IModelChangedEvent.WORLD_CHANGED) {
if (!fEditor.getLastDirtyState())
fEditor.fireSaveNeeded(fEditorInput, true);
IModelChangeProvider provider = e.getChangeProvider();
if (provider instanceof IEditingModel) {
// this is to guard against false notifications
// when a revert operation is performed, focus is taken away from a FormEntry
// and a text edit operation is falsely requested
if (((IEditingModel) provider).isDirty())
addTextEditOperation(fEditOperations, e);
}
}
};
((IModelChangeProvider) fModel).addModelChangedListener(fModelListener);
}
IAnnotationModel amodel = fDocumentProvider.getAnnotationModel(fEditorInput);
if (amodel != null)
amodel.connect(fDocumentProvider.getDocument(fEditorInput));
fDocumentProvider.addElementStateListener(fElementListener);
} catch (CoreException e) {
PDEPlugin.logException(e);
}
}
public synchronized boolean validateEdit() {
if (!fValidated) {
if (fEditorInput instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) fEditorInput).getFile();
if (file.isReadOnly()) {
Shell shell = fEditor.getEditorSite().getShell();
IStatus validateStatus = PDEPlugin.getWorkspace().validateEdit(new IFile[] {file}, shell);
fValidated = true; // to prevent loops
if (validateStatus.getSeverity() != IStatus.OK)
ErrorDialog.openError(shell, fEditor.getTitle(), null, validateStatus);
return validateStatus.getSeverity() == IStatus.OK;
}
}
}
return true;
}
public void doSave(IProgressMonitor monitor) {
try {
IDocument doc = fDocumentProvider.getDocument(fEditorInput);
fDocumentProvider.aboutToChange(fEditorInput);
flushModel(doc);
fDocumentProvider.saveDocument(monitor, fEditorInput, doc, true);
fDocumentProvider.changed(fEditorInput);
fValidated = false;
} catch (CoreException e) {
PDEPlugin.logException(e);
}
}
protected abstract void addTextEditOperation(ArrayList<TextEdit> ops, IModelChangedEvent event);
public void flushEditorInput() {
if (!fEditOperations.isEmpty()) {
IDocument doc = fDocumentProvider.getDocument(fEditorInput);
fDocumentProvider.aboutToChange(fEditorInput);
flushModel(doc);
fDocumentProvider.changed(fEditorInput);
fValidated = false;
} else if ((fModel instanceof IEditable) && ((IEditable) fModel).isDirty()) {
// When text edit operations are made that cancel each other out,
// the editor is not undirtied
// e.g. Extensions page: Move an element up and then move it down
// back in the same position: Bug # 197831
((IEditable) fModel).setDirty(false);
}
}
protected void flushModel(IDocument doc) {
boolean flushed = true;
if (!fEditOperations.isEmpty()) {
try {
MultiTextEdit edit = new MultiTextEdit();
if (isNewlineNeeded(doc))
insert(edit, new InsertEdit(doc.getLength(), TextUtilities.getDefaultLineDelimiter(doc)));
for (int i = 0; i < fEditOperations.size(); i++) {
insert(edit, fEditOperations.get(i));
}
if (fModel instanceof IEditingModel)
((IEditingModel) fModel).setStale(true);
edit.apply(doc);
fEditOperations.clear();
} catch (MalformedTreeException | BadLocationException e) {
PDEPlugin.logException(e);
flushed = false;
}
}
// If no errors were encountered flushing the model, then undirty the
// model. This needs to be done regardless of whether there are any
// edit operations or not; since, the contributed actions need to be
// updated and the editor needs to be undirtied
if (flushed && (fModel instanceof IEditable)) {
((IEditable) fModel).setDirty(false);
}
}
protected boolean isNewlineNeeded(IDocument doc) throws BadLocationException {
return PropertiesUtil.isNewlineNeeded(doc);
}
protected static void insert(TextEdit parent, TextEdit edit) {
if (!parent.hasChildren()) {
parent.addChild(edit);
if (edit instanceof MoveSourceEdit) {
parent.addChild(((MoveSourceEdit) edit).getTargetEdit());
}
return;
}
TextEdit[] children = parent.getChildren();
// First dive down to find the right parent.
for (TextEdit child : children) {
if (covers(child, edit)) {
insert(child, edit);
return;
}
}
// We have the right parent. Now check if some of the children have to
// be moved under the new edit since it is covering it.
for (int i = children.length - 1; i >= 0; i--) {
TextEdit child = children[i];
if (covers(edit, child)) {
parent.removeChild(i);
edit.addChild(child);
}
}
parent.addChild(edit);
if (edit instanceof MoveSourceEdit) {
parent.addChild(((MoveSourceEdit) edit).getTargetEdit());
}
}
protected static boolean covers(TextEdit thisEdit, TextEdit otherEdit) {
if (thisEdit.getLength() == 0) // an insertion point can't cover anything
return false;
int thisOffset = thisEdit.getOffset();
int thisEnd = thisEdit.getExclusiveEnd();
if (otherEdit.getLength() == 0) {
int otherOffset = otherEdit.getOffset();
return thisOffset < otherOffset && otherOffset < thisEnd;
}
int otherOffset = otherEdit.getOffset();
int otherEnd = otherEdit.getExclusiveEnd();
return thisOffset <= otherOffset && otherEnd <= thisEnd;
}
public boolean mustSave() {
if (!fIsSourceMode) {
if (fModel instanceof IEditable) {
if (((IEditable) fModel).isDirty()) {
return true;
}
}
}
return !fEditOperations.isEmpty() || fDocumentProvider.canSaveDocument(fEditorInput);
}
public void dispose() {
IAnnotationModel amodel = fDocumentProvider.getAnnotationModel(fEditorInput);
if (amodel != null)
amodel.disconnect(fDocumentProvider.getDocument(fEditorInput));
fDocumentProvider.removeElementStateListener(fElementListener);
fDocumentProvider.disconnect(fEditorInput);
if (fModelListener != null && fModel instanceof IModelChangeProvider) {
((IModelChangeProvider) fModel).removeModelChangedListener(fModelListener);
//if (undoManager != null)
//undoManager.disconnect((IModelChangeProvider) model);
}
if (fModel != null)
fModel.dispose();
}
/**
* @return Returns the primary.
*/
public boolean isPrimary() {
return fPrimary;
}
/**
* @param primary The primary to set.
*/
public void setPrimary(boolean primary) {
this.fPrimary = primary;
}
public boolean setSourceEditingMode(boolean sourceMode) {
fIsSourceMode = sourceMode;
if (sourceMode) {
// entered source editing mode; in this mode,
// this context's document will be edited directly
// in the source editor. All changes in the model
// are caused by reconciliation and should not be
// fired to the world.
flushModel(fDocumentProvider.getDocument(fEditorInput));
if (fModel instanceof IEditable) {
if (!((IEditable) fModel).isDirty()) {
fMustSynchronize = false;
return true;
}
}
fMustSynchronize = true;
return true;
}
// leaving source editing mode; if the document
// has been modified while in this mode,
// fire the 'world changed' event from the model
// to cause all the model listeners to become stale.
return synchronizeModelIfNeeded();
}
private boolean synchronizeModelIfNeeded() {
if (fMustSynchronize) {
boolean result = synchronizeModel(fDocumentProvider.getDocument(fEditorInput));
fMustSynchronize = false;
return result;
}
return true;
}
public void doRevert() {
fMustSynchronize = true;
synchronizeModelIfNeeded();
/*
if (model instanceof IEditable) {
((IEditable)model).setDirty(false);
}
*/
}
public boolean isInSourceMode() {
return fIsSourceMode;
}
public boolean isModelCorrect() {
synchronizeModelIfNeeded();
return fModel != null ? fModel.isValid() : false;
}
protected boolean synchronizeModel(IDocument doc) {
return true;
}
public boolean matches(IResource resource) {
if (fEditorInput instanceof IFileEditorInput) {
IFileEditorInput finput = (IFileEditorInput) fEditorInput;
IFile file = finput.getFile();
if (file.equals(resource))
return true;
}
return false;
}
/**
* @return Returns the validated.
*/
public boolean isValidated() {
return fValidated;
}
/**
* @param validated The validated to set.
*/
public void setValidated(boolean validated) {
this.fValidated = validated;
}
public String getLineDelimiter() {
if (fDocumentProvider != null) {
IDocument document = fDocumentProvider.getDocument(fEditorInput);
if (document != null) {
return TextUtilities.getDefaultLineDelimiter(document);
}
}
return TextUtil.getDefaultLineDelimiter();
}
private void updateInput(IEditorInput newInput) throws CoreException {
deinitializeDocumentProvider();
fEditorInput = newInput;
initializeDocumentProvider();
}
private void deinitializeDocumentProvider() {
IAnnotationModel amodel = fDocumentProvider.getAnnotationModel(fEditorInput);
if (amodel != null) {
amodel.disconnect(fDocumentProvider.getDocument(fEditorInput));
}
fDocumentProvider.removeElementStateListener(fElementListener);
fDocumentProvider.disconnect(fEditorInput);
}
private void initializeDocumentProvider() throws CoreException {
fDocumentProvider.connect(fEditorInput);
IAnnotationModel amodel = fDocumentProvider.getAnnotationModel(fEditorInput);
if (amodel != null) {
amodel.connect(fDocumentProvider.getDocument(fEditorInput));
}
fDocumentProvider.addElementStateListener(fElementListener);
}
public void doSaveAs(IProgressMonitor monitor) throws Exception {
// Get the editor shell
Shell shell = getEditor().getSite().getShell();
// Create the save as dialog
SaveAsDialog dialog = new SaveAsDialog(shell);
// Set the initial file name to the original file name
IFile file = null;
if (fEditorInput instanceof IFileEditorInput) {
file = ((IFileEditorInput) fEditorInput).getFile();
dialog.setOriginalFile(file);
}
// Create the dialog
dialog.create();
// Warn the user if the underlying file does not exist
if (fDocumentProvider.isDeleted(fEditorInput) && (file != null)) {
String message = NLS.bind(PDEUIMessages.InputContext_errorMessageFileDoesNotExist, file.getName());
dialog.setErrorMessage(null);
dialog.setMessage(message, IMessageProvider.WARNING);
}
// Open the dialog
if (dialog.open() == Window.OK) {
// Get the path to where the new file will be stored
IPath path = dialog.getResult();
handleSaveAs(monitor, path);
}
}
private void handleSaveAs(IProgressMonitor monitor, IPath path) throws Exception, CoreException, InterruptedException, InvocationTargetException {
// Ensure a new location was selected
if (path == null) {
monitor.setCanceled(true);
throw new Exception(PDEUIMessages.InputContext_errorMessageLocationNotSet);
}
// Resolve the new file location
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IFile newFile = workspace.getRoot().getFile(path);
// Create the new editor input
final IEditorInput newInput = new FileEditorInput(newFile);
// Send notice of editor input changes
fDocumentProvider.aboutToChange(newInput);
// Flush any unsaved changes
flushModel(fDocumentProvider.getDocument(fEditorInput));
try {
// Execute the workspace modification in a separate thread
PlatformUI.getWorkbench().getProgressService().busyCursorWhile(createWorkspaceModifyOperation(newInput));
monitor.setCanceled(false);
// Store the new editor input in this context
updateInput(newInput);
} catch (InterruptedException | InvocationTargetException e) {
monitor.setCanceled(true);
throw e;
} finally {
fDocumentProvider.changed(newInput);
}
}
private WorkspaceModifyOperation createWorkspaceModifyOperation(final IEditorInput newInput) {
WorkspaceModifyOperation operation = new WorkspaceModifyOperation() {
@Override
public void execute(final IProgressMonitor monitor) throws CoreException {
// Save the old editor input content to the new editor input
// location
fDocumentProvider.saveDocument(monitor,
// New editor input location
newInput,
// Old editor input content
fDocumentProvider.getDocument(fEditorInput), true);
}
};
return operation;
}
}