/* * 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 <a> 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(); } } }