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