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