• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
AndroidXmlEditor()160     public AndroidXmlEditor() {
161         this(true);
162     }
163 
164     /**
165      * Creates a form editor.
166      * @param addTargetListener whether to create an {@link ITargetChangeListener}.
167      */
AndroidXmlEditor(boolean addTargetListener)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      */
getUiRootNode()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      */
createFormPages()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      */
postCreatePages()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      */
initUiRootNode(boolean force)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      */
xmlModelChanged(Document xml_doc)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      */
setIgnoreXmlUpdate(boolean ignore)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
addPages()250     protected void addPages() {
251         createAndroidPages();
252         selectDefaultPage(null /* defaultPageId */);
253     }
254 
255     /**
256      * Creates the page for the Android Editors
257      */
createAndroidPages()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      */
isCreatingPages()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
addPage(IFormPage page)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
addPage(IEditorPart editor, IEditorInput input)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      */
createUndoRedoActions()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      */
selectDefaultPage(String defaultPageId)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      */
removePages()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
setActivePage(String pageId)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
pageChange(int newPageIndex)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      */
resourceChanged(final IResourceChangeEvent event)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
init(IEditorSite site, IEditorInput editorInput)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      */
getInputFile()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
dispose()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
doSave(IProgressMonitor monitor)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
doSaveAs()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
commitPages(boolean onSave)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
isSaveAsAllowed()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      */
createHyperlinkListener()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