| /* |
| * Copyright (C) 2007 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 static org.eclipse.wst.sse.ui.internal.actions.StructuredTextEditorActionConstants.ACTION_NAME_FORMAT_DOCUMENT; |
| |
| import com.android.annotations.Nullable; |
| import com.android.ide.eclipse.adt.AdtConstants; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.AdtUtils; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; |
| import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; |
| import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; |
| import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; |
| import com.android.sdklib.IAndroidTarget; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.QualifiedName; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.IAction; |
| import org.eclipse.jface.dialogs.ErrorDialog; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.swt.custom.StyledText; |
| 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.IEditorReference; |
| import org.eclipse.ui.IEditorSite; |
| import org.eclipse.ui.IFileEditorInput; |
| import org.eclipse.ui.IURIEditorInput; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PartInitException; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.actions.ActionFactory; |
| import org.eclipse.ui.browser.IWorkbenchBrowserSupport; |
| 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.ide.IDEActionFactory; |
| import org.eclipse.ui.ide.IGotoMarker; |
| import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport; |
| import org.eclipse.ui.part.MultiPageEditorPart; |
| import org.eclipse.ui.part.WorkbenchPart; |
| import org.eclipse.ui.views.contentoutline.IContentOutlinePage; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.ui.StructuredTextEditor; |
| import org.eclipse.wst.sse.ui.internal.StructuredTextViewer; |
| import org.eclipse.wst.xml.core.internal.document.NodeContainer; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Node; |
| |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.Collections; |
| |
| /** |
| * Multi-page form editor for Android XML files. |
| * <p/> |
| * It is designed to work with a {@link StructuredTextEditor} that will display an XML 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") // Uses XML model, which has no non-restricted replacement yet |
| public abstract class AndroidXmlEditor extends FormEditor { |
| |
| /** Icon used for the XML source page. */ |
| public static final String ICON_XML_PAGE = "editor_page_source"; //$NON-NLS-1$ |
| |
| /** Preference name for the current page of this file */ |
| private static final String PREF_CURRENT_PAGE = "_current_page"; //$NON-NLS-1$ |
| |
| /** 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) */ |
| protected int mTextPageIndex; |
| /** The text editor */ |
| private StructuredTextEditor mTextEditor; |
| /** Listener for the XML model from the StructuredEditor */ |
| private XmlModelStateListener mXmlModelStateListener; |
| /** Listener to update the root node if the target of the file is changed because of a |
| * SDK location change or a project target change */ |
| private TargetChangeListener mTargetListener = null; |
| |
| /** flag set during page creation */ |
| private boolean mIsCreatingPage = false; |
| |
| /** |
| * Flag used to ignore XML model updates. For example, the flag is set during |
| * formatting. A format operation should completely preserve the semantics of the XML |
| * so the document listeners can use this flag to skip updating the model when edits |
| * are observed during a formatting operation |
| */ |
| private boolean mIgnoreXmlUpdate; |
| |
| /** |
| * Flag indicating we're inside {@link #wrapEditXmlModel(Runnable)}. |
| * This is a counter, which allows us to nest the edit XML calls. |
| * There is no pending operation when the counter is at zero. |
| */ |
| private int mIsEditXmlModelPending; |
| |
| /** |
| * Usually null, but during an editing operation, represents the highest |
| * node which should be formatted when the editing operation is complete. |
| */ |
| private UiElementNode mFormatNode; |
| |
| /** |
| * Whether {@link #mFormatNode} should be formatted recursively, or just |
| * the node itself (its arguments) |
| */ |
| private boolean mFormatChildren; |
| |
| /** |
| * Creates a form editor. |
| * <p/> |
| * Some derived classes will want to use {@link #addDefaultTargetListener()} |
| * to setup the default listener to monitor SDK target changes. This |
| * is no longer the default. |
| */ |
| public AndroidXmlEditor() { |
| super(); |
| } |
| |
| @Override |
| public void init(IEditorSite site, IEditorInput input) throws PartInitException { |
| super.init(site, input); |
| // Trigger a check to see if the SDK needs to be reloaded (which will |
| // invoke onSdkLoaded or ITargetChangeListener asynchronously as needed). |
| AdtPlugin.getDefault().refreshSdk(); |
| } |
| |
| /** |
| * Setups a default {@link ITargetChangeListener} that will call |
| * {@link #initUiRootNode(boolean)} when the SDK or the target changes. |
| */ |
| public void addDefaultTargetListener() { |
| if (mTargetListener == null) { |
| mTargetListener = new TargetChangeListener() { |
| @Override |
| public IProject getProject() { |
| return AndroidXmlEditor.this.getProject(); |
| } |
| |
| @Override |
| public void reload() { |
| commitPages(false /* onSave */); |
| |
| // recreate the ui root node always |
| initUiRootNode(true /*force*/); |
| } |
| }; |
| AdtPlugin.getDefault().addTargetListener(mTargetListener); |
| } |
| } |
| |
| // ---- Abstract Methods ---- |
| |
| /** |
| * Returns the root node of the UI element hierarchy manipulated by the current |
| * UI node editor. |
| */ |
| abstract public UiElementNode getUiRootNode(); |
| |
| /** |
| * 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 AndroidXmlEditor} 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. |
| } |
| |
| /** |
| * Creates the initial UI Root Node, including the known mandatory elements. |
| * @param force if true, a new UiManifestNode is recreated even if it already exists. |
| */ |
| abstract protected void initUiRootNode(boolean force); |
| |
| /** |
| * Subclasses should override this method to process the new XML Model, which XML |
| * root node is given. |
| * |
| * The base implementation is empty. |
| * |
| * @param xml_doc The XML document, if available, or null if none exists. |
| */ |
| abstract protected void xmlModelChanged(Document xml_doc); |
| |
| /** |
| * Controls whether XML models are ignored or not. |
| * |
| * @param ignore when true, ignore all subsequent XML model updates, when false start |
| * processing XML model updates again |
| */ |
| public void setIgnoreXmlUpdate(boolean ignore) { |
| mIgnoreXmlUpdate = ignore; |
| } |
| |
| /** |
| * Returns whether XML model events are ignored or not. This is the case |
| * when we are deliberately modifying the document in a way which does not |
| * change the semantics (such as formatting), or when we have already |
| * directly updated the model ourselves. |
| * |
| * @return true if XML events should be ignored |
| */ |
| public boolean getIgnoreXmlUpdate() { |
| return mIgnoreXmlUpdate; |
| } |
| |
| // ---- Base Class Overrides, Interfaces Implemented ---- |
| |
| @Override |
| public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) { |
| Object result = super.getAdapter(adapter); |
| |
| if (result != null && adapter.equals(IGotoMarker.class) ) { |
| final IGotoMarker gotoMarker = (IGotoMarker) result; |
| return new IGotoMarker() { |
| @Override |
| public void gotoMarker(IMarker marker) { |
| gotoMarker.gotoMarker(marker); |
| try { |
| // Lint markers should always jump to XML text |
| if (marker.getType().equals(AdtConstants.MARKER_LINT)) { |
| IEditorPart editor = AdtUtils.getActiveEditor(); |
| if (editor instanceof AndroidXmlEditor) { |
| AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor; |
| xmlEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); |
| } |
| } |
| } catch (CoreException e) { |
| AdtPlugin.log(e, null); |
| } |
| } |
| }; |
| } |
| |
| if (result == null && adapter == IContentOutlinePage.class) { |
| return getStructuredTextEditor().getAdapter(adapter); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Creates the pages of the multi-page editor. |
| */ |
| @Override |
| protected void addPages() { |
| createAndroidPages(); |
| selectDefaultPage(null /* defaultPageId */); |
| } |
| |
| /** |
| * Creates the page for the Android Editors |
| */ |
| public void createAndroidPages() { |
| mIsCreatingPage = true; |
| createFormPages(); |
| createTextEditor(); |
| updateActionBindings(); |
| postCreatePages(); |
| mIsCreatingPage = false; |
| } |
| |
| /** |
| * Returns whether the editor is currently creating its pages. |
| */ |
| public boolean isCreatingPages() { |
| return mIsCreatingPage; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * If the page is an instance of {@link IPageImageProvider}, the image returned by |
| * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab. |
| */ |
| @Override |
| public int addPage(IFormPage page) throws PartInitException { |
| int index = super.addPage(page); |
| if (page instanceof IPageImageProvider) { |
| setPageImage(index, ((IPageImageProvider) page).getPageImage()); |
| } |
| return index; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * If the editor is an instance of {@link IPageImageProvider}, the image returned by |
| * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab. |
| */ |
| @Override |
| public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException { |
| int index = super.addPage(editor, input); |
| if (editor instanceof IPageImageProvider) { |
| setPageImage(index, ((IPageImageProvider) editor).getPageImage()); |
| } |
| return index; |
| } |
| |
| /** |
| * Creates undo redo (etc) 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 StructuredTextEditor} |
| * (aka the XML text editor.) |
| */ |
| protected void updateActionBindings() { |
| 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.setGlobalActionHandler(ActionFactory.DELETE.getId(), |
| mTextEditor.getAction(ActionFactory.DELETE.getId())); |
| bars.setGlobalActionHandler(ActionFactory.CUT.getId(), |
| mTextEditor.getAction(ActionFactory.CUT.getId())); |
| bars.setGlobalActionHandler(ActionFactory.COPY.getId(), |
| mTextEditor.getAction(ActionFactory.COPY.getId())); |
| bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), |
| mTextEditor.getAction(ActionFactory.PASTE.getId())); |
| bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), |
| mTextEditor.getAction(ActionFactory.SELECT_ALL.getId())); |
| bars.setGlobalActionHandler(ActionFactory.FIND.getId(), |
| mTextEditor.getAction(ActionFactory.FIND.getId())); |
| bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(), |
| mTextEditor.getAction(IDEActionFactory.BOOKMARK.getId())); |
| |
| bars.updateActionBars(); |
| } |
| } |
| |
| /** |
| * Clears the action bindings for the editor site. |
| */ |
| protected void clearActionBindings(boolean includeUndoRedo) { |
| IActionBars bars = getEditorSite().getActionBars(); |
| if (bars != null) { |
| // For some reason, undo/redo doesn't seem to work in the form editor. |
| // This appears to be the case for pure Eclipse form editors too, e.g. see |
| // https://bugs.eclipse.org/bugs/show_bug.cgi?id=68423 |
| // However, as a workaround we can use the *text* editor's underlying undo |
| // to revert operations being done in the UI, and the form automatically updates. |
| // Therefore, to work around this, we simply leave the text editor bindings |
| // in place if {@code includeUndoRedo} is not set |
| if (includeUndoRedo) { |
| bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.REDO.getId(), null); |
| } |
| bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.CUT.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.COPY.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), null); |
| bars.setGlobalActionHandler(ActionFactory.FIND.getId(), null); |
| bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(), null); |
| |
| 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. |
| */ |
| public void selectDefaultPage(String defaultPageId) { |
| if (defaultPageId == null) { |
| IFile file = getInputFile(); |
| if (file != null) { |
| 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); |
| } |
| } else if (AdtPrefs.getPrefs().isXmlEditorPreferred(getPersistenceCategory())) { |
| setActivePage(mTextPageIndex); |
| } |
| } |
| |
| /** The layout editor */ |
| public static final int CATEGORY_LAYOUT = 1 << 0; |
| /** The manifest editor */ |
| public static final int CATEGORY_MANIFEST = 1 << 1; |
| /** Any other XML editor */ |
| public static final int CATEGORY_OTHER = 1 << 2; |
| |
| /** |
| * Returns the persistence category to use for this editor; this should be |
| * one of the {@code CATEGORY_} constants such as {@link #CATEGORY_MANIFEST}, |
| * {@link #CATEGORY_LAYOUT}, {@link #CATEGORY_OTHER}, ... |
| * <p> |
| * The persistence category is used to group editors together when it comes |
| * to certain types of persistence metadata. For example, whether this type |
| * of file was most recently edited graphically or with an XML text editor. |
| * We'll open new files in the same text or graphical mode as the last time |
| * the user edited a file of the same persistence category. |
| * <p> |
| * Before we added the persistence category, we had a single boolean flag |
| * recording whether the XML files were most recently edited graphically or |
| * not. However, this meant that users can't for example prefer to edit |
| * Manifest files graphically and string files via XML. By splitting the |
| * editors up into categories, we can track the mode at a finer granularity, |
| * and still allow similar editors such as those used for animations and |
| * colors to be treated the same way. |
| * |
| * @return the persistence category constant |
| */ |
| protected int getPersistenceCategory() { |
| return CATEGORY_OTHER; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| IFile file = getInputFile(); |
| if (file != null) { |
| QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, |
| getClass().getSimpleName() + PREF_CURRENT_PAGE); |
| try { |
| file.setPersistentProperty(qname, Integer.toString(newPageIndex)); |
| } catch (CoreException e) { |
| // ignore |
| } |
| } |
| |
| boolean isTextPage = newPageIndex == mTextPageIndex; |
| AdtPrefs.getPrefs().setXmlEditorPreferred(getPersistenceCategory(), isTextPage); |
| } |
| |
| /** |
| * Returns true if the active page is the editor page |
| * |
| * @return true if the active page is the editor page |
| */ |
| public boolean isEditorPageActive() { |
| return getActivePage() == mTextPageIndex; |
| } |
| |
| /** |
| * Returns the {@link IFile} matching the editor's input or null. |
| */ |
| @Nullable |
| public IFile getInputFile() { |
| IEditorInput input = getEditorInput(); |
| if (input instanceof IFileEditorInput) { |
| return ((IFileEditorInput) input).getFile(); |
| } |
| return null; |
| } |
| |
| /** |
| * Removes attached listeners. |
| * |
| * @see WorkbenchPart |
| */ |
| @Override |
| public void dispose() { |
| IStructuredModel xml_model = getModelForRead(); |
| if (xml_model != null) { |
| try { |
| if (mXmlModelStateListener != null) { |
| xml_model.removeModelStateListener(mXmlModelStateListener); |
| } |
| |
| } finally { |
| xml_model.releaseFromRead(); |
| } |
| } |
| |
| if (mTargetListener != null) { |
| AdtPlugin.getDefault().removeTargetListener(mTargetListener); |
| mTargetListener = null; |
| } |
| |
| 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 */); |
| |
| if (AdtPrefs.getPrefs().isFormatOnSave()) { |
| IAction action = mTextEditor.getAction(ACTION_NAME_FORMAT_DOCUMENT); |
| if (action != null) { |
| try { |
| mIgnoreXmlUpdate = true; |
| action.run(); |
| } finally { |
| mIgnoreXmlUpdate = false; |
| } |
| } |
| } |
| |
| // The actual "save" operation is done by the Structured XML Editor |
| getEditor(mTextPageIndex).doSave(monitor); |
| |
| // Check for errors on save, if enabled |
| if (AdtPrefs.getPrefs().isLintOnSave()) { |
| runLint(); |
| } |
| } |
| |
| /** |
| * Tells the editor to start a Lint check. |
| * It's up to the caller to check whether this should be done depending on preferences. |
| * <p/> |
| * The default implementation is to call {@link #startLintJob()}. |
| * |
| * @return The Job started by {@link EclipseLintRunner} or null if no job was started. |
| */ |
| protected Job runLint() { |
| return startLintJob(); |
| } |
| |
| /** |
| * Utility method that creates a Job to run Lint on the current document. |
| * Does not wait for the job to finish - just returns immediately. |
| * |
| * @return a new job, or null |
| * @see EclipseLintRunner#startLint(java.util.List, IResource, IDocument, |
| * boolean, boolean) |
| */ |
| @Nullable |
| public Job startLintJob() { |
| IFile file = getInputFile(); |
| if (file != null) { |
| return EclipseLintRunner.startLint(Collections.singletonList(file), file, |
| getStructuredDocument(), false /*fatalOnly*/, false /*show*/); |
| } |
| |
| return null; |
| } |
| |
| /* (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; |
| } |
| |
| /** |
| * Returns the page index of the text editor (always the last page) |
| |
| * @return the page index of the text editor (always the last page) |
| */ |
| public int getTextPageIndex() { |
| return mTextPageIndex; |
| } |
| |
| // ---- 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 StructuredTextEditor() { |
| @Override |
| protected void createActions() { |
| super.createActions(); |
| |
| Action action = new RenameResourceXmlTextAction(mTextEditor); |
| action.setActionDefinitionId(IJavaEditorActionDefinitionIds.RENAME_ELEMENT); |
| setAction(IJavaEditorActionDefinitionIds.RENAME_ELEMENT, action); |
| } |
| }; |
| int index = addPage(mTextEditor, getEditorInput()); |
| mTextPageIndex = index; |
| setPageText(index, mTextEditor.getTitle()); |
| setPageImage(index, |
| IconFactory.getInstance().getIcon(ICON_XML_PAGE)); |
| |
| if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) { |
| Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, |
| "Error opening the Android XML editor. Is the document an XML file?"); |
| throw new RuntimeException("Android XML Editor Error", new CoreException(status)); |
| } |
| |
| IStructuredModel xml_model = getModelForRead(); |
| if (xml_model != null) { |
| try { |
| mXmlModelStateListener = new XmlModelStateListener(); |
| xml_model.addModelStateListener(mXmlModelStateListener); |
| mXmlModelStateListener.modelChanged(xml_model); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$ |
| } finally { |
| xml_model.releaseFromRead(); |
| } |
| } |
| } catch (PartInitException e) { |
| ErrorDialog.openError(getSite().getShell(), |
| "Android XML Editor Error", null, e.getStatus()); |
| } |
| } |
| |
| /** |
| * Returns the ISourceViewer associated with the Structured Text editor. |
| */ |
| public final ISourceViewer getStructuredSourceViewer() { |
| if (mTextEditor != null) { |
| // We can't access mDelegate.getSourceViewer() because it is protected, |
| // however getTextViewer simply returns the SourceViewer casted, so we |
| // can use it instead. |
| return mTextEditor.getTextViewer(); |
| } |
| return null; |
| } |
| |
| /** |
| * Return the {@link StructuredTextEditor} associated with this XML editor |
| * |
| * @return the associated {@link StructuredTextEditor} |
| */ |
| public StructuredTextEditor getStructuredTextEditor() { |
| return mTextEditor; |
| } |
| |
| /** |
| * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source |
| * Editor) or null if not available. |
| */ |
| public IStructuredDocument getStructuredDocument() { |
| if (mTextEditor != null && mTextEditor.getTextViewer() != null) { |
| return (IStructuredDocument) mTextEditor.getTextViewer().getDocument(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a version of the model that has been shared for read. |
| * <p/> |
| * Callers <em>must</em> call model.releaseFromRead() when done, typically |
| * in a try..finally clause. |
| * |
| * Portability note: this uses getModelManager which is part of wst.sse.core; however |
| * the interface returned is part of wst.sse.core.internal.provisional so we can |
| * expect it to change in a distant future if they start cleaning their codebase, |
| * however unlikely that is. |
| * |
| * @return The model for the XML document or null if cannot be obtained from the editor |
| */ |
| public IStructuredModel getModelForRead() { |
| IStructuredDocument document = getStructuredDocument(); |
| if (document != null) { |
| IModelManager mm = StructuredModelManager.getModelManager(); |
| if (mm != null) { |
| // TODO simplify this by not using the internal IStructuredDocument. |
| // Instead we can now use mm.getModelForRead(getFile()). |
| // However we must first check that SSE for Eclipse 3.3 or 3.4 has this |
| // method. IIRC 3.3 didn't have it. |
| |
| return mm.getModelForRead(document); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a version of the model that has been shared for edit. |
| * <p/> |
| * Callers <em>must</em> call model.releaseFromEdit() when done, typically |
| * in a try..finally clause. |
| * <p/> |
| * Because of this, it is mandatory to use the wrapper |
| * {@link #wrapEditXmlModel(Runnable)} which executes a runnable into a |
| * properly configured model and then performs whatever cleanup is necessary. |
| * |
| * @return The model for the XML document or null if cannot be obtained from the editor |
| */ |
| private IStructuredModel getModelForEdit() { |
| IStructuredDocument document = getStructuredDocument(); |
| if (document != null) { |
| IModelManager mm = StructuredModelManager.getModelManager(); |
| if (mm != null) { |
| // TODO simplify this by not using the internal IStructuredDocument. |
| // Instead we can now use mm.getModelForRead(getFile()). |
| // However we must first check that SSE for Eclipse 3.3 or 3.4 has this |
| // method. IIRC 3.3 didn't have it. |
| |
| return mm.getModelForEdit(document); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Helper class to perform edits on the XML model whilst making sure the |
| * model has been prepared to be changed. |
| * <p/> |
| * It first gets a model for edition using {@link #getModelForEdit()}, |
| * then calls {@link IStructuredModel#aboutToChangeModel()}, |
| * then performs the requested action |
| * and finally calls {@link IStructuredModel#changedModel()} |
| * and {@link IStructuredModel#releaseFromEdit()}. |
| * <p/> |
| * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method |
| * is called, XML model listeners will be triggered. |
| * <p/> |
| * Calls can be nested: only the first outer call will actually start and close the edit |
| * session. |
| * <p/> |
| * This method is <em>not synchronized</em> and is not thread safe. |
| * Callers must be using it from the the main UI thread. |
| * |
| * @param editAction Something that will change the XML. |
| */ |
| public final void wrapEditXmlModel(Runnable editAction) { |
| wrapEditXmlModel(editAction, null); |
| } |
| |
| /** |
| * Perform any editor-specific hooks after applying an edit. When edits are |
| * nested, the hooks will only run after the final top level edit has been |
| * performed. |
| * <p> |
| * Note that the edit hooks are performed outside of the edit lock so |
| * the hooks should not perform edits on the model without acquiring |
| * a lock first. |
| */ |
| public void runEditHooks() { |
| if (!mIgnoreXmlUpdate) { |
| // Check for errors, if enabled |
| if (AdtPrefs.getPrefs().isLintOnSave()) { |
| runLint(); |
| } |
| } |
| } |
| |
| /** |
| * Executor which performs the given action under an edit lock (and optionally as a |
| * single undo event). |
| * |
| * @param editAction the action to be executed |
| * @param undoLabel if non null, the edit action will be run as a single undo event |
| * and the label used as the name of the undoable action |
| */ |
| private final void wrapEditXmlModel(final Runnable editAction, final String undoLabel) { |
| Display display = mTextEditor.getSite().getShell().getDisplay(); |
| if (display.getThread() != Thread.currentThread()) { |
| display.syncExec(new Runnable() { |
| @Override |
| public void run() { |
| if (!mTextEditor.getTextViewer().getControl().isDisposed()) { |
| wrapEditXmlModel(editAction, undoLabel); |
| } |
| } |
| }); |
| return; |
| } |
| |
| IStructuredModel model = null; |
| int undoReverseCount = 0; |
| try { |
| |
| if (mIsEditXmlModelPending == 0) { |
| try { |
| model = getModelForEdit(); |
| if (undoLabel != null) { |
| // Run this action as an undoable unit. |
| // We have to do it more than once, because in some scenarios |
| // Eclipse WTP decides to cancel the current undo command on its |
| // own -- see http://code.google.com/p/android/issues/detail?id=15901 |
| // for one such call chain. By nesting these calls several times |
| // we've incrementing the command count such that a couple of |
| // cancellations are ignored. Interfering with this mechanism may |
| // sound dangerous, but it appears that this undo-termination is |
| // done for UI reasons to anticipate what the user wants, and we know |
| // that in *our* scenarios we want the entire unit run as a single |
| // unit. Here's what the documentation for |
| // IStructuredTextUndoManager#forceEndOfPendingCommand says |
| // "Normally, the undo manager can figure out the best |
| // times when to end a pending command and begin a new |
| // one ... to the structure of a structured |
| // document. There are times, however, when clients may |
| // wish to override those algorithms and end one earlier |
| // than normal. The one known case is for multi-page |
| // editors. If a user is on one page, and type '123' as |
| // attribute value, then click around to other parts of |
| // page, or different pages, then return to '123|' and |
| // type 456, then "undo" they typically expect the undo |
| // to just undo what they just typed, the 456, not the |
| // whole attribute value." |
| for (int i = 0; i < 4; i++) { |
| model.beginRecording(this, undoLabel); |
| undoReverseCount++; |
| } |
| } |
| model.aboutToChangeModel(); |
| } catch (Throwable t) { |
| // This is never supposed to happen unless we suddenly don't have a model. |
| // If it does, we don't want to even try to modify anyway. |
| AdtPlugin.log(t, "XML Editor failed to get model to edit"); //$NON-NLS-1$ |
| return; |
| } |
| } |
| mIsEditXmlModelPending++; |
| editAction.run(); |
| } finally { |
| mIsEditXmlModelPending--; |
| if (model != null) { |
| try { |
| boolean oldIgnore = mIgnoreXmlUpdate; |
| try { |
| mIgnoreXmlUpdate = true; |
| |
| if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) { |
| if (mFormatNode == getUiRootNode()) { |
| reformatDocument(); |
| } else { |
| Node node = mFormatNode.getXmlNode(); |
| if (node instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion) node; |
| int begin = region.getStartOffset(); |
| int end = region.getEndOffset(); |
| |
| if (!mFormatChildren) { |
| // This will format just the attribute list |
| end = begin + 1; |
| } |
| |
| if (mFormatChildren |
| && node == node.getOwnerDocument().getDocumentElement()) { |
| reformatDocument(); |
| } else { |
| reformatRegion(begin, end); |
| } |
| } |
| } |
| mFormatNode = null; |
| mFormatChildren = false; |
| } |
| |
| // Notify the model we're done modifying it. This must *always* be executed. |
| model.changedModel(); |
| |
| // Clean up the undo unit. This is done more than once as explained |
| // above for beginRecording. |
| for (int i = 0; i < undoReverseCount; i++) { |
| model.endRecording(this); |
| } |
| } finally { |
| mIgnoreXmlUpdate = oldIgnore; |
| } |
| } catch (Exception e) { |
| AdtPlugin.log(e, "Failed to clean up undo unit"); |
| } |
| model.releaseFromEdit(); |
| |
| if (mIsEditXmlModelPending < 0) { |
| AdtPlugin.log(IStatus.ERROR, |
| "wrapEditXmlModel finished with invalid nested counter==%1$d", //$NON-NLS-1$ |
| mIsEditXmlModelPending); |
| mIsEditXmlModelPending = 0; |
| } |
| |
| runEditHooks(); |
| |
| // Notify listeners |
| IStructuredModel readModel = getModelForRead(); |
| if (readModel != null) { |
| try { |
| mXmlModelStateListener.modelChanged(readModel); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "Error while notifying changes"); //$NON-NLS-1$ |
| } finally { |
| readModel.releaseFromRead(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Does this editor participate in the "format GUI editor changes" option? |
| * |
| * @return true if this editor supports automatically formatting XML |
| * affected by GUI changes |
| */ |
| public boolean supportsFormatOnGuiEdit() { |
| return false; |
| } |
| |
| /** |
| * Mark the given node as needing to be formatted when the current edits are |
| * done, provided the user has turned that option on (see |
| * {@link AdtPrefs#getFormatGuiXml()}). |
| * |
| * @param node the node to be scheduled for formatting |
| * @param attributesOnly if true, only update the attributes list of the |
| * node, otherwise update the node recursively (e.g. all children |
| * too) |
| */ |
| public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) { |
| if (!supportsFormatOnGuiEdit()) { |
| return; |
| } |
| |
| if (node == mFormatNode) { |
| if (!attributesOnly) { |
| mFormatChildren = true; |
| } |
| } else if (mFormatNode == null) { |
| mFormatNode = node; |
| mFormatChildren = !attributesOnly; |
| } else { |
| if (mFormatNode.isAncestorOf(node)) { |
| mFormatChildren = true; |
| } else if (node.isAncestorOf(mFormatNode)) { |
| mFormatNode = node; |
| mFormatChildren = true; |
| } else { |
| // Two independent nodes; format their closest common ancestor. |
| // Later we could consider having a small number of independent nodes |
| // and formatting those, and only switching to formatting the common ancestor |
| // when the number of individual nodes gets large. |
| mFormatChildren = true; |
| mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node); |
| } |
| } |
| } |
| |
| /** |
| * Creates an "undo recording" session by calling the undoableAction runnable |
| * under an undo session. |
| * <p/> |
| * This also automatically starts an edit XML session, as if |
| * {@link #wrapEditXmlModel(Runnable)} had been called. |
| * <p> |
| * You can nest several calls to {@link #wrapUndoEditXmlModel(String, Runnable)}, only one |
| * recording session will be created. |
| * |
| * @param label The label for the undo operation. Can be null. Ideally we should really try |
| * to put something meaningful if possible. |
| * @param undoableAction the action to be run as a single undoable unit |
| */ |
| public void wrapUndoEditXmlModel(String label, Runnable undoableAction) { |
| assert label != null : "All undoable actions should have a label"; |
| wrapEditXmlModel(undoableAction, label == null ? "" : label); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns true when the runnable of {@link #wrapEditXmlModel(Runnable)} is currently |
| * being executed. This means it is safe to actually edit the XML model. |
| * |
| * @return true if the XML model is already locked for edits |
| */ |
| public boolean isEditXmlModelPending() { |
| return mIsEditXmlModelPending > 0; |
| } |
| |
| /** |
| * Returns the XML {@link Document} or null if we can't get it |
| */ |
| public final Document getXmlDocument(IStructuredModel model) { |
| if (model == null) { |
| AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$ |
| return null; |
| } |
| |
| if (model instanceof IDOMModel) { |
| IDOMModel dom_model = (IDOMModel) model; |
| return dom_model.getDocument(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the {@link IProject} for the edited file. |
| */ |
| @Nullable |
| public IProject getProject() { |
| IFile file = getInputFile(); |
| if (file != null) { |
| return file.getProject(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the {@link AndroidTargetData} for the edited file. |
| */ |
| @Nullable |
| public AndroidTargetData getTargetData() { |
| IProject project = getProject(); |
| if (project != null) { |
| Sdk currentSdk = Sdk.getCurrent(); |
| if (currentSdk != null) { |
| IAndroidTarget target = currentSdk.getTarget(project); |
| |
| if (target != null) { |
| return currentSdk.getTargetData(target); |
| } |
| } |
| } |
| |
| IEditorInput input = getEditorInput(); |
| if (input instanceof IURIEditorInput) { |
| IURIEditorInput urlInput = (IURIEditorInput) input; |
| Sdk currentSdk = Sdk.getCurrent(); |
| if (currentSdk != null) { |
| try { |
| String path = AdtUtils.getFile(urlInput.getURI().toURL()).getPath(); |
| IAndroidTarget[] targets = currentSdk.getTargets(); |
| for (IAndroidTarget target : targets) { |
| if (path.startsWith(target.getLocation())) { |
| return currentSdk.getTargetData(target); |
| } |
| } |
| } catch (MalformedURLException e) { |
| // File might be in some other weird random location we can't |
| // handle: Just ignore these |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Shows the editor range corresponding to the given XML node. This will |
| * front the editor and select the text range. |
| * |
| * @param xmlNode The DOM node to be shown. The DOM node should be an XML |
| * node from the existing XML model used by the structured XML |
| * editor; it will not do attribute matching to find a |
| * "corresponding" element in the document from some foreign DOM |
| * tree. |
| * @return True if the node was shown. |
| */ |
| public boolean show(Node xmlNode) { |
| if (xmlNode instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion)xmlNode; |
| |
| IEditorPart textPage = getEditor(mTextPageIndex); |
| if (textPage instanceof StructuredTextEditor) { |
| StructuredTextEditor editor = (StructuredTextEditor) textPage; |
| |
| setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); |
| |
| // Note - we cannot use region.getLength() because that seems to |
| // always return 0. |
| int regionLength = region.getEndOffset() - region.getStartOffset(); |
| editor.selectAndReveal(region.getStartOffset(), regionLength); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Selects and reveals the given range in the text editor |
| * |
| * @param start the beginning offset |
| * @param length the length of the region to show |
| * @param frontTab if true, front the tab, otherwise just make the selection but don't |
| * change the active tab |
| */ |
| public void show(int start, int length, boolean frontTab) { |
| IEditorPart textPage = getEditor(mTextPageIndex); |
| if (textPage instanceof StructuredTextEditor) { |
| StructuredTextEditor editor = (StructuredTextEditor) textPage; |
| if (frontTab) { |
| setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); |
| } |
| editor.selectAndReveal(start, length); |
| if (frontTab) { |
| editor.setFocus(); |
| } |
| } |
| } |
| |
| /** |
| * Returns true if this editor has more than one page (usually a graphical view and an |
| * editor) |
| * |
| * @return true if this editor has multiple pages |
| */ |
| public boolean hasMultiplePages() { |
| return getPageCount() > 1; |
| } |
| |
| /** |
| * Get the XML text directly from the editor. |
| * |
| * @param xmlNode The node whose XML text we want to obtain. |
| * @return The XML representation of the {@link Node}, or null if there was an error. |
| */ |
| public String getXmlText(Node xmlNode) { |
| String data = null; |
| IStructuredModel model = getModelForRead(); |
| try { |
| IStructuredDocument document = getStructuredDocument(); |
| if (xmlNode instanceof NodeContainer) { |
| // The easy way to get the source of an SSE XML node. |
| data = ((NodeContainer) xmlNode).getSource(); |
| } else if (xmlNode instanceof IndexedRegion && document != null) { |
| // Try harder. |
| IndexedRegion region = (IndexedRegion) xmlNode; |
| int start = region.getStartOffset(); |
| int end = region.getEndOffset(); |
| |
| if (end > start) { |
| data = document.get(start, end - start); |
| } |
| } |
| } catch (BadLocationException e) { |
| // the region offset was invalid. ignore. |
| } finally { |
| model.releaseFromRead(); |
| } |
| return data; |
| } |
| |
| /** |
| * Formats the text around the given caret range, using the current Eclipse |
| * XML formatter settings. |
| * |
| * @param begin The starting offset of the range to be reformatted. |
| * @param end The ending offset of the range to be reformatted. |
| */ |
| public void reformatRegion(int begin, int end) { |
| ISourceViewer textViewer = getStructuredSourceViewer(); |
| |
| // Clamp text range to valid offsets. |
| IDocument document = textViewer.getDocument(); |
| int documentLength = document.getLength(); |
| end = Math.min(end, documentLength); |
| begin = Math.min(begin, end); |
| |
| if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { |
| // Workarounds which only apply to the builtin Eclipse formatter: |
| // |
| // It turns out the XML formatter does *NOT* format things correctly if you |
| // select just a region of text. You *MUST* also include the leading whitespace |
| // on the line, or it will dedent all the content to column 0. Therefore, |
| // we must figure out the offset of the start of the line that contains the |
| // beginning of the tag. |
| try { |
| IRegion lineInformation = document.getLineInformationOfOffset(begin); |
| if (lineInformation != null) { |
| int lineBegin = lineInformation.getOffset(); |
| if (lineBegin != begin) { |
| begin = lineBegin; |
| } else if (begin > 0) { |
| // Trick #2: It turns out that, if an XML element starts in column 0, |
| // then the XML formatter will NOT indent it (even if its parent is |
| // indented). If you on the other hand include the end of the previous |
| // line (the newline), THEN the formatter also correctly inserts the |
| // element. Therefore, we adjust the beginning range to include the |
| // previous line (if we are not already in column 0 of the first line) |
| // in the case where the element starts the line. |
| begin--; |
| } |
| } |
| } catch (BadLocationException e) { |
| // This cannot happen because we already clamped the offsets |
| AdtPlugin.log(e, e.toString()); |
| } |
| } |
| |
| if (textViewer instanceof StructuredTextViewer) { |
| StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; |
| int operation = ISourceViewer.FORMAT; |
| boolean canFormat = structuredTextViewer.canDoOperation(operation); |
| if (canFormat) { |
| StyledText textWidget = textViewer.getTextWidget(); |
| textWidget.setSelection(begin, end); |
| |
| boolean oldIgnore = mIgnoreXmlUpdate; |
| try { |
| // Formatting does not affect the XML model so ignore notifications |
| // about model edits from this |
| mIgnoreXmlUpdate = true; |
| structuredTextViewer.doOperation(operation); |
| } finally { |
| mIgnoreXmlUpdate = oldIgnore; |
| } |
| |
| textWidget.setSelection(0, 0); |
| } |
| } |
| } |
| |
| /** |
| * Invokes content assist in this editor at the given offset |
| * |
| * @param offset the offset to invoke content assist at, or -1 to leave |
| * caret alone |
| */ |
| public void invokeContentAssist(int offset) { |
| ISourceViewer textViewer = getStructuredSourceViewer(); |
| if (textViewer instanceof StructuredTextViewer) { |
| StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; |
| int operation = ISourceViewer.CONTENTASSIST_PROPOSALS; |
| boolean allowed = structuredTextViewer.canDoOperation(operation); |
| if (allowed) { |
| if (offset != -1) { |
| StyledText textWidget = textViewer.getTextWidget(); |
| // Clamp text range to valid offsets. |
| IDocument document = textViewer.getDocument(); |
| int documentLength = document.getLength(); |
| offset = Math.max(0, Math.min(offset, documentLength)); |
| textWidget.setSelection(offset, offset); |
| } |
| structuredTextViewer.doOperation(operation); |
| } |
| } |
| } |
| |
| /** |
| * Formats the XML region corresponding to the given node. |
| * |
| * @param node The node to be formatted. |
| */ |
| public void reformatNode(Node node) { |
| if (mIsCreatingPage) { |
| return; |
| } |
| |
| if (node instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion) node; |
| int begin = region.getStartOffset(); |
| int end = region.getEndOffset(); |
| reformatRegion(begin, end); |
| } |
| } |
| |
| /** |
| * Formats the XML document according to the user's XML formatting settings. |
| */ |
| public void reformatDocument() { |
| ISourceViewer textViewer = getStructuredSourceViewer(); |
| if (textViewer instanceof StructuredTextViewer) { |
| StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; |
| int operation = StructuredTextViewer.FORMAT_DOCUMENT; |
| boolean canFormat = structuredTextViewer.canDoOperation(operation); |
| if (canFormat) { |
| boolean oldIgnore = mIgnoreXmlUpdate; |
| try { |
| // Formatting does not affect the XML model so ignore notifications |
| // about model edits from this |
| mIgnoreXmlUpdate = true; |
| structuredTextViewer.doOperation(operation); |
| } finally { |
| mIgnoreXmlUpdate = oldIgnore; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the indentation String of the given node. |
| * |
| * @param xmlNode The node whose indentation we want. |
| * @return The indent-string of the given node, or "" if the indentation for some reason could |
| * not be computed. |
| */ |
| public String getIndent(Node xmlNode) { |
| return getIndent(getStructuredDocument(), xmlNode); |
| } |
| |
| /** |
| * Returns the indentation String of the given node. |
| * |
| * @param document The Eclipse document containing the XML |
| * @param xmlNode The node whose indentation we want. |
| * @return The indent-string of the given node, or "" if the indentation for some reason could |
| * not be computed. |
| */ |
| public static String getIndent(IDocument document, Node xmlNode) { |
| if (xmlNode instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion)xmlNode; |
| int startOffset = region.getStartOffset(); |
| return getIndentAtOffset(document, startOffset); |
| } |
| |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the indentation String at the line containing the given offset |
| * |
| * @param document the document containing the offset |
| * @param offset The offset of a character on a line whose indentation we seek |
| * @return The indent-string of the given node, or "" if the indentation for some |
| * reason could not be computed. |
| */ |
| public static String getIndentAtOffset(IDocument document, int offset) { |
| try { |
| IRegion lineInformation = document.getLineInformationOfOffset(offset); |
| if (lineInformation != null) { |
| int lineBegin = lineInformation.getOffset(); |
| if (lineBegin != offset) { |
| String prefix = document.get(lineBegin, offset - lineBegin); |
| |
| // It's possible that the tag whose indentation we seek is not |
| // at the beginning of the line. In that case we'll just return |
| // the indentation of the line itself. |
| for (int i = 0; i < prefix.length(); i++) { |
| if (!Character.isWhitespace(prefix.charAt(i))) { |
| return prefix.substring(0, i); |
| } |
| } |
| |
| return prefix; |
| } |
| } |
| } catch (BadLocationException e) { |
| AdtPlugin.log(e, "Could not obtain indentation"); //$NON-NLS-1$ |
| } |
| |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the active {@link AndroidXmlEditor}, provided it matches the given source |
| * viewer |
| * |
| * @param viewer the source viewer to ensure the active editor is associated with |
| * @return the active editor provided it matches the given source viewer or null. |
| */ |
| public static AndroidXmlEditor fromTextViewer(ITextViewer viewer) { |
| IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); |
| if (wwin != null) { |
| // Try the active editor first. |
| IWorkbenchPage page = wwin.getActivePage(); |
| if (page != null) { |
| IEditorPart editor = page.getActiveEditor(); |
| if (editor instanceof AndroidXmlEditor) { |
| ISourceViewer ssviewer = |
| ((AndroidXmlEditor) editor).getStructuredSourceViewer(); |
| if (ssviewer == viewer) { |
| return (AndroidXmlEditor) editor; |
| } |
| } |
| } |
| |
| // If that didn't work, try all the editors |
| for (IWorkbenchPage page2 : wwin.getPages()) { |
| if (page2 != null) { |
| for (IEditorReference editorRef : page2.getEditorReferences()) { |
| IEditorPart editor = editorRef.getEditor(false /*restore*/); |
| if (editor instanceof AndroidXmlEditor) { |
| ISourceViewer ssviewer = |
| ((AndroidXmlEditor) editor).getStructuredSourceViewer(); |
| if (ssviewer == viewer) { |
| return (AndroidXmlEditor) editor; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Called when this editor is activated */ |
| public void activated() { |
| if (getActivePage() == mTextPageIndex) { |
| updateActionBindings(); |
| } |
| } |
| |
| /** Called when this editor is deactivated */ |
| public void deactivated() { |
| } |
| |
| /** |
| * Listen to changes in the underlying XML model in the structured editor. |
| */ |
| private class XmlModelStateListener implements IModelStateListener { |
| |
| /** |
| * A model is about to be changed. This typically is initiated by one |
| * client of the model, to signal a large change and/or a change to the |
| * model's ID or base Location. A typical use might be if a client might |
| * want to suspend processing until all changes have been made. |
| * <p/> |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelAboutToBeChanged(IStructuredModel model) { |
| // pass |
| } |
| |
| /** |
| * Signals that the changes foretold by modelAboutToBeChanged have been |
| * made. A typical use might be to refresh, or to resume processing that |
| * was suspended as a result of modelAboutToBeChanged. |
| * <p/> |
| * This AndroidXmlEditor implementation calls the xmlModelChanged callback. |
| */ |
| @Override |
| public void modelChanged(IStructuredModel model) { |
| if (mIgnoreXmlUpdate) { |
| return; |
| } |
| xmlModelChanged(getXmlDocument(model)); |
| } |
| |
| /** |
| * Notifies that a model's dirty state has changed, and passes that state |
| * in isDirty. A model becomes dirty when any change is made, and becomes |
| * not-dirty when the model is saved. |
| * <p/> |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) { |
| // pass |
| } |
| |
| /** |
| * A modelDeleted means the underlying resource has been deleted. The |
| * model itself is not removed from model management until all have |
| * released it. Note: baseLocation is not (necessarily) changed in this |
| * event, but may not be accurate. |
| * <p/> |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelResourceDeleted(IStructuredModel model) { |
| // pass |
| } |
| |
| /** |
| * A model has been renamed or copied (as in saveAs..). In the renamed |
| * case, the two parameters are the same instance, and only contain the |
| * new info for id and base location. |
| * <p/> |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) { |
| // pass |
| } |
| |
| /** |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelAboutToBeReinitialized(IStructuredModel structuredModel) { |
| // pass |
| } |
| |
| /** |
| * This AndroidXmlEditor implementation of IModelChangedListener is empty. |
| */ |
| @Override |
| public void modelReinitialized(IStructuredModel structuredModel) { |
| // pass |
| } |
| } |
| } |