blob: 2e60df5bfe9aab1bc0e0e206ab7b987d331108c3 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors;
import com.android.ide.eclipse.adt.AdtPlugin;
import org.eclipse.core.internal.filebuffers.SynchronizableDocument;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.swt.widgets.Display;
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.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.ui.forms.editor.IFormPage;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.events.IHyperlinkListener;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.part.MultiPageEditorPart;
import org.eclipse.ui.part.WorkbenchPart;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.wst.sse.ui.StructuredTextEditor;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Multi-page form editor for Android text files.
* <p/>
* It is designed to work with a {@link TextEditor} that will display a text file.
* <br/>
* Derived classes must implement createFormPages to create the forms before the
* source editor. This can be a no-op if desired.
*/
@SuppressWarnings("restriction")
public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
/** Preference name for the current page of this file */
private static final String PREF_CURRENT_PAGE = "_current_page";
/** Id string used to create the Android SDK browser */
private static String BROWSER_ID = "android"; //$NON-NLS-1$
/** Page id of the XML source editor, used for switching tabs programmatically */
public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
/** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
public static final int TEXT_WIDTH_HINT = 50;
/** Page index of the text editor (always the last page) */
private int mTextPageIndex;
/** The text editor */
private TextEditor mTextEditor;
/** flag set during page creation */
private boolean mIsCreatingPage = false;
private IDocument mDocument;
/**
* Creates a form editor.
*/
public AndroidTextEditor() {
super();
}
// ---- Abstract Methods ----
/**
* Creates the various form pages.
* <p/>
* Derived classes must implement this to add their own specific tabs.
*/
abstract protected void createFormPages();
/**
* Called by the base class {@link AndroidTextEditor} once all pages (custom form pages
* as well as text editor page) have been created. This give a chance to deriving
* classes to adjust behavior once the text page has been created.
*/
protected void postCreatePages() {
// Nothing in the base class.
}
/**
* Subclasses should override this method to process the new text model.
* This is called after the document has been edited.
*
* The base implementation is empty.
*
* @param event Specification of changes applied to document.
*/
protected void onDocumentChanged(DocumentEvent event) {
// pass
}
// ---- Base Class Overrides, Interfaces Implemented ----
/**
* Creates the pages of the multi-page editor.
*/
@Override
protected void addPages() {
createAndroidPages();
selectDefaultPage(null /* defaultPageId */);
}
/**
* Creates the page for the Android Editors
*/
protected void createAndroidPages() {
mIsCreatingPage = true;
createFormPages();
createTextEditor();
createUndoRedoActions();
postCreatePages();
mIsCreatingPage = false;
}
/**
* Returns whether the editor is currently creating its pages.
*/
public boolean isCreatingPages() {
return mIsCreatingPage;
}
/**
* Creates undo redo actions for the editor site (so that it works for any page of this
* multi-page editor) by re-using the actions defined by the {@link TextEditor}
* (aka the XML text editor.)
*/
private void createUndoRedoActions() {
IActionBars bars = getEditorSite().getActionBars();
if (bars != null) {
IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
action = mTextEditor.getAction(ActionFactory.REDO.getId());
bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
bars.updateActionBars();
}
}
/**
* Selects the default active page.
* @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
* find the default page in the properties of the {@link IResource} object being edited.
*/
protected void selectDefaultPage(String defaultPageId) {
if (defaultPageId == null) {
if (getEditorInput() instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) getEditorInput()).getFile();
QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
getClass().getSimpleName() + PREF_CURRENT_PAGE);
String pageId;
try {
pageId = file.getPersistentProperty(qname);
if (pageId != null) {
defaultPageId = pageId;
}
} catch (CoreException e) {
// ignored
}
}
}
if (defaultPageId != null) {
try {
setActivePage(Integer.parseInt(defaultPageId));
} catch (Exception e) {
// We can get NumberFormatException from parseInt but also
// AssertionError from setActivePage when the index is out of bounds.
// Generally speaking we just want to ignore any exception and fall back on the
// first page rather than crash the editor load. Logging the error is enough.
AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
}
}
}
/**
* Removes all the pages from the editor.
*/
protected void removePages() {
int count = getPageCount();
for (int i = count - 1 ; i >= 0 ; i--) {
removePage(i);
}
}
/**
* Overrides the parent's setActivePage to be able to switch to the xml editor.
*
* If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
* This is needed because the editor doesn't actually derive from IFormPage and thus
* doesn't have the get-by-page-id method. In this case, the method returns null since
* IEditorPart does not implement IFormPage.
*/
@Override
public IFormPage setActivePage(String pageId) {
if (pageId.equals(TEXT_EDITOR_ID)) {
super.setActivePage(mTextPageIndex);
return null;
} else {
return super.setActivePage(pageId);
}
}
/**
* Notifies this multi-page editor that the page with the given id has been
* activated. This method is called when the user selects a different tab.
*
* @see MultiPageEditorPart#pageChange(int)
*/
@Override
protected void pageChange(int newPageIndex) {
super.pageChange(newPageIndex);
// Do not record page changes during creation of pages
if (mIsCreatingPage) {
return;
}
if (getEditorInput() instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) getEditorInput()).getFile();
QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
getClass().getSimpleName() + PREF_CURRENT_PAGE);
try {
file.setPersistentProperty(qname, Integer.toString(newPageIndex));
} catch (CoreException e) {
// ignore
}
}
}
/**
* Notifies this listener that some resource changes
* are happening, or have already happened.
*
* Closes all project files on project close.
* @see IResourceChangeListener
*/
@Override
public void resourceChanged(final IResourceChangeEvent event) {
if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
@SuppressWarnings("hiding")
IWorkbenchPage[] pages = getSite().getWorkbenchWindow().getPages();
for (int i = 0; i < pages.length; i++) {
if (((FileEditorInput)mTextEditor.getEditorInput())
.getFile().getProject().equals(
event.getResource())) {
IEditorPart editorPart = pages[i].findEditor(mTextEditor
.getEditorInput());
pages[i].closeEditor(editorPart, true);
}
}
}
});
}
}
/**
* Initializes the editor part with a site and input.
* <p/>
* Checks that the input is an instance of {@link IFileEditorInput}.
*
* @see FormEditor
*/
@Override
public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
if (!(editorInput instanceof IFileEditorInput))
throw new PartInitException("Invalid Input: Must be IFileEditorInput");
super.init(site, editorInput);
}
/**
* Returns the {@link IFile} matching the editor's input or null.
* <p/>
* By construction, the editor input has to be an {@link IFileEditorInput} so it must
* have an associated {@link IFile}. Null can only be returned if this editor has no
* input somehow.
*/
public IFile getFile() {
if (getEditorInput() instanceof IFileEditorInput) {
return ((IFileEditorInput) getEditorInput()).getFile();
}
return null;
}
/**
* Removes attached listeners.
*
* @see WorkbenchPart
*/
@Override
public void dispose() {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
super.dispose();
}
/**
* Commit all dirty pages then saves the contents of the text editor.
* <p/>
* This works by committing all data to the XML model and then
* asking the Structured XML Editor to save the XML.
*
* @see IEditorPart
*/
@Override
public void doSave(IProgressMonitor monitor) {
commitPages(true /* onSave */);
// The actual "save" operation is done by the Structured XML Editor
getEditor(mTextPageIndex).doSave(monitor);
}
/* (non-Javadoc)
* Saves the contents of this editor to another object.
* <p>
* Subclasses must override this method to implement the open-save-close lifecycle
* for an editor. For greater details, see <code>IEditorPart</code>
* </p>
*
* @see IEditorPart
*/
@Override
public void doSaveAs() {
commitPages(true /* onSave */);
IEditorPart editor = getEditor(mTextPageIndex);
editor.doSaveAs();
setPageText(mTextPageIndex, editor.getTitle());
setInput(editor.getEditorInput());
}
/**
* Commits all dirty pages in the editor. This method should
* be called as a first step of a 'save' operation.
* <p/>
* This is the same implementation as in {@link FormEditor}
* except it fixes two bugs: a cast to IFormPage is done
* from page.get(i) <em>before</em> being tested with instanceof.
* Another bug is that the last page might be a null pointer.
* <p/>
* The incorrect casting makes the original implementation crash due
* to our {@link StructuredTextEditor} not being an {@link IFormPage}
* so we have to override and duplicate to fix it.
*
* @param onSave <code>true</code> if commit is performed as part
* of the 'save' operation, <code>false</code> otherwise.
* @since 3.3
*/
@Override
public void commitPages(boolean onSave) {
if (pages != null) {
for (int i = 0; i < pages.size(); i++) {
Object page = pages.get(i);
if (page != null && page instanceof IFormPage) {
IFormPage form_page = (IFormPage) page;
IManagedForm managed_form = form_page.getManagedForm();
if (managed_form != null && managed_form.isDirty()) {
managed_form.commit(onSave);
}
}
}
}
}
/* (non-Javadoc)
* Returns whether the "save as" operation is supported by this editor.
* <p>
* Subclasses must override this method to implement the open-save-close lifecycle
* for an editor. For greater details, see <code>IEditorPart</code>
* </p>
*
* @see IEditorPart
*/
@Override
public boolean isSaveAsAllowed() {
return false;
}
// ---- Local methods ----
/**
* Helper method that creates a new hyper-link Listener.
* Used by derived classes which need active links in {@link FormText}.
* <p/>
* This link listener handles two kinds of URLs:
* <ul>
* <li> Links starting with "http" are simply sent to a local browser.
* <li> Links starting with "file:/" are simply sent to a local browser.
* <li> Links starting with "page:" are expected to be an editor page id to switch to.
* <li> Other links are ignored.
* </ul>
*
* @return A new hyper-link listener for FormText to use.
*/
public final IHyperlinkListener createHyperlinkListener() {
return new HyperlinkAdapter() {
/**
* Switch to the page corresponding to the link that has just been clicked.
* For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
*/
@Override
public void linkActivated(HyperlinkEvent e) {
super.linkActivated(e);
String link = e.data.toString();
if (link.startsWith("http") || //$NON-NLS-1$
link.startsWith("file:/")) { //$NON-NLS-1$
openLinkInBrowser(link);
} else if (link.startsWith("page:")) { //$NON-NLS-1$
// Switch to an internal page
setActivePage(link.substring(5 /* strlen("page:") */));
}
}
};
}
/**
* Open the http link into a browser
*
* @param link The URL to open in a browser
*/
private void openLinkInBrowser(String link) {
try {
IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
} catch (PartInitException e1) {
// pass
} catch (MalformedURLException e1) {
// pass
}
}
/**
* Creates the XML source editor.
* <p/>
* Memorizes the index page of the source editor (it's always the last page, but the number
* of pages before can change.)
* <br/>
* Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
* Finally triggers modelChanged() on the model listener -- derived classes can use this
* to initialize the model the first time.
* <p/>
* Called only once <em>after</em> createFormPages.
*/
private void createTextEditor() {
try {
mTextEditor = new TextEditor();
int index = addPage(mTextEditor, getEditorInput());
mTextPageIndex = index;
setPageText(index, mTextEditor.getTitle());
IDocumentProvider provider = mTextEditor.getDocumentProvider();
mDocument = provider.getDocument(getEditorInput());
mDocument.addDocumentListener(new IDocumentListener() {
@Override
public void documentChanged(DocumentEvent event) {
onDocumentChanged(event);
}
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
// ignore
}
});
} catch (PartInitException e) {
ErrorDialog.openError(getSite().getShell(),
"Android Text Editor Error", null, e.getStatus());
}
}
/**
* Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
* the current file input.
* <p/>
* All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
* The actual document instance is a {@link SynchronizableDocument}, which creates a lock
* around read/set operations. The base API provided by {@link IDocument} provides ways to
* manipulate the document line per line or as a bulk.
*/
public IDocument getDocument() {
return mDocument;
}
/**
* Returns the {@link IProject} for the edited file.
*/
public IProject getProject() {
if (mTextEditor != null) {
IEditorInput input = mTextEditor.getEditorInput();
if (input instanceof FileEditorInput) {
FileEditorInput fileInput = (FileEditorInput)input;
IFile inputFile = fileInput.getFile();
if (inputFile != null) {
return inputFile.getProject();
}
}
}
return null;
}
/**
* Runs the given operation in the context of a document RewriteSession.
* Takes care of properly starting and stopping the operation.
* <p/>
* The operation itself should just access {@link #getDocument()} and use the
* normal document's API to manipulate it.
*
* @see #getDocument()
*/
public void wrapRewriteSession(Runnable operation) {
if (mDocument instanceof IDocumentExtension4) {
IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
DocumentRewriteSession session = null;
try {
session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
operation.run();
} catch(IllegalStateException e) {
AdtPlugin.log(e, "wrapRewriteSession failed");
e.printStackTrace();
} finally {
if (session != null) {
doc4.stopRewriteSession(session);
}
}
} else {
// Not an IDocumentExtension4? Unlikely. Try the operation anyway.
operation.run();
}
}
}