• 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 com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
21 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
22 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
23 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
24 import com.android.sdklib.IAndroidTarget;
25 
26 import org.eclipse.core.resources.IFile;
27 import org.eclipse.core.resources.IProject;
28 import org.eclipse.core.resources.IResource;
29 import org.eclipse.core.resources.IResourceChangeEvent;
30 import org.eclipse.core.resources.IResourceChangeListener;
31 import org.eclipse.core.resources.ResourcesPlugin;
32 import org.eclipse.core.runtime.CoreException;
33 import org.eclipse.core.runtime.IProgressMonitor;
34 import org.eclipse.core.runtime.IStatus;
35 import org.eclipse.core.runtime.QualifiedName;
36 import org.eclipse.core.runtime.Status;
37 import org.eclipse.jface.action.IAction;
38 import org.eclipse.jface.dialogs.ErrorDialog;
39 import org.eclipse.jface.text.source.ISourceViewer;
40 import org.eclipse.swt.widgets.Display;
41 import org.eclipse.ui.IActionBars;
42 import org.eclipse.ui.IEditorInput;
43 import org.eclipse.ui.IEditorPart;
44 import org.eclipse.ui.IEditorSite;
45 import org.eclipse.ui.IFileEditorInput;
46 import org.eclipse.ui.IWorkbenchPage;
47 import org.eclipse.ui.PartInitException;
48 import org.eclipse.ui.actions.ActionFactory;
49 import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
50 import org.eclipse.ui.forms.IManagedForm;
51 import org.eclipse.ui.forms.editor.FormEditor;
52 import org.eclipse.ui.forms.editor.IFormPage;
53 import org.eclipse.ui.forms.events.HyperlinkAdapter;
54 import org.eclipse.ui.forms.events.HyperlinkEvent;
55 import org.eclipse.ui.forms.events.IHyperlinkListener;
56 import org.eclipse.ui.forms.widgets.FormText;
57 import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
58 import org.eclipse.ui.part.FileEditorInput;
59 import org.eclipse.ui.part.MultiPageEditorPart;
60 import org.eclipse.ui.part.WorkbenchPart;
61 import org.eclipse.wst.sse.core.StructuredModelManager;
62 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
63 import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
64 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
65 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
66 import org.eclipse.wst.sse.ui.StructuredTextEditor;
67 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
68 import org.w3c.dom.Document;
69 
70 import java.net.MalformedURLException;
71 import java.net.URL;
72 
73 /**
74  * Multi-page form editor for Android XML files.
75  * <p/>
76  * It is designed to work with a {@link StructuredTextEditor} that will display an XML file.
77  * <br/>
78  * Derived classes must implement createFormPages to create the forms before the
79  * source editor. This can be a no-op if desired.
80  */
81 public abstract class AndroidEditor extends FormEditor implements IResourceChangeListener {
82 
83     /** Preference name for the current page of this file */
84     private static final String PREF_CURRENT_PAGE = "_current_page";
85 
86     /** Id string used to create the Android SDK browser */
87     private static String BROWSER_ID = "android"; // $NON-NLS-1$
88 
89     /** Page id of the XML source editor, used for switching tabs programmatically */
90     public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
91 
92     /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
93     public static final int TEXT_WIDTH_HINT = 50;
94 
95     /** Page index of the text editor (always the last page) */
96     private int mTextPageIndex;
97     /** The text editor */
98     private StructuredTextEditor mTextEditor;
99     /** Listener for the XML model from the StructuredEditor */
100     private XmlModelStateListener mXmlModelStateListener;
101     /** Listener to update the root node if the target of the file is changed because of a
102      * SDK location change or a project target change */
103     private ITargetChangeListener mTargetListener;
104 
105     /**
106      * Creates a form editor.
107      */
AndroidEditor()108     public AndroidEditor() {
109         super();
110         ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
111 
112         mTargetListener = new ITargetChangeListener() {
113             public void onProjectTargetChange(IProject changedProject) {
114                 if (changedProject == getProject()) {
115                     onTargetsLoaded();
116                 }
117             }
118 
119             public void onTargetsLoaded() {
120                 commitPages(false /* onSave */);
121 
122                 // recreate the ui root node always
123                 initUiRootNode(true /*force*/);
124             }
125         };
126         AdtPlugin.getDefault().addTargetListener(mTargetListener);
127     }
128 
129     // ---- Abstract Methods ----
130 
131     /**
132      * Returns the root node of the UI element hierarchy manipulated by the current
133      * UI node editor.
134      */
getUiRootNode()135     abstract public UiElementNode getUiRootNode();
136 
137     /**
138      * Creates the various form pages.
139      * <p/>
140      * Derived classes must implement this to add their own specific tabs.
141      */
createFormPages()142     abstract protected void createFormPages();
143 
144     /**
145      * Creates the initial UI Root Node, including the known mandatory elements.
146      * @param force if true, a new UiManifestNode is recreated even if it already exists.
147      */
initUiRootNode(boolean force)148     abstract protected void initUiRootNode(boolean force);
149 
150     /**
151      * Subclasses should override this method to process the new XML Model, which XML
152      * root node is given.
153      *
154      * The base implementation is empty.
155      *
156      * @param xml_doc The XML document, if available, or null if none exists.
157      */
xmlModelChanged(Document xml_doc)158     protected void xmlModelChanged(Document xml_doc) {
159         // pass
160     }
161 
162     // ---- Base Class Overrides, Interfaces Implemented ----
163 
164     /**
165      * Creates the pages of the multi-page editor.
166      */
167     @Override
addPages()168     protected void addPages() {
169         createAndroidPages();
170         selectDefaultPage(null /* defaultPageId */);
171     }
172 
173     /**
174      * Creates the page for the Android Editors
175      */
createAndroidPages()176     protected void createAndroidPages() {
177         createFormPages();
178         createTextEditor();
179 
180         createUndoRedoActions();
181     }
182 
183     /**
184      * Creates undo redo actions for the editor site (so that it works for any page of this
185      * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor}
186      * (aka the XML text editor.)
187      */
createUndoRedoActions()188     private void createUndoRedoActions() {
189         IActionBars bars = getEditorSite().getActionBars();
190         if (bars != null) {
191             IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
192             bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
193 
194             action = mTextEditor.getAction(ActionFactory.REDO.getId());
195             bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
196 
197             bars.updateActionBars();
198         }
199     }
200 
201     /**
202      * Selects the default active page.
203      * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
204      * find the default page in the properties of the {@link IResource} object being edited.
205      */
selectDefaultPage(String defaultPageId)206     protected void selectDefaultPage(String defaultPageId) {
207         if (defaultPageId == null) {
208             if (getEditorInput() instanceof IFileEditorInput) {
209                 IFile file = ((IFileEditorInput) getEditorInput()).getFile();
210 
211                 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
212                         getClass().getSimpleName() + PREF_CURRENT_PAGE);
213                 String pageId;
214                 try {
215                     pageId = file.getPersistentProperty(qname);
216                     if (pageId != null) {
217                         defaultPageId = pageId;
218                     }
219                 } catch (CoreException e) {
220                     // ignored
221                 }
222             }
223         }
224 
225         if (defaultPageId != null) {
226             try {
227                 setActivePage(Integer.parseInt(defaultPageId));
228             } catch (Exception e) {
229                 // We can get NumberFormatException from parseInt but also
230                 // AssertionError from setActivePage when the index is out of bounds.
231                 // Generally speaking we just want to ignore any exception and fall back on the
232                 // first page rather than crash the editor load. Logging the error is enough.
233                 AdtPlugin.log(e, "Selecting page '%s' in AndroidEditor failed", defaultPageId);
234             }
235         }
236     }
237 
238     /**
239      * Removes all the pages from the editor.
240      */
removePages()241     protected void removePages() {
242         int count = getPageCount();
243         for (int i = count - 1 ; i >= 0 ; i--) {
244             removePage(i);
245         }
246     }
247 
248     /**
249      * Overrides the parent's setActivePage to be able to switch to the xml editor.
250      *
251      * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
252      * This is needed because the editor doesn't actually derive from IFormPage and thus
253      * doesn't have the get-by-page-id method. In this case, the method returns null since
254      * IEditorPart does not implement IFormPage.
255      */
256     @Override
setActivePage(String pageId)257     public IFormPage setActivePage(String pageId) {
258         if (pageId.equals(TEXT_EDITOR_ID)) {
259             super.setActivePage(mTextPageIndex);
260             return null;
261         } else {
262             return super.setActivePage(pageId);
263         }
264     }
265 
266 
267     /**
268      * Notifies this multi-page editor that the page with the given id has been
269      * activated. This method is called when the user selects a different tab.
270      *
271      * @see MultiPageEditorPart#pageChange(int)
272      */
273     @Override
pageChange(int newPageIndex)274     protected void pageChange(int newPageIndex) {
275         super.pageChange(newPageIndex);
276 
277         if (getEditorInput() instanceof IFileEditorInput) {
278             IFile file = ((IFileEditorInput) getEditorInput()).getFile();
279 
280             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
281                     getClass().getSimpleName() + PREF_CURRENT_PAGE);
282             try {
283                 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
284             } catch (CoreException e) {
285                 // ignore
286             }
287         }
288     }
289 
290     /**
291      * Notifies this listener that some resource changes
292      * are happening, or have already happened.
293      *
294      * Closes all project files on project close.
295      * @see IResourceChangeListener
296      */
resourceChanged(final IResourceChangeEvent event)297     public void resourceChanged(final IResourceChangeEvent event) {
298         if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
299             Display.getDefault().asyncExec(new Runnable() {
300                 public void run() {
301                     IWorkbenchPage[] pages = getSite().getWorkbenchWindow()
302                             .getPages();
303                     for (int i = 0; i < pages.length; i++) {
304                         if (((FileEditorInput)mTextEditor.getEditorInput())
305                                 .getFile().getProject().equals(
306                                         event.getResource())) {
307                             IEditorPart editorPart = pages[i].findEditor(mTextEditor
308                                     .getEditorInput());
309                             pages[i].closeEditor(editorPart, true);
310                         }
311                     }
312                 }
313             });
314         }
315     }
316 
317     /**
318      * Initializes the editor part with a site and input.
319      * <p/>
320      * Checks that the input is an instance of {@link IFileEditorInput}.
321      *
322      * @see FormEditor
323      */
324     @Override
init(IEditorSite site, IEditorInput editorInput)325     public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
326         if (!(editorInput instanceof IFileEditorInput))
327             throw new PartInitException("Invalid Input: Must be IFileEditorInput");
328         super.init(site, editorInput);
329     }
330 
331     /**
332      * Removes attached listeners.
333      *
334      * @see WorkbenchPart
335      */
336     @Override
dispose()337     public void dispose() {
338         IStructuredModel xml_model = getModelForRead();
339         if (xml_model != null) {
340             try {
341                 if (mXmlModelStateListener != null) {
342                     xml_model.removeModelStateListener(mXmlModelStateListener);
343                 }
344 
345             } finally {
346                 xml_model.releaseFromRead();
347             }
348         }
349         ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
350 
351         if (mTargetListener != null) {
352             AdtPlugin.getDefault().removeTargetListener(mTargetListener);
353             mTargetListener = null;
354         }
355 
356         super.dispose();
357     }
358 
359     /**
360      * Commit all dirty pages then saves the contents of the text editor.
361      * <p/>
362      * This works by committing all data to the XML model and then
363      * asking the Structured XML Editor to save the XML.
364      *
365      * @see IEditorPart
366      */
367     @Override
doSave(IProgressMonitor monitor)368     public void doSave(IProgressMonitor monitor) {
369         commitPages(true /* onSave */);
370 
371         // The actual "save" operation is done by the Structured XML Editor
372         getEditor(mTextPageIndex).doSave(monitor);
373     }
374 
375     /* (non-Javadoc)
376      * Saves the contents of this editor to another object.
377      * <p>
378      * Subclasses must override this method to implement the open-save-close lifecycle
379      * for an editor.  For greater details, see <code>IEditorPart</code>
380      * </p>
381      *
382      * @see IEditorPart
383      */
384     @Override
doSaveAs()385     public void doSaveAs() {
386         commitPages(true /* onSave */);
387 
388         IEditorPart editor = getEditor(mTextPageIndex);
389         editor.doSaveAs();
390         setPageText(mTextPageIndex, editor.getTitle());
391         setInput(editor.getEditorInput());
392     }
393 
394     /**
395      * Commits all dirty pages in the editor. This method should
396      * be called as a first step of a 'save' operation.
397      * <p/>
398      * This is the same implementation as in {@link FormEditor}
399      * except it fixes two bugs: a cast to IFormPage is done
400      * from page.get(i) <em>before</em> being tested with instanceof.
401      * Another bug is that the last page might be a null pointer.
402      * <p/>
403      * The incorrect casting makes the original implementation crash due
404      * to our {@link StructuredTextEditor} not being an {@link IFormPage}
405      * so we have to override and duplicate to fix it.
406      *
407      * @param onSave <code>true</code> if commit is performed as part
408      * of the 'save' operation, <code>false</code> otherwise.
409      * @since 3.3
410      */
411     @Override
commitPages(boolean onSave)412     public void commitPages(boolean onSave) {
413         if (pages != null) {
414             for (int i = 0; i < pages.size(); i++) {
415                 Object page = pages.get(i);
416                 if (page != null && page instanceof IFormPage) {
417                     IFormPage form_page = (IFormPage) page;
418                     IManagedForm managed_form = form_page.getManagedForm();
419                     if (managed_form != null && managed_form.isDirty()) {
420                         managed_form.commit(onSave);
421                     }
422                 }
423             }
424         }
425     }
426 
427     /* (non-Javadoc)
428      * Returns whether the "save as" operation is supported by this editor.
429      * <p>
430      * Subclasses must override this method to implement the open-save-close lifecycle
431      * for an editor.  For greater details, see <code>IEditorPart</code>
432      * </p>
433      *
434      * @see IEditorPart
435      */
436     @Override
isSaveAsAllowed()437     public boolean isSaveAsAllowed() {
438         return false;
439     }
440 
441     // ---- Local methods ----
442 
443 
444     /**
445      * Helper method that creates a new hyper-link Listener.
446      * Used by derived classes which need active links in {@link FormText}.
447      * <p/>
448      * This link listener handles two kinds of URLs:
449      * <ul>
450      * <li> Links starting with "http" are simply sent to a local browser.
451      * <li> Links starting with "file:/" are simply sent to a local browser.
452      * <li> Links starting with "page:" are expected to be an editor page id to switch to.
453      * <li> Other links are ignored.
454      * </ul>
455      *
456      * @return A new hyper-link listener for FormText to use.
457      */
createHyperlinkListener()458     public final IHyperlinkListener createHyperlinkListener() {
459         return new HyperlinkAdapter() {
460             /**
461              * Switch to the page corresponding to the link that has just been clicked.
462              * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
463              */
464             @Override
465             public void linkActivated(HyperlinkEvent e) {
466                 super.linkActivated(e);
467                 String link = e.data.toString();
468                 if (link.startsWith("http") ||          //$NON-NLS-1$
469                         link.startsWith("file:/")) {    //$NON-NLS-1$
470                     openLinkInBrowser(link);
471                 } else if (link.startsWith("page:")) {  //$NON-NLS-1$
472                     // Switch to an internal page
473                     setActivePage(link.substring(5 /* strlen("page:") */));
474                 }
475             }
476         };
477     }
478 
479     /**
480      * Open the http link into a browser
481      *
482      * @param link The URL to open in a browser
483      */
484     private void openLinkInBrowser(String link) {
485         try {
486             IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
487             wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
488         } catch (PartInitException e1) {
489             // pass
490         } catch (MalformedURLException e1) {
491             // pass
492         }
493     }
494 
495     /**
496      * Creates the XML source editor.
497      * <p/>
498      * Memorizes the index page of the source editor (it's always the last page, but the number
499      * of pages before can change.)
500      * <br/>
501      * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
502      * Finally triggers modelChanged() on the model listener -- derived classes can use this
503      * to initialize the model the first time.
504      * <p/>
505      * Called only once <em>after</em> createFormPages.
506      */
507     private void createTextEditor() {
508         try {
509             mTextEditor = new StructuredTextEditor();
510             int index = addPage(mTextEditor, getEditorInput());
511             mTextPageIndex = index;
512             setPageText(index, mTextEditor.getTitle());
513 
514             if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) {
515                 Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
516                         "Error opening the Android XML editor. Is the document an XML file?");
517                 throw new RuntimeException("Android XML Editor Error", new CoreException(status));
518             }
519 
520             IStructuredModel xml_model = getModelForRead();
521             if (xml_model != null) {
522                 try {
523                     mXmlModelStateListener = new XmlModelStateListener();
524                     xml_model.addModelStateListener(mXmlModelStateListener);
525                     mXmlModelStateListener.modelChanged(xml_model);
526                 } catch (Exception e) {
527                     AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$
528                 } finally {
529                     xml_model.releaseFromRead();
530                 }
531             }
532         } catch (PartInitException e) {
533             ErrorDialog.openError(getSite().getShell(),
534                     "Android XML Editor Error", null, e.getStatus());
535         }
536     }
537 
538     /**
539      * Returns the ISourceViewer associated with the Structured Text editor.
540      */
541     public final ISourceViewer getStructuredSourceViewer() {
542         if (mTextEditor != null) {
543             // We can't access mEditor.getSourceViewer() because it is protected,
544             // however getTextViewer simply returns the SourceViewer casted, so we
545             // can use it instead.
546             return mTextEditor.getTextViewer();
547         }
548         return null;
549     }
550 
551     /**
552      * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
553      * Editor) or null if not available.
554      */
555     public final IStructuredDocument getStructuredDocument() {
556         if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
557             return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
558         }
559         return null;
560     }
561 
562     /**
563      * Returns a version of the model that has been shared for read.
564      * <p/>
565      * Callers <em>must</em> call model.releaseFromRead() when done, typically
566      * in a try..finally clause.
567      *
568      * Portability note: this uses getModelManager which is part of wst.sse.core; however
569      * the interface returned is part of wst.sse.core.internal.provisional so we can
570      * expect it to change in a distant future if they start cleaning their codebase,
571      * however unlikely that is.
572      *
573      * @return The model for the XML document or null if cannot be obtained from the editor
574      */
575     public final IStructuredModel getModelForRead() {
576         IStructuredDocument document = getStructuredDocument();
577         if (document != null) {
578             IModelManager mm = StructuredModelManager.getModelManager();
579             if (mm != null) {
580                 return mm.getModelForRead(document);
581             }
582         }
583         return null;
584     }
585 
586     /**
587      * Returns a version of the model that has been shared for edit.
588      * <p/>
589      * Callers <em>must</em> call model.releaseFromEdit() when done, typically
590      * in a try..finally clause.
591      *
592      * @return The model for the XML document or null if cannot be obtained from the editor
593      */
594     public final IStructuredModel getModelForEdit() {
595         IStructuredDocument document = getStructuredDocument();
596         if (document != null) {
597             IModelManager mm = StructuredModelManager.getModelManager();
598             if (mm != null) {
599                 return mm.getModelForEdit(document);
600             }
601         }
602         return null;
603     }
604 
605     /**
606      * Helper class to perform edits on the XML model whilst making sure the
607      * model has been prepared to be changed.
608      * <p/>
609      * It first gets a model for edition using {@link #getModelForEdit()},
610      * then calls {@link IStructuredModel#aboutToChangeModel()},
611      * then performs the requested action
612      * and finally calls {@link IStructuredModel#changedModel()}
613      * and {@link IStructuredModel#releaseFromEdit()}.
614      * <p/>
615      * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
616      * is called, XML model listeners will be triggered.
617      *
618      * @param edit_action Something that will change the XML.
619      */
620     public final void editXmlModel(Runnable edit_action) {
621         IStructuredModel model = getModelForEdit();
622         try {
623             model.aboutToChangeModel();
624             edit_action.run();
625         } finally {
626             // Notify the model we're done modifying it. This must *always* be executed.
627             model.changedModel();
628             model.releaseFromEdit();
629         }
630     }
631 
632     /**
633      * Starts an "undo recording" session. This is managed by the underlying undo manager
634      * associated to the structured XML model.
635      * <p/>
636      * There <em>must</em> be a corresponding call to {@link #endUndoRecording()}.
637      * <p/>
638      * beginUndoRecording/endUndoRecording calls can be nested (inner calls are ignored, only one
639      * undo operation is recorded.)
640      *
641      * @param label The label for the undo operation. Can be null but we should really try to put
642      *              something meaningful if possible.
643      * @return True if the undo recording actually started, false if any kind of error occured.
644      *         {@link #endUndoRecording()} should only be called if True is returned.
645      */
646     private final boolean beginUndoRecording(String label) {
647         IStructuredDocument document = getStructuredDocument();
648         if (document != null) {
649             IModelManager mm = StructuredModelManager.getModelManager();
650             if (mm != null) {
651                 IStructuredModel model = mm.getModelForEdit(document);
652                 if (model != null) {
653                     model.beginRecording(this, label);
654                     return true;
655                 }
656             }
657         }
658         return false;
659     }
660 
661     /**
662      * Ends an "undo recording" session.
663      * <p/>
664      * This is the counterpart call to {@link #beginUndoRecording(String)} and should only be
665      * used if the initial call returned true.
666      */
667     private final void endUndoRecording() {
668         IStructuredDocument document = getStructuredDocument();
669         if (document != null) {
670             IModelManager mm = StructuredModelManager.getModelManager();
671             if (mm != null) {
672                 IStructuredModel model = mm.getModelForEdit(document);
673                 if (model != null) {
674                     model.endRecording(this);
675                 }
676             }
677         }
678     }
679 
680     /**
681      * Creates an "undo recording" session by calling the undoableAction runnable
682      * using {@link #beginUndoRecording(String)} and {@link #endUndoRecording()}.
683      * <p>
684      * You can nest several calls to {@link #wrapUndoRecording(String, Runnable)}, only one
685      * recording session will be created.
686      *
687      * @param label The label for the undo operation. Can be null. Ideally we should really try
688      *              to put something meaningful if possible.
689      */
690     public void wrapUndoRecording(String label, Runnable undoableAction) {
691         boolean recording = false;
692         try {
693             recording = beginUndoRecording(label);
694             undoableAction.run();
695         } finally {
696             if (recording) {
697                 endUndoRecording();
698             }
699         }
700     }
701 
702     /**
703      * Returns the XML {@link Document} or null if we can't get it
704      */
705     protected final Document getXmlDocument(IStructuredModel model) {
706         if (model == null) {
707             AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$
708             return null;
709         }
710 
711         if (model instanceof IDOMModel) {
712             IDOMModel dom_model = (IDOMModel) model;
713             return dom_model.getDocument();
714         }
715         return null;
716     }
717 
718     /**
719      * Returns the {@link IProject} for the edited file.
720      */
721     public IProject getProject() {
722         if (mTextEditor != null) {
723             IEditorInput input = mTextEditor.getEditorInput();
724             if (input instanceof FileEditorInput) {
725                 FileEditorInput fileInput = (FileEditorInput)input;
726                 IFile inputFile = fileInput.getFile();
727 
728                 if (inputFile != null) {
729                     return inputFile.getProject();
730                 }
731             }
732         }
733 
734         return null;
735     }
736 
737     /**
738      * Returns the {@link AndroidTargetData} for the edited file.
739      */
740     public AndroidTargetData getTargetData() {
741         IProject project = getProject();
742         if (project != null) {
743             Sdk currentSdk = Sdk.getCurrent();
744             if (currentSdk != null) {
745                 IAndroidTarget target = currentSdk.getTarget(project);
746 
747                 if (target != null) {
748                     return currentSdk.getTargetData(target);
749                 }
750             }
751         }
752 
753         return null;
754     }
755 
756 
757     /**
758      * Listen to changes in the underlying XML model in the structured editor.
759      */
760     private class XmlModelStateListener implements IModelStateListener {
761 
762         /**
763          * A model is about to be changed. This typically is initiated by one
764          * client of the model, to signal a large change and/or a change to the
765          * model's ID or base Location. A typical use might be if a client might
766          * want to suspend processing until all changes have been made.
767          * <p/>
768          * This AndroidEditor implementation of IModelChangedListener is empty.
769          */
770         public void modelAboutToBeChanged(IStructuredModel model) {
771             // pass
772         }
773 
774         /**
775          * Signals that the changes foretold by modelAboutToBeChanged have been
776          * made. A typical use might be to refresh, or to resume processing that
777          * was suspended as a result of modelAboutToBeChanged.
778          * <p/>
779          * This AndroidEditor implementation calls the xmlModelChanged callback.
780          */
781         public void modelChanged(IStructuredModel model) {
782             xmlModelChanged(getXmlDocument(model));
783         }
784 
785         /**
786          * Notifies that a model's dirty state has changed, and passes that state
787          * in isDirty. A model becomes dirty when any change is made, and becomes
788          * not-dirty when the model is saved.
789          * <p/>
790          * This AndroidEditor implementation of IModelChangedListener is empty.
791          */
792         public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
793             // pass
794         }
795 
796         /**
797          * A modelDeleted means the underlying resource has been deleted. The
798          * model itself is not removed from model management until all have
799          * released it. Note: baseLocation is not (necessarily) changed in this
800          * event, but may not be accurate.
801          * <p/>
802          * This AndroidEditor implementation of IModelChangedListener is empty.
803          */
804         public void modelResourceDeleted(IStructuredModel model) {
805             // pass
806         }
807 
808         /**
809          * A model has been renamed or copied (as in saveAs..). In the renamed
810          * case, the two paramenters are the same instance, and only contain the
811          * new info for id and base location.
812          * <p/>
813          * This AndroidEditor implementation of IModelChangedListener is empty.
814          */
815         public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
816             // pass
817         }
818 
819         /**
820          * This AndroidEditor implementation of IModelChangedListener is empty.
821          */
822         public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
823             // pass
824         }
825 
826         /**
827          * This AndroidEditor implementation of IModelChangedListener is empty.
828          */
829         public void modelReinitialized(IStructuredModel structuredModel) {
830             // pass
831         }
832     }
833 }
834