OSDN Git Service

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