package com.buildml.eclipse;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.operations.RedoActionHandler;
import org.eclipse.ui.operations.UndoActionHandler;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.part.MultiPageEditorPart;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import com.buildml.eclipse.actions.ActionsEditor;
import com.buildml.eclipse.files.FilesEditor;
import com.buildml.eclipse.outline.OutlinePage;
import com.buildml.eclipse.packages.PackageDiagramEditor;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.EclipsePartUtils;
import com.buildml.model.FatalBuildStoreError;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageMgrListener;
import com.buildml.model.IPackageRootMgr;
import com.buildml.refactor.IImportRefactorer;
import com.buildml.refactor.imports.ImportRefactorer;
/**
* The main Eclipse editor for editing/viewing BuildML files. This editor is a "multi-part"
* editor, since most of the work is done by sub-editors.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
public class MainEditor extends MultiPageEditorPart
implements IResourceChangeListener, IPackageMgrListener {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** the BuildStore we've opened for editing */
private IBuildStore buildStore = null;
/** The BuildStore's package manager */
private IPackageMgr pkgMgr;
/** The BuildStore's package root manager */
private IPackageRootMgr pkgRootMgr;
/** The refactorer that will manage refactoring (and its history) for this editor */
private IImportRefactorer importRefactorer;
/** the currently active tab index */
private int currentPageIndex = -1;
/** the tab that was most recently visible (before the current tab was made visible) */
private int previousPageIndex = -1;
/** The file that this editor has open. */
private File fileInput;
/** This editor's undo/redo context - applies to all sub editors */
private IUndoContext undoContext;
/** This editor's "undo" action - trigger by menu or keyboard shortcut (Ctrl-Z) */
private UndoActionHandler undoAction;
/** This editor's "redo" action - trigger by menu or keyboard shortcut (Ctrl-Y) */
private RedoActionHandler redoAction;
/** "dirty" state of this editor - true means that it needs saving */
private boolean editorIsDirty = false;
/** Counts the number of times the underlying BuildStore model has changed */
private long modelChangeCounter = 0;
/** This editor's associated outline page content */
private OutlinePage outlinePage = null;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new top-level BuildML Editor.
*/
public MainEditor() {
super();
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#init(org.eclipse.ui.IEditorSite, org.eclipse.ui.IEditorInput)
*/
@Override
public void init(IEditorSite site, IEditorInput input)
throws PartInitException {
if (! (input instanceof IURIEditorInput)) {
throw new PartInitException("Invalid Input: Must be IURIEditorInput");
}
super.init(site, input);
/* open the BuildStore file we're editing */
IURIEditorInput editorInput = (IURIEditorInput)input;
URI uri = editorInput.getURI();
if (uri == null) {
throw new PartInitException("Invalid Input: File URI is invalid");
}
fileInput = new File(uri.getPath());
// TODO: handle the case where multiple editors have the same BuildStore open.
// TODO: put a top-level catch for FatalBuildStoreException() (and other
// exceptions) to display a meaningful error.
buildStore = EclipsePartUtils.getNewBuildStore(fileInput.getPath());
if (buildStore == null) {
throw new PartInitException("Can't open the BuildML database.");
}
pkgMgr = buildStore.getPackageMgr();
pkgRootMgr = buildStore.getPackageRootMgr();
/*
* Register to learn about changes to resources in our workspace. We might need to
* know if somebody deletes or renames the file we're editing.
*/
ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
/* Listen to changes in the package naming */
pkgMgr.addListener(this);
/*
* Create a refactor manager that will be responsible for doing refactoring
* (deletion, merging, etc) for this BuildStore. This also handles the undo/redo
* of those operations.
*/
importRefactorer = new ImportRefactorer(buildStore);
/* Ensure that our focus-gain activities are enabled */
setFocus();
}
/*-------------------------------------------------------------------------------------*/
/**
* Add the specified Editor to the MainEditor, as a new tab. The text of the tab
* will be set to be the same as the title of the window.
*
* @param editor The new editor instance to add within the tab. This would usually be
* a FilesEditor, ActionsEditor, or similar.
* @return The tab index of the newly added tab.
*/
public int newPage(ISubEditor editor) {
IEditorInput editorInput = getEditorInput();
int index = -1;
try {
/* set the new tab's text name */
index = addPage(editor, editorInput);
setPageText(index, " " + editor.getTitle() + " ");
/* if it has one, set the new tab's icon */
Image image = editor.getEditorImage();
if (image != null) {
setPageImage(index, image);
}
} catch (PartInitException e) {
ErrorDialog.openError(getSite().getShell(),
"Error creating nested editor", null, e.getStatus());
}
return index;
}
/*-------------------------------------------------------------------------------------*/
/**
* Remove the specified sub-editor, if removal is permitted.
* @param tabIndex The tab index of the sub-editor
*/
@Override
public void removePage(int tabIndex) {
IEditorPart editor = getEditor(tabIndex);
if (editor instanceof ISubEditor) {
ISubEditor subEditor = (ISubEditor)editor;
if (subEditor.hasFeature("removable")) {
int pageToReturnTo = previousPageIndex;
super.removePage(tabIndex);
if (pageToReturnTo != -1) {
/* have the index numbers changed due to removal? */
if (tabIndex < pageToReturnTo) {
pageToReturnTo--;
}
setActivePage(pageToReturnTo);
}
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Switch focus to the next editor tab.
*/
public void nextPage() {
int currentPage = getActivePage();
int pageCount = getPageCount();
if ((currentPage != -1) && (currentPage != pageCount - 1)) {
setActivePage(currentPage + 1);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Switch focus to the previous editor tab.
*/
public void previousPage() {
int currentPage = getActivePage();
if (currentPage > 0) {
setActivePage(currentPage - 1);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the name that's written on the current sub editor's tab.
* @param tabIndex The tab index of the sub-editor.
* @return The tab's current text.
*/
public String getPageName(int tabIndex) {
String text = getPageText(tabIndex);
return text.substring(1, text.length() - 1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#pageChange(int)
*/
@Override
protected void pageChange(int newPageIndex) {
super.pageChange(newPageIndex);
/* trigger the pageChange() method on the sub-editor, so it can update the UI */
ISubEditor subEditor = (ISubEditor)getActiveEditor();
if (subEditor != null) {
subEditor.pageChange();
/* remember the previous actively page, so it's easier to return to */
int activatingPageIndex = getActivePage();
if (currentPageIndex != activatingPageIndex) {
previousPageIndex = currentPageIndex;
currentPageIndex = activatingPageIndex;
}
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#createPages()
*/
@Override
protected void createPages() {
IEditorInput editorInput = getEditorInput();
/* create the file editor tab */
FilesEditor editor1 = new FilesEditor(buildStore, "Files");
editor1.setRemovable(false);
newPage(editor1);
/* create the action editor tab */
ActionsEditor editor2 = new ActionsEditor(buildStore, "Actions");
editor2.setRemovable(false);
newPage(editor2);
/* open the "Main" package */
openPackageDiagram(pkgMgr.getId("Main"));
/* update the editor title with the name of the input file */
setPartName(editorInput.getName());
setTitleToolTip(editorInput.getToolTipText());
/*
* Attach a double-click listener to the MultiPageEditorPart, so that if the
* user double-clicks on one of the tabs at the bottom of the editor, we
* can bring up a Dialog box that will allow them to change the tab name.
*/
if (getContainer() instanceof CTabFolder)
{
CTabFolder folder = (CTabFolder)getContainer();
folder.addListener(SWT.MouseDoubleClick, new Listener() {
@Override
public void handleEvent(Event event) {
renameTab();
}
});
}
/*
* Create a new undo/redo context for this editor (and all sub-editors). Next,
* create Eclipse UI actions that can be added to the global edit menu.
*/
undoContext = new ObjectUndoContext(this);
undoAction = new UndoActionHandler(getSite(), undoContext);
redoAction = new RedoActionHandler(getSite(), undoContext);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.EditorPart#doSave(org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public void doSave(IProgressMonitor monitor) {
try {
IFile file = ((IFileEditorInput) getEditorInput()).getFile();
/* save the database to disk */
String absPath = EclipsePartUtils.workspaceRelativeToAbsolutePath(
file.getFullPath().toOSString());
buildStore.saveAs(absPath);
/*
* Update Eclipse's resources, so it notices that the file has changed. This
* is only necessary if the file is in the workspace.
*/
if (getEditorInput() instanceof IFileEditorInput) {
try {
file.refreshLocal(IFile.DEPTH_ONE, monitor);
} catch (CoreException e) {
throw new FatalBuildStoreError("Unable to update file resource after save", e);
}
}
/* save was successful - notify other parts of Eclipse */
editorIsDirty = false;
firePropertyChange(PROP_DIRTY);
/* undo/redo change is now reset */
modelChangeCounter = 0;
} catch (IOException e) {
AlertDialog.displayErrorDialog("Error Saving Database",
"The database could not be saved to disk. Reason: " + e.getMessage());
}
}
/*-------------------------------------------------------------------------------------*/
/*
* (non-Javadoc)
* @see org.eclipse.ui.part.EditorPart#doSaveAs()
*/
@Override
public void doSaveAs() {
/*
* Open a "save as" dialog box that queries the user for the new file
* name to save as.
*/
Shell shell = getSite().getWorkbenchWindow().getShell();
SaveAsDialog dialog = new SaveAsDialog(shell);
dialog.setOriginalName("Copy of " + getPartName());
dialog.open();
/* if the user provided a file name (as opposed to hitting cancel) */
final IPath newPath = dialog.getResult();
if (newPath != null) {
try {
/* save the file to the newly-selected path */
String absPath = EclipsePartUtils.workspaceRelativeToAbsolutePath(newPath.toOSString());
buildStore.saveAs(absPath);
/* update Eclipse's resources to reflect the new file */
IFile newFile = ResourcesPlugin.getWorkspace().getRoot().getFile(newPath);
try {
newFile.refreshLocal(IFile.DEPTH_ONE, null);
} catch (CoreException e) {
throw new FatalBuildStoreError("Unable to update file resource after save", e);
}
/*
* Set this editor's input to refer to the new file, since
* the old file is no longer "attached" to this editor. As
* a result, we mark the editor as "not dirty", as well
* as notifying other parts of Eclipse that our input has changed.
*/
setInput(new FileEditorInput(newFile));
setPartName(newFile.getName());
editorIsDirty = false;
firePropertyChange(PROP_DIRTY);
firePropertyChange(PROP_INPUT);
/* undo/redo change is now reset */
modelChangeCounter = 0;
} catch (IOException e) {
AlertDialog.displayErrorDialog("Error Saving Database",
"The database could not be saved to disk. Reason: " + e.getMessage());
}
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.EditorPart#isSaveAsAllowed()
*/
@Override
public boolean isSaveAsAllowed() {
return true;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#isDirty()
*/
@Override
public boolean isDirty() {
return editorIsDirty;
}
/*-------------------------------------------------------------------------------------*/
/**
* The content of the editor has changed, so mark the editor as dirty (the "save" menu
* option will now be available). The caller must provide an increment to indicate whether
* the recent change is moving forward (+1) or backward/undo (-1). The "*" on the title
* of the editor tab will indicate whether the content is different from what was last
* saved.
*
* @param incr +1 or -1, to indicate whether the stream of changes is moving forward or backward.
*/
public void markDirty(int incr) {
modelChangeCounter += incr;
/* tell Eclipse that we're changing our dirty state (it should put a * in our title). */
boolean oldState = editorIsDirty;
editorIsDirty = (modelChangeCounter != 0);
if (oldState != editorIsDirty) {
firePropertyChange(PROP_DIRTY);
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#dispose()
*/
@Override
public void dispose() {
/* close the BuildStore to release resources */
if (buildStore != null) {
buildStore.close();
pkgMgr.removeListener(this);
}
/* stop listening to resource changes */
ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
super.dispose();
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The BuildStore associated with this instance of the editor
*/
public IBuildStore getBuildStore() {
return buildStore;
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the editor that's currently visible (on the currently selected page). This
* editor (for example, a FilesEditor instance) will be the target of any selection
* operations that occur.
* @return The editor that's currently visible.
*/
public ISubEditor getActiveSubEditor() {
return (ISubEditor) this.getActiveEditor();
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The IImportRefactorer that manages the refactoring operations on this editor.
*/
public IImportRefactorer getImportRefactorer() {
return importRefactorer;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The file that this editor is editing.
*/
public File getFile() {
return fileInput;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return This editor's undo/redo context, which is the central queue for all of
* this editor's operations that can be undone and redone.
*/
public IUndoContext getUndoContext()
{
return undoContext;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.MultiPageEditorPart#setFocus()
*/
@Override
public void setFocus() {
super.setFocus();
/*
* Our editor (or a sub-editor) has just become active. Ensure that the global
* "redo" and "undo" menu items (and keyboard shortcuts) are registered to
* use our queue of operations.
*/
EclipsePartUtils.setActiveMainEditor(this);
if ((undoAction != null) && (redoAction != null)) {
IActionBars actionBars = getEditorSite().getActionBars();
actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
actionBars.updateActionBars();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Returns various objects that are associated with this BuildML editor.
*/
public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
/* Provides the content of the Outline view */
if (adapter.equals(IContentOutlinePage.class)) {
if (outlinePage == null) {
outlinePage = new OutlinePage(this, undoAction, redoAction);
}
return outlinePage;
}
return super.getAdapter(adapter);
}
/*-------------------------------------------------------------------------------------*/
/**
* This method is called whenever a resource in our project is modified. We only
* care about this if somebody has deleted, moved or renamed the file that we're editing.
* Otherwise this event is ignored.
*/
@Override
public void resourceChanged(IResourceChangeEvent event) {
IResourceDelta changes = event.getDelta();
try {
final String workspaceFileInput =
EclipsePartUtils.absoluteToWorkspaceRelativePath(fileInput.toString());
/* process each file change that was made... */
changes.accept(new IResourceDeltaVisitor() {
public boolean visit(IResourceDelta change) {
if (change.getResource().getType() == IResource.FILE) {
/*
* Is the changed file the file that we're editing? Also, is the
* change a "remove" operation (a delete or a rename).
*/
IPath location = change.getFullPath();
if (location.toString().equals(workspaceFileInput) &&
(change.getKind() == IResourceDelta.REMOVED)) {
/* was the file renamed? */
if (change.getFlags() == IResourceDelta.MOVED_TO) {
final File newFile = new File(Platform.getLocation().toOSString(),
change.getMovedToPath().toOSString());
/*
* if so, record the new name, and proceed to change the name
* of the editor tab (in a UI thread) to be the basename of the
* file's path. Also, update the editor's "input" so that Eclipse
* knows that the editor content has moved.
*/
fileInput = newFile;
IResource newIFile = ResourcesPlugin.getWorkspace().getRoot().
findMember(change.getMovedToPath());
setInput(new FileEditorInput((IFile) newIFile));
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
String newPartName = newFile.toString();
int lastSlash = newPartName.lastIndexOf('/');
if (lastSlash == -1) {
MainEditor.this.setPartName(newPartName);
} else {
MainEditor.this.setPartName(newPartName.substring(lastSlash + 1));
}
}
});
}
/* no, the file was completely deleted - close the editor */
else {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
IWorkbenchPage page = MainEditor.this.getEditorSite().getPage();
page.closeEditor(MainEditor.this, false);
}
});
}
}
};
return true;
}
});
}
catch (CoreException exception) {
/* nothing */
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Open a new package diagram, or if such a diagram already exists, bring it to the front.
*
* @param pkgId PackageMgr ID of the package to be displayed.
*/
public void openPackageDiagram(int pkgId) {
/* first, see if a suitable editor is already open */
int index = findPackageDiagramById(pkgId);
if (index == -1) {
/* no existing editor for this package, open a new one */
PackageDiagramEditor newEditor = new PackageDiagramEditor(buildStore, pkgId);
index = newPage(newEditor);
}
setActivePage(index);
}
/*-------------------------------------------------------------------------------------*/
/**
* A package has been renamed or modified in some way, so make sure any open editors
* are updated from the model.
* @param pkgId The ID of the package that was updated.
* @param how The way in which is was changed.
*/
@Override
public void packageChangeNotification(int pkgId, int how) {
/* for now, we only care about name changes */
if (how == IPackageMgrListener.CHANGED_NAME) {
int index = findPackageDiagramById(pkgId);
if (index != -1) {
/* update the editor tab's text */
String pkgName = pkgMgr.getName(pkgId);
setPageText(index, "Package: " + pkgName);
}
}
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Invoked whenever the user double-clicks on the "tab name" at the bottom of the
* editor window. This brings up a dialog box allowing the user to change the
* tab's name.
*/
private void renameTab() {
/* fetch the current tab text, being careful to remove trailing and leading " ". */
int pageIndex = getActivePage();
String currentTabName = getPageText(pageIndex);
currentTabName = currentTabName.substring(1, currentTabName.length() - 1);
/* Open the dialog box, to allow the user to modify the name */
EditorTabNameChangeDialog dialog = new EditorTabNameChangeDialog();
dialog.setName(currentTabName);
dialog.open();
/* If the user pressed OK, set the new name */
if (dialog.getReturnCode() == Dialog.OK) {
String newTabName = dialog.getName();
if (pageIndex != -1) {
setPageText(pageIndex, " " + newTabName + " ");
}
}
dialog.close();
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a package ID number, return the tab index of the editor (if it's currently open).
*
* @param pkgId The package ID for which we want to find an open PackageDiagramEditor.
* @return The tab index of the open editor, or -1 if no such editor can be found.
*/
private int findPackageDiagramById(int pkgId) {
int numEditors = getPageCount();
for (int i = 0; i < numEditors; i++) {
IEditorPart subEditor = getEditor(i);
if (subEditor instanceof PackageDiagramEditor) {
if (((PackageDiagramEditor)subEditor).getPackageId() == pkgId) {
return i;
}
}
}
/* not found */
return -1;
}
/*-------------------------------------------------------------------------------------*/
}