Home | History | Annotate | Download | only in editors
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors;
     18 
     19 import static org.eclipse.wst.sse.ui.internal.actions.StructuredTextEditorActionConstants.ACTION_NAME_FORMAT_DOCUMENT;
     20 
     21 import com.android.annotations.Nullable;
     22 import com.android.ide.eclipse.adt.AdtConstants;
     23 import com.android.ide.eclipse.adt.AdtPlugin;
     24 import com.android.ide.eclipse.adt.AdtUtils;
     25 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     26 import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
     27 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     28 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction;
     29 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     30 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     31 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
     32 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
     33 import com.android.sdklib.IAndroidTarget;
     34 
     35 import org.eclipse.core.resources.IFile;
     36 import org.eclipse.core.resources.IMarker;
     37 import org.eclipse.core.resources.IProject;
     38 import org.eclipse.core.resources.IResource;
     39 import org.eclipse.core.runtime.CoreException;
     40 import org.eclipse.core.runtime.IProgressMonitor;
     41 import org.eclipse.core.runtime.IStatus;
     42 import org.eclipse.core.runtime.QualifiedName;
     43 import org.eclipse.core.runtime.Status;
     44 import org.eclipse.core.runtime.jobs.Job;
     45 import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds;
     46 import org.eclipse.jface.action.Action;
     47 import org.eclipse.jface.action.IAction;
     48 import org.eclipse.jface.dialogs.ErrorDialog;
     49 import org.eclipse.jface.text.BadLocationException;
     50 import org.eclipse.jface.text.IDocument;
     51 import org.eclipse.jface.text.IRegion;
     52 import org.eclipse.jface.text.ITextViewer;
     53 import org.eclipse.jface.text.source.ISourceViewer;
     54 import org.eclipse.swt.custom.StyledText;
     55 import org.eclipse.swt.widgets.Display;
     56 import org.eclipse.ui.IActionBars;
     57 import org.eclipse.ui.IEditorInput;
     58 import org.eclipse.ui.IEditorPart;
     59 import org.eclipse.ui.IEditorReference;
     60 import org.eclipse.ui.IEditorSite;
     61 import org.eclipse.ui.IFileEditorInput;
     62 import org.eclipse.ui.IURIEditorInput;
     63 import org.eclipse.ui.IWorkbenchPage;
     64 import org.eclipse.ui.IWorkbenchWindow;
     65 import org.eclipse.ui.PartInitException;
     66 import org.eclipse.ui.PlatformUI;
     67 import org.eclipse.ui.actions.ActionFactory;
     68 import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
     69 import org.eclipse.ui.forms.IManagedForm;
     70 import org.eclipse.ui.forms.editor.FormEditor;
     71 import org.eclipse.ui.forms.editor.IFormPage;
     72 import org.eclipse.ui.forms.events.HyperlinkAdapter;
     73 import org.eclipse.ui.forms.events.HyperlinkEvent;
     74 import org.eclipse.ui.forms.events.IHyperlinkListener;
     75 import org.eclipse.ui.forms.widgets.FormText;
     76 import org.eclipse.ui.ide.IDEActionFactory;
     77 import org.eclipse.ui.ide.IGotoMarker;
     78 import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
     79 import org.eclipse.ui.part.MultiPageEditorPart;
     80 import org.eclipse.ui.part.WorkbenchPart;
     81 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
     82 import org.eclipse.wst.sse.core.StructuredModelManager;
     83 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     84 import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
     85 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     86 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     87 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     88 import org.eclipse.wst.sse.ui.StructuredTextEditor;
     89 import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
     90 import org.eclipse.wst.xml.core.internal.document.NodeContainer;
     91 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     92 import org.w3c.dom.Document;
     93 import org.w3c.dom.Node;
     94 
     95 import java.net.MalformedURLException;
     96 import java.net.URL;
     97 import java.util.Collections;
     98 
     99 /**
    100  * Multi-page form editor for Android XML files.
    101  * <p/>
    102  * It is designed to work with a {@link StructuredTextEditor} that will display an XML file.
    103  * <br/>
    104  * Derived classes must implement createFormPages to create the forms before the
    105  * source editor. This can be a no-op if desired.
    106  */
    107 @SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet
    108 public abstract class AndroidXmlEditor extends FormEditor {
    109 
    110     /** Icon used for the XML source page. */
    111     public static final String ICON_XML_PAGE = "editor_page_source"; //$NON-NLS-1$
    112 
    113     /** Preference name for the current page of this file */
    114     private static final String PREF_CURRENT_PAGE = "_current_page"; //$NON-NLS-1$
    115 
    116     /** Id string used to create the Android SDK browser */
    117     private static String BROWSER_ID = "android"; //$NON-NLS-1$
    118 
    119     /** Page id of the XML source editor, used for switching tabs programmatically */
    120     public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
    121 
    122     /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
    123     public static final int TEXT_WIDTH_HINT = 50;
    124 
    125     /** Page index of the text editor (always the last page) */
    126     protected int mTextPageIndex;
    127     /** The text editor */
    128     private StructuredTextEditor mTextEditor;
    129     /** Listener for the XML model from the StructuredEditor */
    130     private XmlModelStateListener mXmlModelStateListener;
    131     /** Listener to update the root node if the target of the file is changed because of a
    132      * SDK location change or a project target change */
    133     private TargetChangeListener mTargetListener = null;
    134 
    135     /** flag set during page creation */
    136     private boolean mIsCreatingPage = false;
    137 
    138     /**
    139      * Flag used to ignore XML model updates. For example, the flag is set during
    140      * formatting. A format operation should completely preserve the semantics of the XML
    141      * so the document listeners can use this flag to skip updating the model when edits
    142      * are observed during a formatting operation
    143      */
    144     private boolean mIgnoreXmlUpdate;
    145 
    146     /**
    147      * Flag indicating we're inside {@link #wrapEditXmlModel(Runnable)}.
    148      * This is a counter, which allows us to nest the edit XML calls.
    149      * There is no pending operation when the counter is at zero.
    150      */
    151     private int mIsEditXmlModelPending;
    152 
    153     /**
    154      * Usually null, but during an editing operation, represents the highest
    155      * node which should be formatted when the editing operation is complete.
    156      */
    157     private UiElementNode mFormatNode;
    158 
    159     /**
    160      * Whether {@link #mFormatNode} should be formatted recursively, or just
    161      * the node itself (its arguments)
    162      */
    163     private boolean mFormatChildren;
    164 
    165     /**
    166      * Creates a form editor.
    167      * <p/>
    168      * Some derived classes will want to use {@link #addDefaultTargetListener()}
    169      * to setup the default listener to monitor SDK target changes. This
    170      * is no longer the default.
    171      */
    172     public AndroidXmlEditor() {
    173         super();
    174     }
    175 
    176     @Override
    177     public void init(IEditorSite site, IEditorInput input) throws PartInitException {
    178         super.init(site, input);
    179         // Trigger a check to see if the SDK needs to be reloaded (which will
    180         // invoke onSdkLoaded or ITargetChangeListener asynchronously as needed).
    181         AdtPlugin.getDefault().refreshSdk();
    182     }
    183 
    184     /**
    185      * Setups a default {@link ITargetChangeListener} that will call
    186      * {@link #initUiRootNode(boolean)} when the SDK or the target changes.
    187      */
    188     public void addDefaultTargetListener() {
    189         if (mTargetListener == null) {
    190             mTargetListener = new TargetChangeListener() {
    191                 @Override
    192                 public IProject getProject() {
    193                     return AndroidXmlEditor.this.getProject();
    194                 }
    195 
    196                 @Override
    197                 public void reload() {
    198                     commitPages(false /* onSave */);
    199 
    200                     // recreate the ui root node always
    201                     initUiRootNode(true /*force*/);
    202                 }
    203             };
    204             AdtPlugin.getDefault().addTargetListener(mTargetListener);
    205         }
    206     }
    207 
    208     // ---- Abstract Methods ----
    209 
    210     /**
    211      * Returns the root node of the UI element hierarchy manipulated by the current
    212      * UI node editor.
    213      */
    214     abstract public UiElementNode getUiRootNode();
    215 
    216     /**
    217      * Creates the various form pages.
    218      * <p/>
    219      * Derived classes must implement this to add their own specific tabs.
    220      */
    221     abstract protected void createFormPages();
    222 
    223     /**
    224      * Called by the base class {@link AndroidXmlEditor} once all pages (custom form pages
    225      * as well as text editor page) have been created. This give a chance to deriving
    226      * classes to adjust behavior once the text page has been created.
    227      */
    228     protected void postCreatePages() {
    229         // Nothing in the base class.
    230     }
    231 
    232     /**
    233      * Creates the initial UI Root Node, including the known mandatory elements.
    234      * @param force if true, a new UiManifestNode is recreated even if it already exists.
    235      */
    236     abstract protected void initUiRootNode(boolean force);
    237 
    238     /**
    239      * Subclasses should override this method to process the new XML Model, which XML
    240      * root node is given.
    241      *
    242      * The base implementation is empty.
    243      *
    244      * @param xml_doc The XML document, if available, or null if none exists.
    245      */
    246     abstract protected void xmlModelChanged(Document xml_doc);
    247 
    248     /**
    249      * Controls whether XML models are ignored or not.
    250      *
    251      * @param ignore when true, ignore all subsequent XML model updates, when false start
    252      *            processing XML model updates again
    253      */
    254     public void setIgnoreXmlUpdate(boolean ignore) {
    255         mIgnoreXmlUpdate = ignore;
    256     }
    257 
    258     /**
    259      * Returns whether XML model events are ignored or not. This is the case
    260      * when we are deliberately modifying the document in a way which does not
    261      * change the semantics (such as formatting), or when we have already
    262      * directly updated the model ourselves.
    263      *
    264      * @return true if XML events should be ignored
    265      */
    266     public boolean getIgnoreXmlUpdate() {
    267         return mIgnoreXmlUpdate;
    268     }
    269 
    270     // ---- Base Class Overrides, Interfaces Implemented ----
    271 
    272     @Override
    273     public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
    274         Object result = super.getAdapter(adapter);
    275 
    276         if (result != null && adapter.equals(IGotoMarker.class) ) {
    277             final IGotoMarker gotoMarker = (IGotoMarker) result;
    278             return new IGotoMarker() {
    279                 @Override
    280                 public void gotoMarker(IMarker marker) {
    281                     gotoMarker.gotoMarker(marker);
    282                     try {
    283                         // Lint markers should always jump to XML text
    284                         if (marker.getType().equals(AdtConstants.MARKER_LINT)) {
    285                             IEditorPart editor = AdtUtils.getActiveEditor();
    286                             if (editor instanceof AndroidXmlEditor) {
    287                                 AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor;
    288                                 xmlEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
    289                             }
    290                         }
    291                     } catch (CoreException e) {
    292                         AdtPlugin.log(e, null);
    293                     }
    294                 }
    295             };
    296         }
    297 
    298         if (result == null && adapter == IContentOutlinePage.class) {
    299             return getStructuredTextEditor().getAdapter(adapter);
    300         }
    301 
    302         return result;
    303     }
    304 
    305     /**
    306      * Creates the pages of the multi-page editor.
    307      */
    308     @Override
    309     protected void addPages() {
    310         createAndroidPages();
    311         selectDefaultPage(null /* defaultPageId */);
    312     }
    313 
    314     /**
    315      * Creates the page for the Android Editors
    316      */
    317     public void createAndroidPages() {
    318         mIsCreatingPage = true;
    319         createFormPages();
    320         createTextEditor();
    321         updateActionBindings();
    322         postCreatePages();
    323         mIsCreatingPage = false;
    324     }
    325 
    326     /**
    327      * Returns whether the editor is currently creating its pages.
    328      */
    329     public boolean isCreatingPages() {
    330         return mIsCreatingPage;
    331     }
    332 
    333     /**
    334      * {@inheritDoc}
    335      * <p/>
    336      * If the page is an instance of {@link IPageImageProvider}, the image returned by
    337      * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
    338      */
    339     @Override
    340     public int addPage(IFormPage page) throws PartInitException {
    341         int index = super.addPage(page);
    342         if (page instanceof IPageImageProvider) {
    343             setPageImage(index, ((IPageImageProvider) page).getPageImage());
    344         }
    345         return index;
    346     }
    347 
    348     /**
    349      * {@inheritDoc}
    350      * <p/>
    351      * If the editor is an instance of {@link IPageImageProvider}, the image returned by
    352      * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
    353      */
    354     @Override
    355     public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException {
    356         int index = super.addPage(editor, input);
    357         if (editor instanceof IPageImageProvider) {
    358             setPageImage(index, ((IPageImageProvider) editor).getPageImage());
    359         }
    360         return index;
    361     }
    362 
    363     /**
    364      * Creates undo redo (etc) actions for the editor site (so that it works for any page of this
    365      * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor}
    366      * (aka the XML text editor.)
    367      */
    368     protected void updateActionBindings() {
    369         IActionBars bars = getEditorSite().getActionBars();
    370         if (bars != null) {
    371             IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
    372             bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
    373 
    374             action = mTextEditor.getAction(ActionFactory.REDO.getId());
    375             bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
    376 
    377             bars.setGlobalActionHandler(ActionFactory.DELETE.getId(),
    378                     mTextEditor.getAction(ActionFactory.DELETE.getId()));
    379             bars.setGlobalActionHandler(ActionFactory.CUT.getId(),
    380                     mTextEditor.getAction(ActionFactory.CUT.getId()));
    381             bars.setGlobalActionHandler(ActionFactory.COPY.getId(),
    382                     mTextEditor.getAction(ActionFactory.COPY.getId()));
    383             bars.setGlobalActionHandler(ActionFactory.PASTE.getId(),
    384                     mTextEditor.getAction(ActionFactory.PASTE.getId()));
    385             bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
    386                     mTextEditor.getAction(ActionFactory.SELECT_ALL.getId()));
    387             bars.setGlobalActionHandler(ActionFactory.FIND.getId(),
    388                     mTextEditor.getAction(ActionFactory.FIND.getId()));
    389             bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(),
    390                     mTextEditor.getAction(IDEActionFactory.BOOKMARK.getId()));
    391 
    392             bars.updateActionBars();
    393         }
    394     }
    395 
    396     /**
    397      * Clears the action bindings for the editor site.
    398      */
    399     protected void clearActionBindings(boolean includeUndoRedo) {
    400         IActionBars bars = getEditorSite().getActionBars();
    401         if (bars != null) {
    402             // For some reason, undo/redo doesn't seem to work in the form editor.
    403             // This appears to be the case for pure Eclipse form editors too, e.g. see
    404             //      https://bugs.eclipse.org/bugs/show_bug.cgi?id=68423
    405             // However, as a workaround we can use the *text* editor's underlying undo
    406             // to revert operations being done in the UI, and the form automatically updates.
    407             // Therefore, to work around this, we simply leave the text editor bindings
    408             // in place if {@code includeUndoRedo} is not set
    409             if (includeUndoRedo) {
    410                 bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), null);
    411                 bars.setGlobalActionHandler(ActionFactory.REDO.getId(), null);
    412             }
    413             bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), null);
    414             bars.setGlobalActionHandler(ActionFactory.CUT.getId(), null);
    415             bars.setGlobalActionHandler(ActionFactory.COPY.getId(), null);
    416             bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), null);
    417             bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), null);
    418             bars.setGlobalActionHandler(ActionFactory.FIND.getId(), null);
    419             bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(), null);
    420 
    421             bars.updateActionBars();
    422         }
    423     }
    424 
    425     /**
    426      * Selects the default active page.
    427      * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
    428      * find the default page in the properties of the {@link IResource} object being edited.
    429      */
    430     public void selectDefaultPage(String defaultPageId) {
    431         if (defaultPageId == null) {
    432             IFile file = getInputFile();
    433             if (file != null) {
    434                 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
    435                         getClass().getSimpleName() + PREF_CURRENT_PAGE);
    436                 String pageId;
    437                 try {
    438                     pageId = file.getPersistentProperty(qname);
    439                     if (pageId != null) {
    440                         defaultPageId = pageId;
    441                     }
    442                 } catch (CoreException e) {
    443                     // ignored
    444                 }
    445             }
    446         }
    447 
    448         if (defaultPageId != null) {
    449             try {
    450                 setActivePage(Integer.parseInt(defaultPageId));
    451             } catch (Exception e) {
    452                 // We can get NumberFormatException from parseInt but also
    453                 // AssertionError from setActivePage when the index is out of bounds.
    454                 // Generally speaking we just want to ignore any exception and fall back on the
    455                 // first page rather than crash the editor load. Logging the error is enough.
    456                 AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
    457             }
    458         } else if (AdtPrefs.getPrefs().isXmlEditorPreferred(getPersistenceCategory())) {
    459             setActivePage(mTextPageIndex);
    460         }
    461     }
    462 
    463     /** The layout editor */
    464     public static final int CATEGORY_LAYOUT   = 1 << 0;
    465     /** The manifest editor */
    466     public static final int CATEGORY_MANIFEST = 1 << 1;
    467     /** Any other XML editor */
    468     public static final int CATEGORY_OTHER    = 1 << 2;
    469 
    470     /**
    471      * Returns the persistence category to use for this editor; this should be
    472      * one of the {@code CATEGORY_} constants such as {@link #CATEGORY_MANIFEST},
    473      * {@link #CATEGORY_LAYOUT}, {@link #CATEGORY_OTHER}, ...
    474      * <p>
    475      * The persistence category is used to group editors together when it comes
    476      * to certain types of persistence metadata. For example, whether this type
    477      * of file was most recently edited graphically or with an XML text editor.
    478      * We'll open new files in the same text or graphical mode as the last time
    479      * the user edited a file of the same persistence category.
    480      * <p>
    481      * Before we added the persistence category, we had a single boolean flag
    482      * recording whether the XML files were most recently edited graphically or
    483      * not. However, this meant that users can't for example prefer to edit
    484      * Manifest files graphically and string files via XML. By splitting the
    485      * editors up into categories, we can track the mode at a finer granularity,
    486      * and still allow similar editors such as those used for animations and
    487      * colors to be treated the same way.
    488      *
    489      * @return the persistence category constant
    490      */
    491     protected int getPersistenceCategory() {
    492         return CATEGORY_OTHER;
    493     }
    494 
    495     /**
    496      * Removes all the pages from the editor.
    497      */
    498     protected void removePages() {
    499         int count = getPageCount();
    500         for (int i = count - 1 ; i >= 0 ; i--) {
    501             removePage(i);
    502         }
    503     }
    504 
    505     /**
    506      * Overrides the parent's setActivePage to be able to switch to the xml editor.
    507      *
    508      * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
    509      * This is needed because the editor doesn't actually derive from IFormPage and thus
    510      * doesn't have the get-by-page-id method. In this case, the method returns null since
    511      * IEditorPart does not implement IFormPage.
    512      */
    513     @Override
    514     public IFormPage setActivePage(String pageId) {
    515         if (pageId.equals(TEXT_EDITOR_ID)) {
    516             super.setActivePage(mTextPageIndex);
    517             return null;
    518         } else {
    519             return super.setActivePage(pageId);
    520         }
    521     }
    522 
    523     /**
    524      * Notifies this multi-page editor that the page with the given id has been
    525      * activated. This method is called when the user selects a different tab.
    526      *
    527      * @see MultiPageEditorPart#pageChange(int)
    528      */
    529     @Override
    530     protected void pageChange(int newPageIndex) {
    531         super.pageChange(newPageIndex);
    532 
    533         // Do not record page changes during creation of pages
    534         if (mIsCreatingPage) {
    535             return;
    536         }
    537 
    538         IFile file = getInputFile();
    539         if (file != null) {
    540             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
    541                     getClass().getSimpleName() + PREF_CURRENT_PAGE);
    542             try {
    543                 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
    544             } catch (CoreException e) {
    545                 // ignore
    546             }
    547         }
    548 
    549         boolean isTextPage = newPageIndex == mTextPageIndex;
    550         AdtPrefs.getPrefs().setXmlEditorPreferred(getPersistenceCategory(), isTextPage);
    551     }
    552 
    553     /**
    554      * Returns true if the active page is the editor page
    555      *
    556      * @return true if the active page is the editor page
    557      */
    558     public boolean isEditorPageActive() {
    559         return getActivePage() == mTextPageIndex;
    560     }
    561 
    562     /**
    563      * Returns the {@link IFile} matching the editor's input or null.
    564      */
    565     @Nullable
    566     public IFile getInputFile() {
    567         IEditorInput input = getEditorInput();
    568         if (input instanceof IFileEditorInput) {
    569             return ((IFileEditorInput) input).getFile();
    570         }
    571         return null;
    572     }
    573 
    574     /**
    575      * Removes attached listeners.
    576      *
    577      * @see WorkbenchPart
    578      */
    579     @Override
    580     public void dispose() {
    581         IStructuredModel xml_model = getModelForRead();
    582         if (xml_model != null) {
    583             try {
    584                 if (mXmlModelStateListener != null) {
    585                     xml_model.removeModelStateListener(mXmlModelStateListener);
    586                 }
    587 
    588             } finally {
    589                 xml_model.releaseFromRead();
    590             }
    591         }
    592 
    593         if (mTargetListener != null) {
    594             AdtPlugin.getDefault().removeTargetListener(mTargetListener);
    595             mTargetListener = null;
    596         }
    597 
    598         super.dispose();
    599     }
    600 
    601     /**
    602      * Commit all dirty pages then saves the contents of the text editor.
    603      * <p/>
    604      * This works by committing all data to the XML model and then
    605      * asking the Structured XML Editor to save the XML.
    606      *
    607      * @see IEditorPart
    608      */
    609     @Override
    610     public void doSave(IProgressMonitor monitor) {
    611         commitPages(true /* onSave */);
    612 
    613         if (AdtPrefs.getPrefs().isFormatOnSave()) {
    614             IAction action = mTextEditor.getAction(ACTION_NAME_FORMAT_DOCUMENT);
    615             if (action != null) {
    616                 try {
    617                     mIgnoreXmlUpdate = true;
    618                     action.run();
    619                 } finally {
    620                     mIgnoreXmlUpdate = false;
    621                 }
    622             }
    623         }
    624 
    625         // The actual "save" operation is done by the Structured XML Editor
    626         getEditor(mTextPageIndex).doSave(monitor);
    627 
    628         // Check for errors on save, if enabled
    629         if (AdtPrefs.getPrefs().isLintOnSave()) {
    630             runLint();
    631         }
    632     }
    633 
    634     /**
    635      * Tells the editor to start a Lint check.
    636      * It's up to the caller to check whether this should be done depending on preferences.
    637      * <p/>
    638      * The default implementation is to call {@link #startLintJob()}.
    639      *
    640      * @return The Job started by {@link EclipseLintRunner} or null if no job was started.
    641      */
    642     protected Job runLint() {
    643         return startLintJob();
    644     }
    645 
    646     /**
    647      * Utility method that creates a Job to run Lint on the current document.
    648      * Does not wait for the job to finish - just returns immediately.
    649      *
    650      * @return a new job, or null
    651      * @see EclipseLintRunner#startLint(java.util.List, IResource, IDocument,
    652      *      boolean, boolean)
    653      */
    654     @Nullable
    655     public Job startLintJob() {
    656         IFile file = getInputFile();
    657         if (file != null) {
    658             return EclipseLintRunner.startLint(Collections.singletonList(file), file,
    659                     getStructuredDocument(), false /*fatalOnly*/, false /*show*/);
    660         }
    661 
    662         return null;
    663     }
    664 
    665     /* (non-Javadoc)
    666      * Saves the contents of this editor to another object.
    667      * <p>
    668      * Subclasses must override this method to implement the open-save-close lifecycle
    669      * for an editor.  For greater details, see <code>IEditorPart</code>
    670      * </p>
    671      *
    672      * @see IEditorPart
    673      */
    674     @Override
    675     public void doSaveAs() {
    676         commitPages(true /* onSave */);
    677 
    678         IEditorPart editor = getEditor(mTextPageIndex);
    679         editor.doSaveAs();
    680         setPageText(mTextPageIndex, editor.getTitle());
    681         setInput(editor.getEditorInput());
    682     }
    683 
    684     /**
    685      * Commits all dirty pages in the editor. This method should
    686      * be called as a first step of a 'save' operation.
    687      * <p/>
    688      * This is the same implementation as in {@link FormEditor}
    689      * except it fixes two bugs: a cast to IFormPage is done
    690      * from page.get(i) <em>before</em> being tested with instanceof.
    691      * Another bug is that the last page might be a null pointer.
    692      * <p/>
    693      * The incorrect casting makes the original implementation crash due
    694      * to our {@link StructuredTextEditor} not being an {@link IFormPage}
    695      * so we have to override and duplicate to fix it.
    696      *
    697      * @param onSave <code>true</code> if commit is performed as part
    698      * of the 'save' operation, <code>false</code> otherwise.
    699      * @since 3.3
    700      */
    701     @Override
    702     public void commitPages(boolean onSave) {
    703         if (pages != null) {
    704             for (int i = 0; i < pages.size(); i++) {
    705                 Object page = pages.get(i);
    706                 if (page != null && page instanceof IFormPage) {
    707                     IFormPage form_page = (IFormPage) page;
    708                     IManagedForm managed_form = form_page.getManagedForm();
    709                     if (managed_form != null && managed_form.isDirty()) {
    710                         managed_form.commit(onSave);
    711                     }
    712                 }
    713             }
    714         }
    715     }
    716 
    717     /* (non-Javadoc)
    718      * Returns whether the "save as" operation is supported by this editor.
    719      * <p>
    720      * Subclasses must override this method to implement the open-save-close lifecycle
    721      * for an editor.  For greater details, see <code>IEditorPart</code>
    722      * </p>
    723      *
    724      * @see IEditorPart
    725      */
    726     @Override
    727     public boolean isSaveAsAllowed() {
    728         return false;
    729     }
    730 
    731     /**
    732      * Returns the page index of the text editor (always the last page)
    733 
    734      * @return the page index of the text editor (always the last page)
    735      */
    736     public int getTextPageIndex() {
    737         return mTextPageIndex;
    738     }
    739 
    740     // ---- Local methods ----
    741 
    742 
    743     /**
    744      * Helper method that creates a new hyper-link Listener.
    745      * Used by derived classes which need active links in {@link FormText}.
    746      * <p/>
    747      * This link listener handles two kinds of URLs:
    748      * <ul>
    749      * <li> Links starting with "http" are simply sent to a local browser.
    750      * <li> Links starting with "file:/" are simply sent to a local browser.
    751      * <li> Links starting with "page:" are expected to be an editor page id to switch to.
    752      * <li> Other links are ignored.
    753      * </ul>
    754      *
    755      * @return A new hyper-link listener for FormText to use.
    756      */
    757     public final IHyperlinkListener createHyperlinkListener() {
    758         return new HyperlinkAdapter() {
    759             /**
    760              * Switch to the page corresponding to the link that has just been clicked.
    761              * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
    762              */
    763             @Override
    764             public void linkActivated(HyperlinkEvent e) {
    765                 super.linkActivated(e);
    766                 String link = e.data.toString();
    767                 if (link.startsWith("http") ||          //$NON-NLS-1$
    768                         link.startsWith("file:/")) {    //$NON-NLS-1$
    769                     openLinkInBrowser(link);
    770                 } else if (link.startsWith("page:")) {  //$NON-NLS-1$
    771                     // Switch to an internal page
    772                     setActivePage(link.substring(5 /* strlen("page:") */));
    773                 }
    774             }
    775         };
    776     }
    777 
    778     /**
    779      * Open the http link into a browser
    780      *
    781      * @param link The URL to open in a browser
    782      */
    783     private void openLinkInBrowser(String link) {
    784         try {
    785             IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
    786             wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
    787         } catch (PartInitException e1) {
    788             // pass
    789         } catch (MalformedURLException e1) {
    790             // pass
    791         }
    792     }
    793 
    794     /**
    795      * Creates the XML source editor.
    796      * <p/>
    797      * Memorizes the index page of the source editor (it's always the last page, but the number
    798      * of pages before can change.)
    799      * <br/>
    800      * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
    801      * Finally triggers modelChanged() on the model listener -- derived classes can use this
    802      * to initialize the model the first time.
    803      * <p/>
    804      * Called only once <em>after</em> createFormPages.
    805      */
    806     private void createTextEditor() {
    807         try {
    808             mTextEditor = new StructuredTextEditor() {
    809                 @Override
    810                 protected void createActions() {
    811                     super.createActions();
    812 
    813                     Action action = new RenameResourceXmlTextAction(mTextEditor);
    814                     action.setActionDefinitionId(IJavaEditorActionDefinitionIds.RENAME_ELEMENT);
    815                     setAction(IJavaEditorActionDefinitionIds.RENAME_ELEMENT, action);
    816                 }
    817             };
    818             int index = addPage(mTextEditor, getEditorInput());
    819             mTextPageIndex = index;
    820             setPageText(index, mTextEditor.getTitle());
    821             setPageImage(index,
    822                     IconFactory.getInstance().getIcon(ICON_XML_PAGE));
    823 
    824             if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) {
    825                 Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    826                         "Error opening the Android XML editor. Is the document an XML file?");
    827                 throw new RuntimeException("Android XML Editor Error", new CoreException(status));
    828             }
    829 
    830             IStructuredModel xml_model = getModelForRead();
    831             if (xml_model != null) {
    832                 try {
    833                     mXmlModelStateListener = new XmlModelStateListener();
    834                     xml_model.addModelStateListener(mXmlModelStateListener);
    835                     mXmlModelStateListener.modelChanged(xml_model);
    836                 } catch (Exception e) {
    837                     AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$
    838                 } finally {
    839                     xml_model.releaseFromRead();
    840                 }
    841             }
    842         } catch (PartInitException e) {
    843             ErrorDialog.openError(getSite().getShell(),
    844                     "Android XML Editor Error", null, e.getStatus());
    845         }
    846     }
    847 
    848     /**
    849      * Returns the ISourceViewer associated with the Structured Text editor.
    850      */
    851     public final ISourceViewer getStructuredSourceViewer() {
    852         if (mTextEditor != null) {
    853             // We can't access mDelegate.getSourceViewer() because it is protected,
    854             // however getTextViewer simply returns the SourceViewer casted, so we
    855             // can use it instead.
    856             return mTextEditor.getTextViewer();
    857         }
    858         return null;
    859     }
    860 
    861     /**
    862      * Return the {@link StructuredTextEditor} associated with this XML editor
    863      *
    864      * @return the associated {@link StructuredTextEditor}
    865      */
    866     public StructuredTextEditor getStructuredTextEditor() {
    867         return mTextEditor;
    868     }
    869 
    870     /**
    871      * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
    872      * Editor) or null if not available.
    873      */
    874     public IStructuredDocument getStructuredDocument() {
    875         if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
    876             return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
    877         }
    878         return null;
    879     }
    880 
    881     /**
    882      * Returns a version of the model that has been shared for read.
    883      * <p/>
    884      * Callers <em>must</em> call model.releaseFromRead() when done, typically
    885      * in a try..finally clause.
    886      *
    887      * Portability note: this uses getModelManager which is part of wst.sse.core; however
    888      * the interface returned is part of wst.sse.core.internal.provisional so we can
    889      * expect it to change in a distant future if they start cleaning their codebase,
    890      * however unlikely that is.
    891      *
    892      * @return The model for the XML document or null if cannot be obtained from the editor
    893      */
    894     public IStructuredModel getModelForRead() {
    895         IStructuredDocument document = getStructuredDocument();
    896         if (document != null) {
    897             IModelManager mm = StructuredModelManager.getModelManager();
    898             if (mm != null) {
    899                 // TODO simplify this by not using the internal IStructuredDocument.
    900                 // Instead we can now use mm.getModelForRead(getFile()).
    901                 // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
    902                 // method. IIRC 3.3 didn't have it.
    903 
    904                 return mm.getModelForRead(document);
    905             }
    906         }
    907         return null;
    908     }
    909 
    910     /**
    911      * Returns a version of the model that has been shared for edit.
    912      * <p/>
    913      * Callers <em>must</em> call model.releaseFromEdit() when done, typically
    914      * in a try..finally clause.
    915      * <p/>
    916      * Because of this, it is mandatory to use the wrapper
    917      * {@link #wrapEditXmlModel(Runnable)} which executes a runnable into a
    918      * properly configured model and then performs whatever cleanup is necessary.
    919      *
    920      * @return The model for the XML document or null if cannot be obtained from the editor
    921      */
    922     private IStructuredModel getModelForEdit() {
    923         IStructuredDocument document = getStructuredDocument();
    924         if (document != null) {
    925             IModelManager mm = StructuredModelManager.getModelManager();
    926             if (mm != null) {
    927                 // TODO simplify this by not using the internal IStructuredDocument.
    928                 // Instead we can now use mm.getModelForRead(getFile()).
    929                 // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
    930                 // method. IIRC 3.3 didn't have it.
    931 
    932                 return mm.getModelForEdit(document);
    933             }
    934         }
    935         return null;
    936     }
    937 
    938     /**
    939      * Helper class to perform edits on the XML model whilst making sure the
    940      * model has been prepared to be changed.
    941      * <p/>
    942      * It first gets a model for edition using {@link #getModelForEdit()},
    943      * then calls {@link IStructuredModel#aboutToChangeModel()},
    944      * then performs the requested action
    945      * and finally calls {@link IStructuredModel#changedModel()}
    946      * and {@link IStructuredModel#releaseFromEdit()}.
    947      * <p/>
    948      * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
    949      * is called, XML model listeners will be triggered.
    950      * <p/>
    951      * Calls can be nested: only the first outer call will actually start and close the edit
    952      * session.
    953      * <p/>
    954      * This method is <em>not synchronized</em> and is not thread safe.
    955      * Callers must be using it from the the main UI thread.
    956      *
    957      * @param editAction Something that will change the XML.
    958      */
    959     public final void wrapEditXmlModel(Runnable editAction) {
    960         wrapEditXmlModel(editAction, null);
    961     }
    962 
    963     /**
    964      * Perform any editor-specific hooks after applying an edit. When edits are
    965      * nested, the hooks will only run after the final top level edit has been
    966      * performed.
    967      * <p>
    968      * Note that the edit hooks are performed outside of the edit lock so
    969      * the hooks should not perform edits on the model without acquiring
    970      * a lock first.
    971      */
    972     public void runEditHooks() {
    973         if (!mIgnoreXmlUpdate) {
    974             // Check for errors, if enabled
    975             if (AdtPrefs.getPrefs().isLintOnSave()) {
    976                 runLint();
    977             }
    978         }
    979     }
    980 
    981     /**
    982      * Executor which performs the given action under an edit lock (and optionally as a
    983      * single undo event).
    984      *
    985      * @param editAction the action to be executed
    986      * @param undoLabel if non null, the edit action will be run as a single undo event
    987      *            and the label used as the name of the undoable action
    988      */
    989     private final void wrapEditXmlModel(final Runnable editAction, final String undoLabel) {
    990         Display display = mTextEditor.getSite().getShell().getDisplay();
    991         if (display.getThread() != Thread.currentThread()) {
    992             display.syncExec(new Runnable() {
    993                 @Override
    994                 public void run() {
    995                     if (!mTextEditor.getTextViewer().getControl().isDisposed()) {
    996                         wrapEditXmlModel(editAction, undoLabel);
    997                     }
    998                 }
    999             });
   1000             return;
   1001         }
   1002 
   1003         IStructuredModel model = null;
   1004         int undoReverseCount = 0;
   1005         try {
   1006 
   1007             if (mIsEditXmlModelPending == 0) {
   1008                 try {
   1009                     model = getModelForEdit();
   1010                     if (undoLabel != null) {
   1011                         // Run this action as an undoable unit.
   1012                         // We have to do it more than once, because in some scenarios
   1013                         // Eclipse WTP decides to cancel the current undo command on its
   1014                         // own -- see http://code.google.com/p/android/issues/detail?id=15901
   1015                         // for one such call chain. By nesting these calls several times
   1016                         // we've incrementing the command count such that a couple of
   1017                         // cancellations are ignored. Interfering with this mechanism may
   1018                         // sound dangerous, but it appears that this undo-termination is
   1019                         // done for UI reasons to anticipate what the user wants, and we know
   1020                         // that in *our* scenarios we want the entire unit run as a single
   1021                         // unit. Here's what the documentation for
   1022                         // IStructuredTextUndoManager#forceEndOfPendingCommand says
   1023                         //   "Normally, the undo manager can figure out the best
   1024                         //    times when to end a pending command and begin a new
   1025                         //    one ... to the structure of a structured
   1026                         //    document. There are times, however, when clients may
   1027                         //    wish to override those algorithms and end one earlier
   1028                         //    than normal. The one known case is for multi-page
   1029                         //    editors. If a user is on one page, and type '123' as
   1030                         //    attribute value, then click around to other parts of
   1031                         //    page, or different pages, then return to '123|' and
   1032                         //    type 456, then "undo" they typically expect the undo
   1033                         //    to just undo what they just typed, the 456, not the
   1034                         //    whole attribute value."
   1035                         for (int i = 0; i < 4; i++) {
   1036                             model.beginRecording(this, undoLabel);
   1037                             undoReverseCount++;
   1038                         }
   1039                     }
   1040                     model.aboutToChangeModel();
   1041                 } catch (Throwable t) {
   1042                     // This is never supposed to happen unless we suddenly don't have a model.
   1043                     // If it does, we don't want to even try to modify anyway.
   1044                     AdtPlugin.log(t, "XML Editor failed to get model to edit");  //$NON-NLS-1$
   1045                     return;
   1046                 }
   1047             }
   1048             mIsEditXmlModelPending++;
   1049             editAction.run();
   1050         } finally {
   1051             mIsEditXmlModelPending--;
   1052             if (model != null) {
   1053                 try {
   1054                     boolean oldIgnore = mIgnoreXmlUpdate;
   1055                     try {
   1056                         mIgnoreXmlUpdate = true;
   1057 
   1058                         if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) {
   1059                             if (mFormatNode == getUiRootNode()) {
   1060                                 reformatDocument();
   1061                             } else {
   1062                                 Node node = mFormatNode.getXmlNode();
   1063                                 if (node instanceof IndexedRegion) {
   1064                                     IndexedRegion region = (IndexedRegion) node;
   1065                                     int begin = region.getStartOffset();
   1066                                     int end = region.getEndOffset();
   1067 
   1068                                     if (!mFormatChildren) {
   1069                                         // This will format just the attribute list
   1070                                         end = begin + 1;
   1071                                     }
   1072 
   1073                                     if (mFormatChildren
   1074                                          && node == node.getOwnerDocument().getDocumentElement()) {
   1075                                         reformatDocument();
   1076                                     } else {
   1077                                         reformatRegion(begin, end);
   1078                                     }
   1079                                 }
   1080                             }
   1081                             mFormatNode = null;
   1082                             mFormatChildren = false;
   1083                         }
   1084 
   1085                         // Notify the model we're done modifying it. This must *always* be executed.
   1086                         model.changedModel();
   1087 
   1088                         // Clean up the undo unit. This is done more than once as explained
   1089                         // above for beginRecording.
   1090                         for (int i = 0; i < undoReverseCount; i++) {
   1091                             model.endRecording(this);
   1092                         }
   1093                     } finally {
   1094                         mIgnoreXmlUpdate = oldIgnore;
   1095                     }
   1096                 } catch (Exception e) {
   1097                     AdtPlugin.log(e, "Failed to clean up undo unit");
   1098                 }
   1099                 model.releaseFromEdit();
   1100 
   1101                 if (mIsEditXmlModelPending < 0) {
   1102                     AdtPlugin.log(IStatus.ERROR,
   1103                             "wrapEditXmlModel finished with invalid nested counter==%1$d", //$NON-NLS-1$
   1104                             mIsEditXmlModelPending);
   1105                     mIsEditXmlModelPending = 0;
   1106                 }
   1107 
   1108                 runEditHooks();
   1109 
   1110                 // Notify listeners
   1111                 IStructuredModel readModel = getModelForRead();
   1112                 if (readModel != null) {
   1113                     try {
   1114                         mXmlModelStateListener.modelChanged(readModel);
   1115                     } catch (Exception e) {
   1116                         AdtPlugin.log(e, "Error while notifying changes"); //$NON-NLS-1$
   1117                     } finally {
   1118                         readModel.releaseFromRead();
   1119                     }
   1120                 }
   1121             }
   1122         }
   1123     }
   1124 
   1125     /**
   1126      * Does this editor participate in the "format GUI editor changes" option?
   1127      *
   1128      * @return true if this editor supports automatically formatting XML
   1129      *         affected by GUI changes
   1130      */
   1131     public boolean supportsFormatOnGuiEdit() {
   1132         return false;
   1133     }
   1134 
   1135     /**
   1136      * Mark the given node as needing to be formatted when the current edits are
   1137      * done, provided the user has turned that option on (see
   1138      * {@link AdtPrefs#getFormatGuiXml()}).
   1139      *
   1140      * @param node the node to be scheduled for formatting
   1141      * @param attributesOnly if true, only update the attributes list of the
   1142      *            node, otherwise update the node recursively (e.g. all children
   1143      *            too)
   1144      */
   1145     public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) {
   1146         if (!supportsFormatOnGuiEdit()) {
   1147             return;
   1148         }
   1149 
   1150         if (node == mFormatNode) {
   1151             if (!attributesOnly) {
   1152                 mFormatChildren = true;
   1153             }
   1154         } else if (mFormatNode == null) {
   1155             mFormatNode = node;
   1156             mFormatChildren = !attributesOnly;
   1157         } else {
   1158             if (mFormatNode.isAncestorOf(node)) {
   1159                 mFormatChildren = true;
   1160             } else if (node.isAncestorOf(mFormatNode)) {
   1161                 mFormatNode = node;
   1162                 mFormatChildren = true;
   1163             } else {
   1164                 // Two independent nodes; format their closest common ancestor.
   1165                 // Later we could consider having a small number of independent nodes
   1166                 // and formatting those, and only switching to formatting the common ancestor
   1167                 // when the number of individual nodes gets large.
   1168                 mFormatChildren = true;
   1169                 mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node);
   1170             }
   1171         }
   1172     }
   1173 
   1174     /**
   1175      * Creates an "undo recording" session by calling the undoableAction runnable
   1176      * under an undo session.
   1177      * <p/>
   1178      * This also automatically starts an edit XML session, as if
   1179      * {@link #wrapEditXmlModel(Runnable)} had been called.
   1180      * <p>
   1181      * You can nest several calls to {@link #wrapUndoEditXmlModel(String, Runnable)}, only one
   1182      * recording session will be created.
   1183      *
   1184      * @param label The label for the undo operation. Can be null. Ideally we should really try
   1185      *              to put something meaningful if possible.
   1186      * @param undoableAction the action to be run as a single undoable unit
   1187      */
   1188     public void wrapUndoEditXmlModel(String label, Runnable undoableAction) {
   1189         assert label != null : "All undoable actions should have a label";
   1190         wrapEditXmlModel(undoableAction, label == null ? "" : label); //$NON-NLS-1$
   1191     }
   1192 
   1193     /**
   1194      * Returns true when the runnable of {@link #wrapEditXmlModel(Runnable)} is currently
   1195      * being executed. This means it is safe to actually edit the XML model.
   1196      *
   1197      * @return true if the XML model is already locked for edits
   1198      */
   1199     public boolean isEditXmlModelPending() {
   1200         return mIsEditXmlModelPending > 0;
   1201     }
   1202 
   1203     /**
   1204      * Returns the XML {@link Document} or null if we can't get it
   1205      */
   1206     public final Document getXmlDocument(IStructuredModel model) {
   1207         if (model == null) {
   1208             AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$
   1209             return null;
   1210         }
   1211 
   1212         if (model instanceof IDOMModel) {
   1213             IDOMModel dom_model = (IDOMModel) model;
   1214             return dom_model.getDocument();
   1215         }
   1216         return null;
   1217     }
   1218 
   1219     /**
   1220      * Returns the {@link IProject} for the edited file.
   1221      */
   1222     @Nullable
   1223     public IProject getProject() {
   1224         IFile file = getInputFile();
   1225         if (file != null) {
   1226             return file.getProject();
   1227         }
   1228 
   1229         return null;
   1230     }
   1231 
   1232     /**
   1233      * Returns the {@link AndroidTargetData} for the edited file.
   1234      */
   1235     @Nullable
   1236     public AndroidTargetData getTargetData() {
   1237         IProject project = getProject();
   1238         if (project != null) {
   1239             Sdk currentSdk = Sdk.getCurrent();
   1240             if (currentSdk != null) {
   1241                 IAndroidTarget target = currentSdk.getTarget(project);
   1242 
   1243                 if (target != null) {
   1244                     return currentSdk.getTargetData(target);
   1245                 }
   1246             }
   1247         }
   1248 
   1249         IEditorInput input = getEditorInput();
   1250         if (input instanceof IURIEditorInput) {
   1251             IURIEditorInput urlInput = (IURIEditorInput) input;
   1252             Sdk currentSdk = Sdk.getCurrent();
   1253             if (currentSdk != null) {
   1254                 try {
   1255                     String path = AdtUtils.getFile(urlInput.getURI().toURL()).getPath();
   1256                     IAndroidTarget[] targets = currentSdk.getTargets();
   1257                     for (IAndroidTarget target : targets) {
   1258                         if (path.startsWith(target.getLocation())) {
   1259                             return currentSdk.getTargetData(target);
   1260                         }
   1261                     }
   1262                 } catch (MalformedURLException e) {
   1263                     // File might be in some other weird random location we can't
   1264                     // handle: Just ignore these
   1265                 }
   1266             }
   1267         }
   1268 
   1269         return null;
   1270     }
   1271 
   1272     /**
   1273      * Shows the editor range corresponding to the given XML node. This will
   1274      * front the editor and select the text range.
   1275      *
   1276      * @param xmlNode The DOM node to be shown. The DOM node should be an XML
   1277      *            node from the existing XML model used by the structured XML
   1278      *            editor; it will not do attribute matching to find a
   1279      *            "corresponding" element in the document from some foreign DOM
   1280      *            tree.
   1281      * @return True if the node was shown.
   1282      */
   1283     public boolean show(Node xmlNode) {
   1284         if (xmlNode instanceof IndexedRegion) {
   1285             IndexedRegion region = (IndexedRegion)xmlNode;
   1286 
   1287             IEditorPart textPage = getEditor(mTextPageIndex);
   1288             if (textPage instanceof StructuredTextEditor) {
   1289                 StructuredTextEditor editor = (StructuredTextEditor) textPage;
   1290 
   1291                 setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
   1292 
   1293                 // Note - we cannot use region.getLength() because that seems to
   1294                 // always return 0.
   1295                 int regionLength = region.getEndOffset() - region.getStartOffset();
   1296                 editor.selectAndReveal(region.getStartOffset(), regionLength);
   1297                 return true;
   1298             }
   1299         }
   1300 
   1301         return false;
   1302     }
   1303 
   1304     /**
   1305      * Selects and reveals the given range in the text editor
   1306      *
   1307      * @param start the beginning offset
   1308      * @param length the length of the region to show
   1309      * @param frontTab if true, front the tab, otherwise just make the selection but don't
   1310      *     change the active tab
   1311      */
   1312     public void show(int start, int length, boolean frontTab) {
   1313         IEditorPart textPage = getEditor(mTextPageIndex);
   1314         if (textPage instanceof StructuredTextEditor) {
   1315             StructuredTextEditor editor = (StructuredTextEditor) textPage;
   1316             if (frontTab) {
   1317                 setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
   1318             }
   1319             editor.selectAndReveal(start, length);
   1320             if (frontTab) {
   1321                 editor.setFocus();
   1322             }
   1323         }
   1324     }
   1325 
   1326     /**
   1327      * Returns true if this editor has more than one page (usually a graphical view and an
   1328      * editor)
   1329      *
   1330      * @return true if this editor has multiple pages
   1331      */
   1332     public boolean hasMultiplePages() {
   1333         return getPageCount() > 1;
   1334     }
   1335 
   1336     /**
   1337      * Get the XML text directly from the editor.
   1338      *
   1339      * @param xmlNode The node whose XML text we want to obtain.
   1340      * @return The XML representation of the {@link Node}, or null if there was an error.
   1341      */
   1342     public String getXmlText(Node xmlNode) {
   1343         String data = null;
   1344         IStructuredModel model = getModelForRead();
   1345         try {
   1346             IStructuredDocument document = getStructuredDocument();
   1347             if (xmlNode instanceof NodeContainer) {
   1348                 // The easy way to get the source of an SSE XML node.
   1349                 data = ((NodeContainer) xmlNode).getSource();
   1350             } else  if (xmlNode instanceof IndexedRegion && document != null) {
   1351                 // Try harder.
   1352                 IndexedRegion region = (IndexedRegion) xmlNode;
   1353                 int start = region.getStartOffset();
   1354                 int end = region.getEndOffset();
   1355 
   1356                 if (end > start) {
   1357                     data = document.get(start, end - start);
   1358                 }
   1359             }
   1360         } catch (BadLocationException e) {
   1361             // the region offset was invalid. ignore.
   1362         } finally {
   1363             model.releaseFromRead();
   1364         }
   1365         return data;
   1366     }
   1367 
   1368     /**
   1369      * Formats the text around the given caret range, using the current Eclipse
   1370      * XML formatter settings.
   1371      *
   1372      * @param begin The starting offset of the range to be reformatted.
   1373      * @param end The ending offset of the range to be reformatted.
   1374      */
   1375     public void reformatRegion(int begin, int end) {
   1376         ISourceViewer textViewer = getStructuredSourceViewer();
   1377 
   1378         // Clamp text range to valid offsets.
   1379         IDocument document = textViewer.getDocument();
   1380         int documentLength = document.getLength();
   1381         end = Math.min(end, documentLength);
   1382         begin = Math.min(begin, end);
   1383 
   1384         if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
   1385             // Workarounds which only apply to the builtin Eclipse formatter:
   1386             //
   1387             // It turns out the XML formatter does *NOT* format things correctly if you
   1388             // select just a region of text. You *MUST* also include the leading whitespace
   1389             // on the line, or it will dedent all the content to column 0. Therefore,
   1390             // we must figure out the offset of the start of the line that contains the
   1391             // beginning of the tag.
   1392             try {
   1393                 IRegion lineInformation = document.getLineInformationOfOffset(begin);
   1394                 if (lineInformation != null) {
   1395                     int lineBegin = lineInformation.getOffset();
   1396                     if (lineBegin != begin) {
   1397                         begin = lineBegin;
   1398                     } else if (begin > 0) {
   1399                         // Trick #2: It turns out that, if an XML element starts in column 0,
   1400                         // then the XML formatter will NOT indent it (even if its parent is
   1401                         // indented). If you on the other hand include the end of the previous
   1402                         // line (the newline), THEN the formatter also correctly inserts the
   1403                         // element. Therefore, we adjust the beginning range to include the
   1404                         // previous line (if we are not already in column 0 of the first line)
   1405                         // in the case where the element starts the line.
   1406                         begin--;
   1407                     }
   1408                 }
   1409             } catch (BadLocationException e) {
   1410                 // This cannot happen because we already clamped the offsets
   1411                 AdtPlugin.log(e, e.toString());
   1412             }
   1413         }
   1414 
   1415         if (textViewer instanceof StructuredTextViewer) {
   1416             StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
   1417             int operation = ISourceViewer.FORMAT;
   1418             boolean canFormat = structuredTextViewer.canDoOperation(operation);
   1419             if (canFormat) {
   1420                 StyledText textWidget = textViewer.getTextWidget();
   1421                 textWidget.setSelection(begin, end);
   1422 
   1423                 boolean oldIgnore = mIgnoreXmlUpdate;
   1424                 try {
   1425                     // Formatting does not affect the XML model so ignore notifications
   1426                     // about model edits from this
   1427                     mIgnoreXmlUpdate = true;
   1428                     structuredTextViewer.doOperation(operation);
   1429                 } finally {
   1430                     mIgnoreXmlUpdate = oldIgnore;
   1431                 }
   1432 
   1433                 textWidget.setSelection(0, 0);
   1434             }
   1435         }
   1436     }
   1437 
   1438     /**
   1439      * Invokes content assist in this editor at the given offset
   1440      *
   1441      * @param offset the offset to invoke content assist at, or -1 to leave
   1442      *            caret alone
   1443      */
   1444     public void invokeContentAssist(int offset) {
   1445         ISourceViewer textViewer = getStructuredSourceViewer();
   1446         if (textViewer instanceof StructuredTextViewer) {
   1447             StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
   1448             int operation = ISourceViewer.CONTENTASSIST_PROPOSALS;
   1449             boolean allowed = structuredTextViewer.canDoOperation(operation);
   1450             if (allowed) {
   1451                 if (offset != -1) {
   1452                     StyledText textWidget = textViewer.getTextWidget();
   1453                     // Clamp text range to valid offsets.
   1454                     IDocument document = textViewer.getDocument();
   1455                     int documentLength = document.getLength();
   1456                     offset = Math.max(0, Math.min(offset, documentLength));
   1457                     textWidget.setSelection(offset, offset);
   1458                 }
   1459                 structuredTextViewer.doOperation(operation);
   1460             }
   1461         }
   1462     }
   1463 
   1464     /**
   1465      * Formats the XML region corresponding to the given node.
   1466      *
   1467      * @param node The node to be formatted.
   1468      */
   1469     public void reformatNode(Node node) {
   1470         if (mIsCreatingPage) {
   1471             return;
   1472         }
   1473 
   1474         if (node instanceof IndexedRegion) {
   1475             IndexedRegion region = (IndexedRegion) node;
   1476             int begin = region.getStartOffset();
   1477             int end = region.getEndOffset();
   1478             reformatRegion(begin, end);
   1479         }
   1480     }
   1481 
   1482     /**
   1483      * Formats the XML document according to the user's XML formatting settings.
   1484      */
   1485     public void reformatDocument() {
   1486         ISourceViewer textViewer = getStructuredSourceViewer();
   1487         if (textViewer instanceof StructuredTextViewer) {
   1488             StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
   1489             int operation = StructuredTextViewer.FORMAT_DOCUMENT;
   1490             boolean canFormat = structuredTextViewer.canDoOperation(operation);
   1491             if (canFormat) {
   1492                 boolean oldIgnore = mIgnoreXmlUpdate;
   1493                 try {
   1494                     // Formatting does not affect the XML model so ignore notifications
   1495                     // about model edits from this
   1496                     mIgnoreXmlUpdate = true;
   1497                     structuredTextViewer.doOperation(operation);
   1498                 } finally {
   1499                     mIgnoreXmlUpdate = oldIgnore;
   1500                 }
   1501             }
   1502         }
   1503     }
   1504 
   1505     /**
   1506      * Returns the indentation String of the given node.
   1507      *
   1508      * @param xmlNode The node whose indentation we want.
   1509      * @return The indent-string of the given node, or "" if the indentation for some reason could
   1510      *         not be computed.
   1511      */
   1512     public String getIndent(Node xmlNode) {
   1513         return getIndent(getStructuredDocument(), xmlNode);
   1514     }
   1515 
   1516     /**
   1517      * Returns the indentation String of the given node.
   1518      *
   1519      * @param document The Eclipse document containing the XML
   1520      * @param xmlNode The node whose indentation we want.
   1521      * @return The indent-string of the given node, or "" if the indentation for some reason could
   1522      *         not be computed.
   1523      */
   1524     public static String getIndent(IDocument document, Node xmlNode) {
   1525         if (xmlNode instanceof IndexedRegion) {
   1526             IndexedRegion region = (IndexedRegion)xmlNode;
   1527             int startOffset = region.getStartOffset();
   1528             return getIndentAtOffset(document, startOffset);
   1529         }
   1530 
   1531         return ""; //$NON-NLS-1$
   1532     }
   1533 
   1534     /**
   1535      * Returns the indentation String at the line containing the given offset
   1536      *
   1537      * @param document the document containing the offset
   1538      * @param offset The offset of a character on a line whose indentation we seek
   1539      * @return The indent-string of the given node, or "" if the indentation for some
   1540      *         reason could not be computed.
   1541      */
   1542     public static String getIndentAtOffset(IDocument document, int offset) {
   1543         try {
   1544             IRegion lineInformation = document.getLineInformationOfOffset(offset);
   1545             if (lineInformation != null) {
   1546                 int lineBegin = lineInformation.getOffset();
   1547                 if (lineBegin != offset) {
   1548                     String prefix = document.get(lineBegin, offset - lineBegin);
   1549 
   1550                     // It's possible that the tag whose indentation we seek is not
   1551                     // at the beginning of the line. In that case we'll just return
   1552                     // the indentation of the line itself.
   1553                     for (int i = 0; i < prefix.length(); i++) {
   1554                         if (!Character.isWhitespace(prefix.charAt(i))) {
   1555                             return prefix.substring(0, i);
   1556                         }
   1557                     }
   1558 
   1559                     return prefix;
   1560                 }
   1561             }
   1562         } catch (BadLocationException e) {
   1563             AdtPlugin.log(e, "Could not obtain indentation"); //$NON-NLS-1$
   1564         }
   1565 
   1566         return ""; //$NON-NLS-1$
   1567     }
   1568 
   1569     /**
   1570      * Returns the active {@link AndroidXmlEditor}, provided it matches the given source
   1571      * viewer
   1572      *
   1573      * @param viewer the source viewer to ensure the active editor is associated with
   1574      * @return the active editor provided it matches the given source viewer or null.
   1575      */
   1576     public static AndroidXmlEditor fromTextViewer(ITextViewer viewer) {
   1577         IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
   1578         if (wwin != null) {
   1579             // Try the active editor first.
   1580             IWorkbenchPage page = wwin.getActivePage();
   1581             if (page != null) {
   1582                 IEditorPart editor = page.getActiveEditor();
   1583                 if (editor instanceof AndroidXmlEditor) {
   1584                     ISourceViewer ssviewer =
   1585                         ((AndroidXmlEditor) editor).getStructuredSourceViewer();
   1586                     if (ssviewer == viewer) {
   1587                         return (AndroidXmlEditor) editor;
   1588                     }
   1589                 }
   1590             }
   1591 
   1592             // If that didn't work, try all the editors
   1593             for (IWorkbenchPage page2 : wwin.getPages()) {
   1594                 if (page2 != null) {
   1595                     for (IEditorReference editorRef : page2.getEditorReferences()) {
   1596                         IEditorPart editor = editorRef.getEditor(false /*restore*/);
   1597                         if (editor instanceof AndroidXmlEditor) {
   1598                             ISourceViewer ssviewer =
   1599                                 ((AndroidXmlEditor) editor).getStructuredSourceViewer();
   1600                             if (ssviewer == viewer) {
   1601                                 return (AndroidXmlEditor) editor;
   1602                             }
   1603                         }
   1604                     }
   1605                 }
   1606             }
   1607         }
   1608 
   1609         return null;
   1610     }
   1611 
   1612     /** Called when this editor is activated */
   1613     public void activated() {
   1614         if (getActivePage() == mTextPageIndex) {
   1615             updateActionBindings();
   1616         }
   1617     }
   1618 
   1619     /** Called when this editor is deactivated */
   1620     public void deactivated() {
   1621     }
   1622 
   1623     /**
   1624      * Listen to changes in the underlying XML model in the structured editor.
   1625      */
   1626     private class XmlModelStateListener implements IModelStateListener {
   1627 
   1628         /**
   1629          * A model is about to be changed. This typically is initiated by one
   1630          * client of the model, to signal a large change and/or a change to the
   1631          * model's ID or base Location. A typical use might be if a client might
   1632          * want to suspend processing until all changes have been made.
   1633          * <p/>
   1634          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1635          */
   1636         @Override
   1637         public void modelAboutToBeChanged(IStructuredModel model) {
   1638             // pass
   1639         }
   1640 
   1641         /**
   1642          * Signals that the changes foretold by modelAboutToBeChanged have been
   1643          * made. A typical use might be to refresh, or to resume processing that
   1644          * was suspended as a result of modelAboutToBeChanged.
   1645          * <p/>
   1646          * This AndroidXmlEditor implementation calls the xmlModelChanged callback.
   1647          */
   1648         @Override
   1649         public void modelChanged(IStructuredModel model) {
   1650             if (mIgnoreXmlUpdate) {
   1651                 return;
   1652             }
   1653             xmlModelChanged(getXmlDocument(model));
   1654         }
   1655 
   1656         /**
   1657          * Notifies that a model's dirty state has changed, and passes that state
   1658          * in isDirty. A model becomes dirty when any change is made, and becomes
   1659          * not-dirty when the model is saved.
   1660          * <p/>
   1661          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1662          */
   1663         @Override
   1664         public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
   1665             // pass
   1666         }
   1667 
   1668         /**
   1669          * A modelDeleted means the underlying resource has been deleted. The
   1670          * model itself is not removed from model management until all have
   1671          * released it. Note: baseLocation is not (necessarily) changed in this
   1672          * event, but may not be accurate.
   1673          * <p/>
   1674          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1675          */
   1676         @Override
   1677         public void modelResourceDeleted(IStructuredModel model) {
   1678             // pass
   1679         }
   1680 
   1681         /**
   1682          * A model has been renamed or copied (as in saveAs..). In the renamed
   1683          * case, the two parameters are the same instance, and only contain the
   1684          * new info for id and base location.
   1685          * <p/>
   1686          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1687          */
   1688         @Override
   1689         public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
   1690             // pass
   1691         }
   1692 
   1693         /**
   1694          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1695          */
   1696         @Override
   1697         public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
   1698             // pass
   1699         }
   1700 
   1701         /**
   1702          * This AndroidXmlEditor implementation of IModelChangedListener is empty.
   1703          */
   1704         @Override
   1705         public void modelReinitialized(IStructuredModel structuredModel) {
   1706             // pass
   1707         }
   1708     }
   1709 }
   1710