2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors;
19 import static org.eclipse.wst.sse.ui.internal.actions.StructuredTextEditorActionConstants.ACTION_NAME_FORMAT_DOCUMENT;
21 import com.android.ide.eclipse.adt.AdtConstants;
22 import com.android.ide.eclipse.adt.AdtPlugin;
23 import com.android.ide.eclipse.adt.AdtUtils;
24 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
25 import com.android.ide.eclipse.adt.internal.lint.LintRunner;
26 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
27 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
29 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
30 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
31 import com.android.sdklib.IAndroidTarget;
33 import org.eclipse.core.resources.IFile;
34 import org.eclipse.core.resources.IMarker;
35 import org.eclipse.core.resources.IProject;
36 import org.eclipse.core.resources.IResource;
37 import org.eclipse.core.resources.IResourceChangeEvent;
38 import org.eclipse.core.resources.IResourceChangeListener;
39 import org.eclipse.core.resources.ResourcesPlugin;
40 import org.eclipse.core.runtime.CoreException;
41 import org.eclipse.core.runtime.IProgressMonitor;
42 import org.eclipse.core.runtime.IStatus;
43 import org.eclipse.core.runtime.QualifiedName;
44 import org.eclipse.core.runtime.Status;
45 import org.eclipse.core.runtime.jobs.Job;
46 import org.eclipse.jface.action.IAction;
47 import org.eclipse.jface.dialogs.ErrorDialog;
48 import org.eclipse.jface.text.BadLocationException;
49 import org.eclipse.jface.text.IDocument;
50 import org.eclipse.jface.text.IRegion;
51 import org.eclipse.jface.text.ITextViewer;
52 import org.eclipse.jface.text.source.ISourceViewer;
53 import org.eclipse.swt.custom.StyledText;
54 import org.eclipse.swt.widgets.Display;
55 import org.eclipse.ui.IActionBars;
56 import org.eclipse.ui.IEditorInput;
57 import org.eclipse.ui.IEditorPart;
58 import org.eclipse.ui.IEditorSite;
59 import org.eclipse.ui.IFileEditorInput;
60 import org.eclipse.ui.IWorkbenchPage;
61 import org.eclipse.ui.IWorkbenchWindow;
62 import org.eclipse.ui.PartInitException;
63 import org.eclipse.ui.PlatformUI;
64 import org.eclipse.ui.actions.ActionFactory;
65 import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
66 import org.eclipse.ui.forms.IManagedForm;
67 import org.eclipse.ui.forms.editor.FormEditor;
68 import org.eclipse.ui.forms.editor.IFormPage;
69 import org.eclipse.ui.forms.events.HyperlinkAdapter;
70 import org.eclipse.ui.forms.events.HyperlinkEvent;
71 import org.eclipse.ui.forms.events.IHyperlinkListener;
72 import org.eclipse.ui.forms.widgets.FormText;
73 import org.eclipse.ui.ide.IGotoMarker;
74 import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
75 import org.eclipse.ui.part.MultiPageEditorPart;
76 import org.eclipse.ui.part.WorkbenchPart;
77 import org.eclipse.wst.sse.core.StructuredModelManager;
78 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
79 import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
80 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
81 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
82 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
83 import org.eclipse.wst.sse.ui.StructuredTextEditor;
84 import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
85 import org.eclipse.wst.xml.core.internal.document.NodeContainer;
86 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
87 import org.w3c.dom.Document;
88 import org.w3c.dom.Node;
90 import java.net.MalformedURLException;
94 * Multi-page form editor for Android XML files.
96 * It is designed to work with a {@link StructuredTextEditor} that will display an XML file.
98 * Derived classes must implement createFormPages to create the forms before the
99 * source editor. This can be a no-op if desired.
101 @SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet
102 public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener {
104 /** Icon used for the XML source page. */
105 public static final String ICON_XML_PAGE = "editor_page_source"; //$NON-NLS-1$
107 /** Preference name for the current page of this file */
108 private static final String PREF_CURRENT_PAGE = "_current_page"; //$NON-NLS-1$
110 /** Id string used to create the Android SDK browser */
111 private static String BROWSER_ID = "android"; //$NON-NLS-1$
113 /** Page id of the XML source editor, used for switching tabs programmatically */
114 public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
116 /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
117 public static final int TEXT_WIDTH_HINT = 50;
119 /** Page index of the text editor (always the last page) */
120 protected int mTextPageIndex;
121 /** The text editor */
122 private StructuredTextEditor mTextEditor;
123 /** Listener for the XML model from the StructuredEditor */
124 private XmlModelStateListener mXmlModelStateListener;
125 /** Listener to update the root node if the target of the file is changed because of a
126 * SDK location change or a project target change */
127 private TargetChangeListener mTargetListener = null;
129 /** flag set during page creation */
130 private boolean mIsCreatingPage = false;
133 * Flag used to ignore XML model updates. For example, the flag is set during
134 * formatting. A format operation should completely preserve the semantics of the XML
135 * so the document listeners can use this flag to skip updating the model when edits
136 * are observed during a formatting operation
138 protected boolean mIgnoreXmlUpdate;
141 * Flag indicating we're inside {@link #wrapEditXmlModel(Runnable)}.
142 * This is a counter, which allows us to nest the edit XML calls.
143 * There is no pending operation when the counter is at zero.
145 private int mIsEditXmlModelPending;
148 * Usually null, but during an editing operation, represents the highest
149 * node which should be formatted when the editing operation is complete.
151 private UiElementNode mFormatNode;
154 * Whether {@link #mFormatNode} should be formatted recursively, or just
155 * the node itself (its arguments)
157 private boolean mFormatChildren;
160 * Creates a form editor.
161 * <p/>The editor will setup a {@link ITargetChangeListener} and call
162 * {@link #initUiRootNode(boolean)}, when the SDK or the target changes.
164 * @see #AndroidXmlEditor(boolean)
166 public AndroidXmlEditor() {
171 * Creates a form editor.
172 * @param addTargetListener whether to create an {@link ITargetChangeListener}.
174 public AndroidXmlEditor(boolean addTargetListener) {
177 ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
179 if (addTargetListener) {
180 mTargetListener = new TargetChangeListener() {
182 public IProject getProject() {
183 return AndroidXmlEditor.this.getProject();
187 public void reload() {
188 commitPages(false /* onSave */);
190 // recreate the ui root node always
191 initUiRootNode(true /*force*/);
194 AdtPlugin.getDefault().addTargetListener(mTargetListener);
198 // ---- Abstract Methods ----
201 * Returns the root node of the UI element hierarchy manipulated by the current
204 abstract public UiElementNode getUiRootNode();
207 * Creates the various form pages.
209 * Derived classes must implement this to add their own specific tabs.
211 abstract protected void createFormPages();
214 * Called by the base class {@link AndroidXmlEditor} once all pages (custom form pages
215 * as well as text editor page) have been created. This give a chance to deriving
216 * classes to adjust behavior once the text page has been created.
218 protected void postCreatePages() {
219 // Nothing in the base class.
223 * Creates the initial UI Root Node, including the known mandatory elements.
224 * @param force if true, a new UiManifestNode is recreated even if it already exists.
226 abstract protected void initUiRootNode(boolean force);
229 * Subclasses should override this method to process the new XML Model, which XML
230 * root node is given.
232 * The base implementation is empty.
234 * @param xml_doc The XML document, if available, or null if none exists.
236 protected void xmlModelChanged(Document xml_doc) {
241 * Controls whether XML models are ignored or not.
243 * @param ignore when true, ignore all subsequent XML model updates, when false start
244 * processing XML model updates again
246 public void setIgnoreXmlUpdate(boolean ignore) {
247 mIgnoreXmlUpdate = ignore;
250 // ---- Base Class Overrides, Interfaces Implemented ----
253 public Object getAdapter(Class adapter) {
254 Object result = super.getAdapter(adapter);
256 if (result != null && adapter.equals(IGotoMarker.class) ) {
257 final IGotoMarker gotoMarker = (IGotoMarker) result;
258 return new IGotoMarker() {
259 public void gotoMarker(IMarker marker) {
260 gotoMarker.gotoMarker(marker);
262 // Lint markers should always jump to XML text
263 if (marker.getType().equals(AdtConstants.MARKER_LINT)) {
264 IEditorPart editor = AdtUtils.getActiveEditor();
265 if (editor instanceof AndroidXmlEditor) {
266 AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor;
267 xmlEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
270 } catch (CoreException e) {
271 AdtPlugin.log(e, null);
281 * Creates the pages of the multi-page editor.
284 protected void addPages() {
285 createAndroidPages();
286 selectDefaultPage(null /* defaultPageId */);
290 * Creates the page for the Android Editors
292 protected void createAndroidPages() {
293 mIsCreatingPage = true;
296 createUndoRedoActions();
298 mIsCreatingPage = false;
302 * Returns whether the editor is currently creating its pages.
304 public boolean isCreatingPages() {
305 return mIsCreatingPage;
311 * If the page is an instance of {@link IPageImageProvider}, the image returned by
312 * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
315 public int addPage(IFormPage page) throws PartInitException {
316 int index = super.addPage(page);
317 if (page instanceof IPageImageProvider) {
318 setPageImage(index, ((IPageImageProvider) page).getPageImage());
326 * If the editor is an instance of {@link IPageImageProvider}, the image returned by
327 * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
330 public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException {
331 int index = super.addPage(editor, input);
332 if (editor instanceof IPageImageProvider) {
333 setPageImage(index, ((IPageImageProvider) editor).getPageImage());
339 * Creates undo redo actions for the editor site (so that it works for any page of this
340 * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor}
341 * (aka the XML text editor.)
343 private void createUndoRedoActions() {
344 IActionBars bars = getEditorSite().getActionBars();
346 IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
347 bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
349 action = mTextEditor.getAction(ActionFactory.REDO.getId());
350 bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
352 bars.updateActionBars();
357 * Selects the default active page.
358 * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
359 * find the default page in the properties of the {@link IResource} object being edited.
361 protected void selectDefaultPage(String defaultPageId) {
362 if (defaultPageId == null) {
363 IFile file = getInputFile();
365 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
366 getClass().getSimpleName() + PREF_CURRENT_PAGE);
369 pageId = file.getPersistentProperty(qname);
370 if (pageId != null) {
371 defaultPageId = pageId;
373 } catch (CoreException e) {
379 if (defaultPageId != null) {
381 setActivePage(Integer.parseInt(defaultPageId));
382 } catch (Exception e) {
383 // We can get NumberFormatException from parseInt but also
384 // AssertionError from setActivePage when the index is out of bounds.
385 // Generally speaking we just want to ignore any exception and fall back on the
386 // first page rather than crash the editor load. Logging the error is enough.
387 AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
393 * Removes all the pages from the editor.
395 protected void removePages() {
396 int count = getPageCount();
397 for (int i = count - 1 ; i >= 0 ; i--) {
403 * Overrides the parent's setActivePage to be able to switch to the xml editor.
405 * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
406 * This is needed because the editor doesn't actually derive from IFormPage and thus
407 * doesn't have the get-by-page-id method. In this case, the method returns null since
408 * IEditorPart does not implement IFormPage.
411 public IFormPage setActivePage(String pageId) {
412 if (pageId.equals(TEXT_EDITOR_ID)) {
413 super.setActivePage(mTextPageIndex);
416 return super.setActivePage(pageId);
422 * Notifies this multi-page editor that the page with the given id has been
423 * activated. This method is called when the user selects a different tab.
425 * @see MultiPageEditorPart#pageChange(int)
428 protected void pageChange(int newPageIndex) {
429 super.pageChange(newPageIndex);
431 // Do not record page changes during creation of pages
432 if (mIsCreatingPage) {
436 IFile file = getInputFile();
438 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
439 getClass().getSimpleName() + PREF_CURRENT_PAGE);
441 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
442 } catch (CoreException e) {
449 * Notifies this listener that some resource changes
450 * are happening, or have already happened.
452 * Closes all project files on project close.
453 * @see IResourceChangeListener
455 public void resourceChanged(final IResourceChangeEvent event) {
456 if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
457 IFile file = getInputFile();
458 if (file != null && file.getProject().equals(event.getResource())) {
459 final IEditorInput input = getEditorInput();
460 Display.getDefault().asyncExec(new Runnable() {
462 // FIXME understand why this code is accessing the current window's pages,
463 // if that's *this* instance, we have a local pages member from the super
464 // class we can use directly. If this is justified, please explain.
465 IWorkbenchPage[] windowPages = getSite().getWorkbenchWindow().getPages();
466 for (int i = 0; i < windowPages.length; i++) {
467 IEditorPart editorPart = windowPages[i].findEditor(input);
468 windowPages[i].closeEditor(editorPart, true);
477 * Initializes the editor part with a site and input.
479 * Checks that the input is an instance of {@link IFileEditorInput}.
484 public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
485 if (!(editorInput instanceof IFileEditorInput))
486 throw new PartInitException("Invalid Input: Must be IFileEditorInput");
487 super.init(site, editorInput);
491 * Returns the {@link IFile} matching the editor's input or null.
493 * By construction, the editor input has to be an {@link IFileEditorInput} so it must
494 * have an associated {@link IFile}. Null can only be returned if this editor has no
497 public IFile getInputFile() {
498 IEditorInput input = getEditorInput();
499 if (input instanceof IFileEditorInput) {
500 return ((IFileEditorInput) input).getFile();
506 * Removes attached listeners.
511 public void dispose() {
512 IStructuredModel xml_model = getModelForRead();
513 if (xml_model != null) {
515 if (mXmlModelStateListener != null) {
516 xml_model.removeModelStateListener(mXmlModelStateListener);
520 xml_model.releaseFromRead();
523 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
525 if (mTargetListener != null) {
526 AdtPlugin.getDefault().removeTargetListener(mTargetListener);
527 mTargetListener = null;
534 * Commit all dirty pages then saves the contents of the text editor.
536 * This works by committing all data to the XML model and then
537 * asking the Structured XML Editor to save the XML.
542 public void doSave(IProgressMonitor monitor) {
543 commitPages(true /* onSave */);
545 if (AdtPrefs.getPrefs().isFormatOnSave()) {
546 IAction action = mTextEditor.getAction(ACTION_NAME_FORMAT_DOCUMENT);
547 if (action != null) {
549 mIgnoreXmlUpdate = true;
552 mIgnoreXmlUpdate = false;
557 // The actual "save" operation is done by the Structured XML Editor
558 getEditor(mTextPageIndex).doSave(monitor);
563 protected Job runLintOnSave() {
564 // Check for errors, if enabled
565 if (AdtPrefs.getPrefs().isLintOnSave()) {
566 return LintRunner.startLint(getInputFile(), getStructuredDocument());
573 * Saves the contents of this editor to another object.
575 * Subclasses must override this method to implement the open-save-close lifecycle
576 * for an editor. For greater details, see <code>IEditorPart</code>
582 public void doSaveAs() {
583 commitPages(true /* onSave */);
585 IEditorPart editor = getEditor(mTextPageIndex);
587 setPageText(mTextPageIndex, editor.getTitle());
588 setInput(editor.getEditorInput());
592 * Commits all dirty pages in the editor. This method should
593 * be called as a first step of a 'save' operation.
595 * This is the same implementation as in {@link FormEditor}
596 * except it fixes two bugs: a cast to IFormPage is done
597 * from page.get(i) <em>before</em> being tested with instanceof.
598 * Another bug is that the last page might be a null pointer.
600 * The incorrect casting makes the original implementation crash due
601 * to our {@link StructuredTextEditor} not being an {@link IFormPage}
602 * so we have to override and duplicate to fix it.
604 * @param onSave <code>true</code> if commit is performed as part
605 * of the 'save' operation, <code>false</code> otherwise.
609 public void commitPages(boolean onSave) {
611 for (int i = 0; i < pages.size(); i++) {
612 Object page = pages.get(i);
613 if (page != null && page instanceof IFormPage) {
614 IFormPage form_page = (IFormPage) page;
615 IManagedForm managed_form = form_page.getManagedForm();
616 if (managed_form != null && managed_form.isDirty()) {
617 managed_form.commit(onSave);
625 * Returns whether the "save as" operation is supported by this editor.
627 * Subclasses must override this method to implement the open-save-close lifecycle
628 * for an editor. For greater details, see <code>IEditorPart</code>
634 public boolean isSaveAsAllowed() {
638 // ---- Local methods ----
642 * Helper method that creates a new hyper-link Listener.
643 * Used by derived classes which need active links in {@link FormText}.
645 * This link listener handles two kinds of URLs:
647 * <li> Links starting with "http" are simply sent to a local browser.
648 * <li> Links starting with "file:/" are simply sent to a local browser.
649 * <li> Links starting with "page:" are expected to be an editor page id to switch to.
650 * <li> Other links are ignored.
653 * @return A new hyper-link listener for FormText to use.
655 public final IHyperlinkListener createHyperlinkListener() {
656 return new HyperlinkAdapter() {
658 * Switch to the page corresponding to the link that has just been clicked.
659 * For this purpose, the HREF of the <a> tags above is the page ID to switch to.
662 public void linkActivated(HyperlinkEvent e) {
663 super.linkActivated(e);
664 String link = e.data.toString();
665 if (link.startsWith("http") || //$NON-NLS-1$
666 link.startsWith("file:/")) { //$NON-NLS-1$
667 openLinkInBrowser(link);
668 } else if (link.startsWith("page:")) { //$NON-NLS-1$
669 // Switch to an internal page
670 setActivePage(link.substring(5 /* strlen("page:") */));
677 * Open the http link into a browser
679 * @param link The URL to open in a browser
681 private void openLinkInBrowser(String link) {
683 IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
684 wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
685 } catch (PartInitException e1) {
687 } catch (MalformedURLException e1) {
693 * Creates the XML source editor.
695 * Memorizes the index page of the source editor (it's always the last page, but the number
696 * of pages before can change.)
698 * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
699 * Finally triggers modelChanged() on the model listener -- derived classes can use this
700 * to initialize the model the first time.
702 * Called only once <em>after</em> createFormPages.
704 private void createTextEditor() {
706 if (AdtPlugin.DEBUG_XML_FILE_INIT) {
709 "%s.createTextEditor: input=%s %s",
711 getEditorInput() == null ? "null" : getEditorInput().getClass(),
712 getEditorInput() == null ? "null" : getEditorInput().toString()
715 org.eclipse.core.runtime.IAdaptable adaptable= getEditorInput();
716 IFile file1 = (IFile)adaptable.getAdapter(IFile.class);
717 org.eclipse.core.runtime.IPath location= file1.getFullPath();
718 org.eclipse.core.resources.IWorkspaceRoot workspaceRoot= ResourcesPlugin.getWorkspace().getRoot();
719 IFile file2 = workspaceRoot.getFile(location);
722 org.eclipse.core.runtime.content.IContentDescription desc = file2.getContentDescription();
723 org.eclipse.core.runtime.content.IContentType type = desc.getContentType();
725 AdtPlugin.log(IStatus.ERROR,
726 "file %s description %s %s; contentType %s %s",
728 desc == null ? "null" : desc.getClass(),
729 desc == null ? "null" : desc.toString(),
730 type == null ? "null" : type.getClass(),
731 type == null ? "null" : type.toString());
733 } catch (CoreException e) {
738 mTextEditor = new StructuredTextEditor();
739 int index = addPage(mTextEditor, getEditorInput());
740 mTextPageIndex = index;
741 setPageText(index, mTextEditor.getTitle());
743 IconFactory.getInstance().getIcon(ICON_XML_PAGE));
745 if (AdtPlugin.DEBUG_XML_FILE_INIT) {
746 AdtPlugin.log(IStatus.ERROR, "Found document class: %1$s, file=%2$s",
747 mTextEditor.getTextViewer().getDocument() != null ?
748 mTextEditor.getTextViewer().getDocument().getClass() :
754 if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) {
755 Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
756 "Error opening the Android XML editor. Is the document an XML file?");
757 throw new RuntimeException("Android XML Editor Error", new CoreException(status));
760 IStructuredModel xml_model = getModelForRead();
761 if (xml_model != null) {
763 mXmlModelStateListener = new XmlModelStateListener();
764 xml_model.addModelStateListener(mXmlModelStateListener);
765 mXmlModelStateListener.modelChanged(xml_model);
766 } catch (Exception e) {
767 AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$
769 xml_model.releaseFromRead();
772 } catch (PartInitException e) {
773 ErrorDialog.openError(getSite().getShell(),
774 "Android XML Editor Error", null, e.getStatus());
779 * Returns the ISourceViewer associated with the Structured Text editor.
781 public final ISourceViewer getStructuredSourceViewer() {
782 if (mTextEditor != null) {
783 // We can't access mEditor.getSourceViewer() because it is protected,
784 // however getTextViewer simply returns the SourceViewer casted, so we
785 // can use it instead.
786 return mTextEditor.getTextViewer();
792 * Return the {@link StructuredTextEditor} associated with this XML editor
794 * @return the associated {@link StructuredTextEditor}
796 public StructuredTextEditor getStructuredTextEditor() {
801 * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
802 * Editor) or null if not available.
804 public IStructuredDocument getStructuredDocument() {
805 if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
806 return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
812 * Returns a version of the model that has been shared for read.
814 * Callers <em>must</em> call model.releaseFromRead() when done, typically
815 * in a try..finally clause.
817 * Portability note: this uses getModelManager which is part of wst.sse.core; however
818 * the interface returned is part of wst.sse.core.internal.provisional so we can
819 * expect it to change in a distant future if they start cleaning their codebase,
820 * however unlikely that is.
822 * @return The model for the XML document or null if cannot be obtained from the editor
824 public IStructuredModel getModelForRead() {
825 IStructuredDocument document = getStructuredDocument();
826 if (document != null) {
827 IModelManager mm = StructuredModelManager.getModelManager();
829 // TODO simplify this by not using the internal IStructuredDocument.
830 // Instead we can now use mm.getModelForRead(getFile()).
831 // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
832 // method. IIRC 3.3 didn't have it.
834 return mm.getModelForRead(document);
841 * Returns a version of the model that has been shared for edit.
843 * Callers <em>must</em> call model.releaseFromEdit() when done, typically
844 * in a try..finally clause.
846 * Because of this, it is mandatory to use the wrapper
847 * {@link #wrapEditXmlModel(Runnable)} which executes a runnable into a
848 * properly configured model and then performs whatever cleanup is necessary.
850 * @return The model for the XML document or null if cannot be obtained from the editor
852 private IStructuredModel getModelForEdit() {
853 IStructuredDocument document = getStructuredDocument();
854 if (document != null) {
855 IModelManager mm = StructuredModelManager.getModelManager();
857 // TODO simplify this by not using the internal IStructuredDocument.
858 // Instead we can now use mm.getModelForRead(getFile()).
859 // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
860 // method. IIRC 3.3 didn't have it.
862 return mm.getModelForEdit(document);
869 * Helper class to perform edits on the XML model whilst making sure the
870 * model has been prepared to be changed.
872 * It first gets a model for edition using {@link #getModelForEdit()},
873 * then calls {@link IStructuredModel#aboutToChangeModel()},
874 * then performs the requested action
875 * and finally calls {@link IStructuredModel#changedModel()}
876 * and {@link IStructuredModel#releaseFromEdit()}.
878 * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
879 * is called, XML model listeners will be triggered.
881 * Calls can be nested: only the first outer call will actually start and close the edit
884 * This method is <em>not synchronized</em> and is not thread safe.
885 * Callers must be using it from the the main UI thread.
887 * @param editAction Something that will change the XML.
889 public final void wrapEditXmlModel(Runnable editAction) {
890 wrapEditXmlModel(editAction, null);
894 * Executor which performs the given action under an edit lock (and optionally as a
895 * single undo event).
897 * @param editAction the action to be executed
898 * @param undoLabel if non null, the edit action will be run as a single undo event
899 * and the label used as the name of the undoable action
901 private final void wrapEditXmlModel(Runnable editAction, String undoLabel) {
902 IStructuredModel model = null;
903 int undoReverseCount = 0;
906 if (mIsEditXmlModelPending == 0) {
908 model = getModelForEdit();
909 if (undoLabel != null) {
910 // Run this action as an undoable unit.
911 // We have to do it more than once, because in some scenarios
912 // Eclipse WTP decides to cancel the current undo command on its
913 // own -- see http://code.google.com/p/android/issues/detail?id=15901
914 // for one such call chain. By nesting these calls several times
915 // we've incrementing the command count such that a couple of
916 // cancellations are ignored. Interfering which this mechanism may
917 // sound dangerous, but it appears that this undo-termination is
918 // done for UI reasons to anticipate what the user wants, and we know
919 // that in *our* scenarios we want the entire unit run as a single
920 // unit. Here's what the documentation for
921 // IStructuredTextUndoManager#forceEndOfPendingCommand says
922 // "Normally, the undo manager can figure out the best
923 // times when to end a pending command and begin a new
924 // one ... to the structure of a structured
925 // document. There are times, however, when clients may
926 // wish to override those algorithms and end one earlier
927 // than normal. The one known case is for multi-page
928 // editors. If a user is on one page, and type '123' as
929 // attribute value, then click around to other parts of
930 // page, or different pages, then return to '123|' and
931 // type 456, then "undo" they typically expect the undo
932 // to just undo what they just typed, the 456, not the
933 // whole attribute value."
934 for (int i = 0; i < 4; i++) {
935 model.beginRecording(this, undoLabel);
939 model.aboutToChangeModel();
940 } catch (Throwable t) {
941 // This is never supposed to happen unless we suddenly don't have a model.
942 // If it does, we don't want to even try to modify anyway.
943 AdtPlugin.log(t, "XML Editor failed to get model to edit"); //$NON-NLS-1$
947 mIsEditXmlModelPending++;
950 mIsEditXmlModelPending--;
953 // Notify the model we're done modifying it. This must *always* be executed.
954 model.changedModel();
956 if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) {
957 if (!mFormatNode.hasError()) {
958 if (mFormatNode == getUiRootNode()) {
961 Node node = mFormatNode.getXmlNode();
962 if (node instanceof IndexedRegion) {
963 IndexedRegion region = (IndexedRegion) node;
964 int begin = region.getStartOffset();
965 int end = region.getEndOffset();
967 if (!mFormatChildren) {
968 // This will format just the attribute list
972 model.aboutToChangeModel();
974 reformatRegion(begin, end);
976 model.changedModel();
982 mFormatChildren = false;
985 // Clean up the undo unit. This is done more than once as explained
986 // above for beginRecording.
987 for (int i = 0; i < undoReverseCount; i++) {
988 model.endRecording(this);
990 } catch (Exception e) {
991 AdtPlugin.log(e, "Failed to clean up undo unit");
993 model.releaseFromEdit();
995 if (mIsEditXmlModelPending < 0) {
996 AdtPlugin.log(IStatus.ERROR,
997 "wrapEditXmlModel finished with invalid nested counter==%1$d", //$NON-NLS-1$
998 mIsEditXmlModelPending);
999 mIsEditXmlModelPending = 0;
1006 * Does this editor participate in the "format GUI editor changes" option?
1008 * @return true if this editor supports automatically formatting XML
1009 * affected by GUI changes
1011 public boolean supportsFormatOnGuiEdit() {
1016 * Mark the given node as needing to be formatted when the current edits are
1017 * done, provided the user has turned that option on (see
1018 * {@link AdtPrefs#getFormatGuiXml()}).
1020 * @param node the node to be scheduled for formatting
1021 * @param attributesOnly if true, only update the attributes list of the
1022 * node, otherwise update the node recursively (e.g. all children
1025 public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) {
1026 if (!supportsFormatOnGuiEdit()) {
1030 if (node == mFormatNode) {
1031 if (!attributesOnly) {
1032 mFormatChildren = true;
1034 } else if (mFormatNode == null) {
1036 mFormatChildren = !attributesOnly;
1038 if (mFormatNode.isAncestorOf(node)) {
1039 mFormatChildren = true;
1040 } else if (node.isAncestorOf(mFormatNode)) {
1042 mFormatChildren = true;
1044 // Two independent nodes; format their closest common ancestor.
1045 // Later we could consider having a small number of independent nodes
1046 // and formatting those, and only switching to formatting the common ancestor
1047 // when the number of individual nodes gets large.
1048 mFormatChildren = true;
1049 mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node);
1055 * Creates an "undo recording" session by calling the undoableAction runnable
1056 * under an undo session.
1058 * This also automatically starts an edit XML session, as if
1059 * {@link #wrapEditXmlModel(Runnable)} had been called.
1061 * You can nest several calls to {@link #wrapUndoEditXmlModel(String, Runnable)}, only one
1062 * recording session will be created.
1064 * @param label The label for the undo operation. Can be null. Ideally we should really try
1065 * to put something meaningful if possible.
1066 * @param undoableAction the action to be run as a single undoable unit
1068 public void wrapUndoEditXmlModel(String label, Runnable undoableAction) {
1069 assert label != null : "All undoable actions should have a label";
1070 wrapEditXmlModel(undoableAction, label == null ? "" : label); //$NON-NLS-1$
1074 * Returns true when the runnable of {@link #wrapEditXmlModel(Runnable)} is currently
1075 * being executed. This means it is safe to actually edit the XML model.
1077 * @return true if the XML model is already locked for edits
1079 public boolean isEditXmlModelPending() {
1080 return mIsEditXmlModelPending > 0;
1084 * Returns the XML {@link Document} or null if we can't get it
1086 protected final Document getXmlDocument(IStructuredModel model) {
1087 if (model == null) {
1088 AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$
1092 if (model instanceof IDOMModel) {
1093 IDOMModel dom_model = (IDOMModel) model;
1094 return dom_model.getDocument();
1100 * Returns the {@link IProject} for the edited file.
1102 public IProject getProject() {
1103 IFile file = getInputFile();
1105 return file.getProject();
1112 * Returns the {@link AndroidTargetData} for the edited file.
1114 public AndroidTargetData getTargetData() {
1115 IProject project = getProject();
1116 if (project != null) {
1117 Sdk currentSdk = Sdk.getCurrent();
1118 if (currentSdk != null) {
1119 IAndroidTarget target = currentSdk.getTarget(project);
1121 if (target != null) {
1122 return currentSdk.getTargetData(target);
1131 * Shows the editor range corresponding to the given XML node. This will
1132 * front the editor and select the text range.
1134 * @param xmlNode The DOM node to be shown. The DOM node should be an XML
1135 * node from the existing XML model used by the structured XML
1136 * editor; it will not do attribute matching to find a
1137 * "corresponding" element in the document from some foreign DOM
1139 * @return True if the node was shown.
1141 public boolean show(Node xmlNode) {
1142 if (xmlNode instanceof IndexedRegion) {
1143 IndexedRegion region = (IndexedRegion)xmlNode;
1145 IEditorPart textPage = getEditor(mTextPageIndex);
1146 if (textPage instanceof StructuredTextEditor) {
1147 StructuredTextEditor editor = (StructuredTextEditor) textPage;
1149 setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
1151 // Note - we cannot use region.getLength() because that seems to
1153 int regionLength = region.getEndOffset() - region.getStartOffset();
1154 editor.selectAndReveal(region.getStartOffset(), regionLength);
1163 * Selects and reveals the given range in the text editor
1165 * @param start the beginning offset
1166 * @param length the length of the region to show
1167 * @param frontTab if true, front the tab, otherwise just make the selection but don't
1168 * change the active tab
1170 public void show(int start, int length, boolean frontTab) {
1171 IEditorPart textPage = getEditor(mTextPageIndex);
1172 if (textPage instanceof StructuredTextEditor) {
1173 StructuredTextEditor editor = (StructuredTextEditor) textPage;
1175 setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
1177 editor.selectAndReveal(start, length);
1185 * Returns true if this editor has more than one page (usually a graphical view and an
1188 * @return true if this editor has multiple pages
1190 public boolean hasMultiplePages() {
1191 return getPageCount() > 1;
1195 * Get the XML text directly from the editor.
1197 * @param xmlNode The node whose XML text we want to obtain.
1198 * @return The XML representation of the {@link Node}, or null if there was an error.
1200 public String getXmlText(Node xmlNode) {
1202 IStructuredModel model = getModelForRead();
1204 IStructuredDocument document = getStructuredDocument();
1205 if (xmlNode instanceof NodeContainer) {
1206 // The easy way to get the source of an SSE XML node.
1207 data = ((NodeContainer) xmlNode).getSource();
1208 } else if (xmlNode instanceof IndexedRegion && document != null) {
1210 IndexedRegion region = (IndexedRegion) xmlNode;
1211 int start = region.getStartOffset();
1212 int end = region.getEndOffset();
1215 data = document.get(start, end - start);
1218 } catch (BadLocationException e) {
1219 // the region offset was invalid. ignore.
1221 model.releaseFromRead();
1227 * Formats the text around the given caret range, using the current Eclipse
1228 * XML formatter settings.
1230 * @param begin The starting offset of the range to be reformatted.
1231 * @param end The ending offset of the range to be reformatted.
1233 public void reformatRegion(int begin, int end) {
1234 ISourceViewer textViewer = getStructuredSourceViewer();
1236 // Clamp text range to valid offsets.
1237 IDocument document = textViewer.getDocument();
1238 int documentLength = document.getLength();
1239 end = Math.min(end, documentLength);
1240 begin = Math.min(begin, end);
1242 if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
1243 // Workarounds which only apply to the builtin Eclipse formatter:
1245 // It turns out the XML formatter does *NOT* format things correctly if you
1246 // select just a region of text. You *MUST* also include the leading whitespace
1247 // on the line, or it will dedent all the content to column 0. Therefore,
1248 // we must figure out the offset of the start of the line that contains the
1249 // beginning of the tag.
1251 IRegion lineInformation = document.getLineInformationOfOffset(begin);
1252 if (lineInformation != null) {
1253 int lineBegin = lineInformation.getOffset();
1254 if (lineBegin != begin) {
1256 } else if (begin > 0) {
1257 // Trick #2: It turns out that, if an XML element starts in column 0,
1258 // then the XML formatter will NOT indent it (even if its parent is
1259 // indented). If you on the other hand include the end of the previous
1260 // line (the newline), THEN the formatter also correctly inserts the
1261 // element. Therefore, we adjust the beginning range to include the
1262 // previous line (if we are not already in column 0 of the first line)
1263 // in the case where the element starts the line.
1267 } catch (BadLocationException e) {
1268 // This cannot happen because we already clamped the offsets
1269 AdtPlugin.log(e, e.toString());
1273 if (textViewer instanceof StructuredTextViewer) {
1274 StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
1275 int operation = ISourceViewer.FORMAT;
1276 boolean canFormat = structuredTextViewer.canDoOperation(operation);
1278 StyledText textWidget = textViewer.getTextWidget();
1279 textWidget.setSelection(begin, end);
1282 // Formatting does not affect the XML model so ignore notifications
1283 // about model edits from this
1284 mIgnoreXmlUpdate = true;
1285 structuredTextViewer.doOperation(operation);
1287 mIgnoreXmlUpdate = false;
1290 textWidget.setSelection(0, 0);
1296 * Invokes content assist in this editor at the given offset
1298 * @param offset the offset to invoke content assist at, or -1 to leave
1301 public void invokeContentAssist(int offset) {
1302 ISourceViewer textViewer = getStructuredSourceViewer();
1303 if (textViewer instanceof StructuredTextViewer) {
1304 StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
1305 int operation = ISourceViewer.CONTENTASSIST_PROPOSALS;
1306 boolean allowed = structuredTextViewer.canDoOperation(operation);
1309 StyledText textWidget = textViewer.getTextWidget();
1310 // Clamp text range to valid offsets.
1311 IDocument document = textViewer.getDocument();
1312 int documentLength = document.getLength();
1313 offset = Math.max(0, Math.min(offset, documentLength));
1314 textWidget.setSelection(offset, offset);
1316 structuredTextViewer.doOperation(operation);
1322 * Formats the XML region corresponding to the given node.
1324 * @param node The node to be formatted.
1326 public void reformatNode(Node node) {
1327 if (mIsCreatingPage) {
1331 if (node instanceof IndexedRegion) {
1332 IndexedRegion region = (IndexedRegion) node;
1333 int begin = region.getStartOffset();
1334 int end = region.getEndOffset();
1335 reformatRegion(begin, end);
1340 * Formats the XML document according to the user's XML formatting settings.
1342 public void reformatDocument() {
1343 ISourceViewer textViewer = getStructuredSourceViewer();
1344 if (textViewer instanceof StructuredTextViewer) {
1345 StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
1346 int operation = StructuredTextViewer.FORMAT_DOCUMENT;
1347 boolean canFormat = structuredTextViewer.canDoOperation(operation);
1350 // Formatting does not affect the XML model so ignore notifications
1351 // about model edits from this
1352 mIgnoreXmlUpdate = true;
1353 structuredTextViewer.doOperation(operation);
1355 mIgnoreXmlUpdate = false;
1362 * Returns the indentation String of the given node.
1364 * @param xmlNode The node whose indentation we want.
1365 * @return The indent-string of the given node, or "" if the indentation for some reason could
1368 public String getIndent(Node xmlNode) {
1369 return getIndent(getStructuredDocument(), xmlNode);
1373 * Returns the indentation String of the given node.
1375 * @param document The Eclipse document containing the XML
1376 * @param xmlNode The node whose indentation we want.
1377 * @return The indent-string of the given node, or "" if the indentation for some reason could
1380 public static String getIndent(IDocument document, Node xmlNode) {
1381 if (xmlNode instanceof IndexedRegion) {
1382 IndexedRegion region = (IndexedRegion)xmlNode;
1383 int startOffset = region.getStartOffset();
1384 return getIndentAtOffset(document, startOffset);
1387 return ""; //$NON-NLS-1$
1391 * Returns the indentation String at the line containing the given offset
1393 * @param document the document containing the offset
1394 * @param offset The offset of a character on a line whose indentation we seek
1395 * @return The indent-string of the given node, or "" if the indentation for some
1396 * reason could not be computed.
1398 public static String getIndentAtOffset(IDocument document, int offset) {
1400 IRegion lineInformation = document.getLineInformationOfOffset(offset);
1401 if (lineInformation != null) {
1402 int lineBegin = lineInformation.getOffset();
1403 if (lineBegin != offset) {
1404 String prefix = document.get(lineBegin, offset - lineBegin);
1406 // It's possible that the tag whose indentation we seek is not
1407 // at the beginning of the line. In that case we'll just return
1408 // the indentation of the line itself.
1409 for (int i = 0; i < prefix.length(); i++) {
1410 if (!Character.isWhitespace(prefix.charAt(i))) {
1411 return prefix.substring(0, i);
1418 } catch (BadLocationException e) {
1419 AdtPlugin.log(e, "Could not obtain indentation"); //$NON-NLS-1$
1422 return ""; //$NON-NLS-1$
1426 * Returns the active {@link AndroidXmlEditor}, provided it matches the given source
1429 * @param viewer the source viewer to ensure the active editor is associated with
1430 * @return the active editor provided it matches the given source viewer
1432 public static AndroidXmlEditor getAndroidXmlEditor(ITextViewer viewer) {
1433 IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
1435 IWorkbenchPage page = wwin.getActivePage();
1437 IEditorPart editor = page.getActiveEditor();
1438 if (editor instanceof AndroidXmlEditor) {
1439 ISourceViewer ssviewer =
1440 ((AndroidXmlEditor) editor).getStructuredSourceViewer();
1441 if (ssviewer == viewer) {
1442 return (AndroidXmlEditor) editor;
1452 * Listen to changes in the underlying XML model in the structured editor.
1454 private class XmlModelStateListener implements IModelStateListener {
1457 * A model is about to be changed. This typically is initiated by one
1458 * client of the model, to signal a large change and/or a change to the
1459 * model's ID or base Location. A typical use might be if a client might
1460 * want to suspend processing until all changes have been made.
1462 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1464 public void modelAboutToBeChanged(IStructuredModel model) {
1469 * Signals that the changes foretold by modelAboutToBeChanged have been
1470 * made. A typical use might be to refresh, or to resume processing that
1471 * was suspended as a result of modelAboutToBeChanged.
1473 * This AndroidXmlEditor implementation calls the xmlModelChanged callback.
1475 public void modelChanged(IStructuredModel model) {
1476 xmlModelChanged(getXmlDocument(model));
1480 * Notifies that a model's dirty state has changed, and passes that state
1481 * in isDirty. A model becomes dirty when any change is made, and becomes
1482 * not-dirty when the model is saved.
1484 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1486 public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
1491 * A modelDeleted means the underlying resource has been deleted. The
1492 * model itself is not removed from model management until all have
1493 * released it. Note: baseLocation is not (necessarily) changed in this
1494 * event, but may not be accurate.
1496 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1498 public void modelResourceDeleted(IStructuredModel model) {
1503 * A model has been renamed or copied (as in saveAs..). In the renamed
1504 * case, the two parameters are the same instance, and only contain the
1505 * new info for id and base location.
1507 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1509 public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
1514 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1516 public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
1521 * This AndroidXmlEditor implementation of IModelChangedListener is empty.
1523 public void modelReinitialized(IStructuredModel structuredModel) {