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