/******************************************************************************* * Copyright (c) 2011 Sebastian Benz. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Sebastian Benz - initial API and implementation ******************************************************************************/ package de.sebastianbenz.task.ui.editor; import java.io.File; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IWorkspaceRoot; 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.IStatus; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.emf.common.ui.URIEditorInput; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.common.util.WrappedException; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IURIEditorInput; import org.eclipse.ui.PartInitException; import org.eclipse.ui.ide.FileStoreEditorInput; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.xtext.ui.editor.XtextEditor; import com.google.inject.Inject; /** * This class extends the standard XtextEditor to make it capable of * opening and saving external files by managing them as linked resources. * * This implementation also supports temporary files than when saved will change to * a SaveAs operation (also see {@link TmpFileStoreEditorInput}). * * An editor customizer can also be bound and it will receive calls to manage the content * of the context menu (see {@link IExtXtextEditorCustomizer}). * * Also see {@link ExtLinkedXtextEditorMatchingStrategy} which is required to ensure multiple * editors are not opened for the same file. * */ public class ExtLinkedXtextEditor extends XtextEditor { @Inject private IExtXtextEditorCustomizer editorCustomizer; @Inject private ExtLinkedFileHelper linkedFileHelper; /** * Property for last saved location - property stored as persistent property in the * workspace root. * TODO: this should come from the customizer */ public static final QualifiedName LAST_SAVEAS_LOCATION = new QualifiedName( "de.sebastianbenz.task.editor", "lastSaveLocation"); /** * Does nothing except server as a place to set a breakpoint :) */ public ExtLinkedXtextEditor() { super(); } /** * When editor is disposed the unlinking behavior is triggered (depending on reason of dispose, the linked file * may be unlinked). * See {@link ExtLinkedFileHelper#unlinkInput(IFileEditorInput)} for more info, */ @Override public void dispose() { // Unlink the input if it was linked IEditorInput input = getEditorInput(); if(input instanceof IFileEditorInput) linkedFileHelper.unlinkInput((IFileEditorInput) input); super.dispose(); } /* * (non-Javadoc) * * @see org.eclipse.xtext.ui.editor.XtextEditor#doSave(org.eclipse.core.runtime.IProgressMonitor) */ @Override public void doSave(IProgressMonitor progressMonitor) { // If document is a temporary / untitled document, change "save" to "saveAs" final IEditorInput input = getEditorInput(); if(input instanceof IFileEditorInput && ((IFileEditorInput) input).getFile().isLinked() && ((IFileEditorInput) input).getFile().getProject().getName().equals( ExtLinkedFileHelper.AUTOLINK_PROJECT_NAME)) { String val; try { val = ((FileEditorInput) input).getFile().getPersistentProperty( TmpFileStoreEditorInput.UNTITLED_PROPERTY); } catch(CoreException e) { // Don't know what to do here - this is really bad, but doSave does not expect // any errors. throw new WrappedException(e); } if(val != null && "true".equals(val)) { doSaveAs(); return; } } super.doSave(progressMonitor); } /* * (non-Javadoc) * * @see org.eclipse.xtext.ui.editor.XtextEditor#doSaveAs() */ @Override public void doSaveAs() { super.doSaveAs(); } /** * Translates an incoming IEditorInput being an FilestoreEditorInput, or IURIEditorInput * that is not also a IFileEditorInput. * FilestoreEditorInput is used when opening external files in an IDE environment. * The result is that the regular XtextEditor gets an IEFSEditorInput which is also an * IStorageEditorInput. */ @Override public void init(IEditorSite site, IEditorInput input) throws PartInitException { // THE ISSUE HERE: // In the IDE, the File Open Dialog (and elsewhere) uses a FilestoreEditorInput class // which is an IDE specific implementation. // The state at this point: // 1. When creating a file, the IEditorInput is an IURIEditorInput // 2. The only (non IDE specific) interface implemented by FilestoreEditorInput is IURIEditorInput // 3. The creation of a file is however also an IFileEditorInput // // Remedy: if(input instanceof IURIEditorInput && !(input instanceof IFileEditorInput)) { java.net.URI uri = ((IURIEditorInput) input).getURI(); String name = ((IURIEditorInput) input).getName(); // Check if this is linkable input if(uri.getScheme().equals("file")) { IFile linkedFile = null; // check and process tmp file input (untitled) if(input instanceof TmpFileStoreEditorInput) try { linkedFile = linkedFileHelper.obtainLink(uri, true); linkedFile.setPersistentProperty(TmpFileStoreEditorInput.UNTITLED_PROPERTY, "true"); } catch(CoreException e) { throw new PartInitException(e.getStatus()); } else { linkedFile = linkedFileHelper.obtainLink(uri, false); } IFileEditorInput linkedInput = new FileEditorInput(linkedFile); super.init(site, linkedInput); } else { // use EMF URI (readonly) input - will open without validation though... // (Could be improved, i.e. perform a download, make readonly, and keep in project, // or stored in tmp, and processed as the other linked resources.. URIEditorInput uriInput = new URIEditorInput(URI.createURI(uri.toString()), name); super.init(site, uriInput); return; } return; } super.init(site, input); } /** * Overridden to allow customization of editor context menu via injected handler * * @see org.eclipse.ui.editors.text.TextEditor#editorContextMenuAboutToShow(org.eclipse.jface.action.IMenuManager) */ @Override protected void editorContextMenuAboutToShow(IMenuManager menu) { super.editorContextMenuAboutToShow(menu); editorCustomizer.customizeEditorContextMenu(menu); } // SaveAs support for linked files - saves them on local disc, not to workspace if file is in special // hidden external file link project. @Override protected void performSaveAs(IProgressMonitor progressMonitor) { Shell shell = getSite().getShell(); final IEditorInput input = getEditorInput(); // Customize save as if the file is linked, and it is in the special external link project // if(input instanceof IFileEditorInput && ((IFileEditorInput) input).getFile().isLinked() && ((IFileEditorInput) input).getFile().getProject().getName().equals( ExtLinkedFileHelper.AUTOLINK_PROJECT_NAME)) { final IEditorInput newInput; IDocumentProvider provider = getDocumentProvider(); // 1. If file is "untitled" suggest last save location // 2. ...otherwise use the file's location (i.e. likely to be a rename in same folder) // 3. If a "last save location" is unknown, use user's home // String suggestedName = null; String suggestedPath = null; { // is it "untitled" java.net.URI uri = ((IURIEditorInput) input).getURI(); String tmpProperty = null; try { tmpProperty = ((IFileEditorInput) input).getFile().getPersistentProperty( TmpFileStoreEditorInput.UNTITLED_PROPERTY); } catch(CoreException e) { // ignore - tmpProperty will be null } boolean isUntitled = tmpProperty != null && "true".equals(tmpProperty); // suggested name IPath oldPath = URIUtil.toPath(uri); // TODO: input.getName() is probably always correct suggestedName = isUntitled ? input.getName() : oldPath.lastSegment(); // suggested path try { suggestedPath = isUntitled ? ((IFileEditorInput) input).getFile().getWorkspace().getRoot().getPersistentProperty( LAST_SAVEAS_LOCATION) : oldPath.toOSString(); } catch(CoreException e) { // ignore, suggestedPath will be null } if(suggestedPath == null) { // get user.home suggestedPath = System.getProperty("user.home"); } } FileDialog dialog = new FileDialog(shell, SWT.SAVE); if(suggestedName != null) dialog.setFileName(suggestedName); if(suggestedPath != null) dialog.setFilterPath(suggestedPath); dialog.setFilterExtensions(new String[] { "*.todo", "*.taskpaper", "*.*" }); String path = dialog.open(); if(path == null) { if(progressMonitor != null) progressMonitor.setCanceled(true); return; } // Check whether file exists and if so, confirm overwrite final File localFile = new File(path); if(localFile.exists()) { MessageDialog overwriteDialog = new MessageDialog(shell, "Save As", null, path + " already exists.\nDo you want to replace it?", MessageDialog.WARNING, new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 1); // 'No' is the default if(overwriteDialog.open() != Window.OK) { if(progressMonitor != null) { progressMonitor.setCanceled(true); return; } } } IFileStore fileStore; try { fileStore = EFS.getStore(localFile.toURI()); } catch(CoreException ex) { String title = "Problems During Save As..."; String msg = "Save could not be completed. " + ex.getMessage(); MessageDialog.openError(shell, title, msg); return; } IFile file = getWorkspaceFile(fileStore); if(file != null) newInput = new FileEditorInput(file); else { IURIEditorInput uriInput = new FileStoreEditorInput(fileStore); java.net.URI uri = uriInput.getURI(); IFile linkedFile = linkedFileHelper.obtainLink(uri, false); newInput = new FileEditorInput(linkedFile); } if(provider == null) { // editor has been closed while the dialog was open return; } boolean success = false; try { provider.aboutToChange(newInput); provider.saveDocument(progressMonitor, newInput, provider.getDocument(input), true); success = true; } catch(CoreException x) { final IStatus status = x.getStatus(); if(status == null || status.getSeverity() != IStatus.CANCEL) { String title = "Problems During Save As..."; String msg = "Save could not be completed. " + x.getMessage(); MessageDialog.openError(shell, title, msg); } } finally { provider.changed(newInput); if(success) setInput(newInput); // the linked file must be removed (esp. if it is an "untitled" link). linkedFileHelper.unlinkInput(((IFileEditorInput) input)); // remember last saveAs location String lastLocation = URIUtil.toPath(((FileEditorInput) newInput).getURI()).toOSString(); try { ((FileEditorInput) newInput).getFile().getWorkspace().getRoot().setPersistentProperty( LAST_SAVEAS_LOCATION, lastLocation); } catch(CoreException e) { // ignore } } if(progressMonitor != null) progressMonitor.setCanceled(!success); return; } super.performSaveAs(progressMonitor); } private IFile getWorkspaceFile(IFileStore fileStore) { IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IFile[] files = workspaceRoot.findFilesForLocationURI(fileStore.toURI()); if(files != null && files.length == 1) return files[0]; return null; } }