/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors; import static org.eclipse.wst.sse.ui.internal.actions.StructuredTextEditorActionConstants.ACTION_NAME_FORMAT_DOCUMENT; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; import com.android.sdklib.IAndroidTarget; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IURIEditorInput; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.browser.IWorkbenchBrowserSupport; import org.eclipse.ui.forms.IManagedForm; import org.eclipse.ui.forms.editor.FormEditor; import org.eclipse.ui.forms.editor.IFormPage; import org.eclipse.ui.forms.events.HyperlinkAdapter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.events.IHyperlinkListener; import org.eclipse.ui.forms.widgets.FormText; import org.eclipse.ui.ide.IDEActionFactory; import org.eclipse.ui.ide.IGotoMarker; import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport; import org.eclipse.ui.part.MultiPageEditorPart; import org.eclipse.ui.part.WorkbenchPart; import org.eclipse.ui.views.contentoutline.IContentOutlinePage; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.sse.core.internal.provisional.IModelManager; import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.sse.ui.StructuredTextEditor; import org.eclipse.wst.sse.ui.internal.StructuredTextViewer; import org.eclipse.wst.xml.core.internal.document.NodeContainer; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Document; import org.w3c.dom.Node; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; /** * Multi-page form editor for Android XML files. *
* It is designed to work with a {@link StructuredTextEditor} that will display an XML file. *null
the editor attempts to
* find the default page in the properties of the {@link IResource} object being edited.
*/
public void selectDefaultPage(String defaultPageId) {
if (defaultPageId == null) {
IFile file = getInputFile();
if (file != null) {
QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
getClass().getSimpleName() + PREF_CURRENT_PAGE);
String pageId;
try {
pageId = file.getPersistentProperty(qname);
if (pageId != null) {
defaultPageId = pageId;
}
} catch (CoreException e) {
// ignored
}
}
}
if (defaultPageId != null) {
try {
setActivePage(Integer.parseInt(defaultPageId));
} catch (Exception e) {
// We can get NumberFormatException from parseInt but also
// AssertionError from setActivePage when the index is out of bounds.
// Generally speaking we just want to ignore any exception and fall back on the
// first page rather than crash the editor load. Logging the error is enough.
AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
}
} else if (AdtPrefs.getPrefs().isXmlEditorPreferred(getPersistenceCategory())) {
setActivePage(mTextPageIndex);
}
}
/** The layout editor */
public static final int CATEGORY_LAYOUT = 1 << 0;
/** The manifest editor */
public static final int CATEGORY_MANIFEST = 1 << 1;
/** Any other XML editor */
public static final int CATEGORY_OTHER = 1 << 2;
/**
* Returns the persistence category to use for this editor; this should be
* one of the {@code CATEGORY_} constants such as {@link #CATEGORY_MANIFEST},
* {@link #CATEGORY_LAYOUT}, {@link #CATEGORY_OTHER}, ...
* * The persistence category is used to group editors together when it comes * to certain types of persistence metadata. For example, whether this type * of file was most recently edited graphically or with an XML text editor. * We'll open new files in the same text or graphical mode as the last time * the user edited a file of the same persistence category. *
* Before we added the persistence category, we had a single boolean flag * recording whether the XML files were most recently edited graphically or * not. However, this meant that users can't for example prefer to edit * Manifest files graphically and string files via XML. By splitting the * editors up into categories, we can track the mode at a finer granularity, * and still allow similar editors such as those used for animations and * colors to be treated the same way. * * @return the persistence category constant */ protected int getPersistenceCategory() { return CATEGORY_OTHER; } /** * Removes all the pages from the editor. */ protected void removePages() { int count = getPageCount(); for (int i = count - 1 ; i >= 0 ; i--) { removePage(i); } } /** * Overrides the parent's setActivePage to be able to switch to the xml editor. * * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page. * This is needed because the editor doesn't actually derive from IFormPage and thus * doesn't have the get-by-page-id method. In this case, the method returns null since * IEditorPart does not implement IFormPage. */ @Override public IFormPage setActivePage(String pageId) { if (pageId.equals(TEXT_EDITOR_ID)) { super.setActivePage(mTextPageIndex); return null; } else { return super.setActivePage(pageId); } } /** * Notifies this multi-page editor that the page with the given id has been * activated. This method is called when the user selects a different tab. * * @see MultiPageEditorPart#pageChange(int) */ @Override protected void pageChange(int newPageIndex) { super.pageChange(newPageIndex); // Do not record page changes during creation of pages if (mIsCreatingPage) { return; } IFile file = getInputFile(); if (file != null) { QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, getClass().getSimpleName() + PREF_CURRENT_PAGE); try { file.setPersistentProperty(qname, Integer.toString(newPageIndex)); } catch (CoreException e) { // ignore } } boolean isTextPage = newPageIndex == mTextPageIndex; AdtPrefs.getPrefs().setXmlEditorPreferred(getPersistenceCategory(), isTextPage); } /** * Returns true if the active page is the editor page * * @return true if the active page is the editor page */ public boolean isEditorPageActive() { return getActivePage() == mTextPageIndex; } /** * Returns the {@link IFile} matching the editor's input or null. */ @Nullable public IFile getInputFile() { IEditorInput input = getEditorInput(); if (input instanceof IFileEditorInput) { return ((IFileEditorInput) input).getFile(); } return null; } /** * Removes attached listeners. * * @see WorkbenchPart */ @Override public void dispose() { IStructuredModel xml_model = getModelForRead(); if (xml_model != null) { try { if (mXmlModelStateListener != null) { xml_model.removeModelStateListener(mXmlModelStateListener); } } finally { xml_model.releaseFromRead(); } } if (mTargetListener != null) { AdtPlugin.getDefault().removeTargetListener(mTargetListener); mTargetListener = null; } super.dispose(); } /** * Commit all dirty pages then saves the contents of the text editor. *
* This works by committing all data to the XML model and then * asking the Structured XML Editor to save the XML. * * @see IEditorPart */ @Override public void doSave(IProgressMonitor monitor) { commitPages(true /* onSave */); if (AdtPrefs.getPrefs().isFormatOnSave()) { IAction action = mTextEditor.getAction(ACTION_NAME_FORMAT_DOCUMENT); if (action != null) { try { mIgnoreXmlUpdate = true; action.run(); } finally { mIgnoreXmlUpdate = false; } } } // The actual "save" operation is done by the Structured XML Editor getEditor(mTextPageIndex).doSave(monitor); // Check for errors on save, if enabled if (AdtPrefs.getPrefs().isLintOnSave()) { runLint(); } } /** * Tells the editor to start a Lint check. * It's up to the caller to check whether this should be done depending on preferences. * * The default implementation is to call {@link #startLintJob()}. * * @return The Job started by {@link EclipseLintRunner} or null if no job was started. */ protected Job runLint() { return startLintJob(); } /** * Utility method that creates a Job to run Lint on the current document. * Does not wait for the job to finish - just returns immediately. * * @return a new job, or null * @see EclipseLintRunner#startLint(java.util.List, IResource, IDocument, * boolean, boolean) */ @Nullable public Job startLintJob() { IFile file = getInputFile(); if (file != null) { return EclipseLintRunner.startLint(Collections.singletonList(file), file, getStructuredDocument(), false /*fatalOnly*/, false /*show*/); } return null; } /* (non-Javadoc) * Saves the contents of this editor to another object. *
* Subclasses must override this method to implement the open-save-close lifecycle
* for an editor. For greater details, see IEditorPart
*
true
if commit is performed as part
* of the 'save' operation, false
otherwise.
* @since 3.3
*/
@Override
public void commitPages(boolean onSave) {
if (pages != null) {
for (int i = 0; i < pages.size(); i++) {
Object page = pages.get(i);
if (page != null && page instanceof IFormPage) {
IFormPage form_page = (IFormPage) page;
IManagedForm managed_form = form_page.getManagedForm();
if (managed_form != null && managed_form.isDirty()) {
managed_form.commit(onSave);
}
}
}
}
}
/* (non-Javadoc)
* Returns whether the "save as" operation is supported by this editor.
*
* Subclasses must override this method to implement the open-save-close lifecycle
* for an editor. For greater details, see IEditorPart
*
* Note that the edit hooks are performed outside of the edit lock so * the hooks should not perform edits on the model without acquiring * a lock first. */ public void runEditHooks() { if (!mIgnoreXmlUpdate) { // Check for errors, if enabled if (AdtPrefs.getPrefs().isLintOnSave()) { runLint(); } } } /** * Executor which performs the given action under an edit lock (and optionally as a * single undo event). * * @param editAction the action to be executed * @param undoLabel if non null, the edit action will be run as a single undo event * and the label used as the name of the undoable action */ private final void wrapEditXmlModel(final Runnable editAction, final String undoLabel) { Display display = mTextEditor.getSite().getShell().getDisplay(); if (display.getThread() != Thread.currentThread()) { display.syncExec(new Runnable() { @Override public void run() { if (!mTextEditor.getTextViewer().getControl().isDisposed()) { wrapEditXmlModel(editAction, undoLabel); } } }); return; } IStructuredModel model = null; int undoReverseCount = 0; try { if (mIsEditXmlModelPending == 0) { try { model = getModelForEdit(); if (undoLabel != null) { // Run this action as an undoable unit. // We have to do it more than once, because in some scenarios // Eclipse WTP decides to cancel the current undo command on its // own -- see http://code.google.com/p/android/issues/detail?id=15901 // for one such call chain. By nesting these calls several times // we've incrementing the command count such that a couple of // cancellations are ignored. Interfering with this mechanism may // sound dangerous, but it appears that this undo-termination is // done for UI reasons to anticipate what the user wants, and we know // that in *our* scenarios we want the entire unit run as a single // unit. Here's what the documentation for // IStructuredTextUndoManager#forceEndOfPendingCommand says // "Normally, the undo manager can figure out the best // times when to end a pending command and begin a new // one ... to the structure of a structured // document. There are times, however, when clients may // wish to override those algorithms and end one earlier // than normal. The one known case is for multi-page // editors. If a user is on one page, and type '123' as // attribute value, then click around to other parts of // page, or different pages, then return to '123|' and // type 456, then "undo" they typically expect the undo // to just undo what they just typed, the 456, not the // whole attribute value." for (int i = 0; i < 4; i++) { model.beginRecording(this, undoLabel); undoReverseCount++; } } model.aboutToChangeModel(); } catch (Throwable t) { // This is never supposed to happen unless we suddenly don't have a model. // If it does, we don't want to even try to modify anyway. AdtPlugin.log(t, "XML Editor failed to get model to edit"); //$NON-NLS-1$ return; } } mIsEditXmlModelPending++; editAction.run(); } finally { mIsEditXmlModelPending--; if (model != null) { try { boolean oldIgnore = mIgnoreXmlUpdate; try { mIgnoreXmlUpdate = true; if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) { if (mFormatNode == getUiRootNode()) { reformatDocument(); } else { Node node = mFormatNode.getXmlNode(); if (node instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion) node; int begin = region.getStartOffset(); int end = region.getEndOffset(); if (!mFormatChildren) { // This will format just the attribute list end = begin + 1; } if (mFormatChildren && node == node.getOwnerDocument().getDocumentElement()) { reformatDocument(); } else { reformatRegion(begin, end); } } } mFormatNode = null; mFormatChildren = false; } // Notify the model we're done modifying it. This must *always* be executed. model.changedModel(); // Clean up the undo unit. This is done more than once as explained // above for beginRecording. for (int i = 0; i < undoReverseCount; i++) { model.endRecording(this); } } finally { mIgnoreXmlUpdate = oldIgnore; } } catch (Exception e) { AdtPlugin.log(e, "Failed to clean up undo unit"); } model.releaseFromEdit(); if (mIsEditXmlModelPending < 0) { AdtPlugin.log(IStatus.ERROR, "wrapEditXmlModel finished with invalid nested counter==%1$d", //$NON-NLS-1$ mIsEditXmlModelPending); mIsEditXmlModelPending = 0; } runEditHooks(); // Notify listeners IStructuredModel readModel = getModelForRead(); if (readModel != null) { try { mXmlModelStateListener.modelChanged(readModel); } catch (Exception e) { AdtPlugin.log(e, "Error while notifying changes"); //$NON-NLS-1$ } finally { readModel.releaseFromRead(); } } } } } /** * Does this editor participate in the "format GUI editor changes" option? * * @return true if this editor supports automatically formatting XML * affected by GUI changes */ public boolean supportsFormatOnGuiEdit() { return false; } /** * Mark the given node as needing to be formatted when the current edits are * done, provided the user has turned that option on (see * {@link AdtPrefs#getFormatGuiXml()}). * * @param node the node to be scheduled for formatting * @param attributesOnly if true, only update the attributes list of the * node, otherwise update the node recursively (e.g. all children * too) */ public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) { if (!supportsFormatOnGuiEdit()) { return; } if (node == mFormatNode) { if (!attributesOnly) { mFormatChildren = true; } } else if (mFormatNode == null) { mFormatNode = node; mFormatChildren = !attributesOnly; } else { if (mFormatNode.isAncestorOf(node)) { mFormatChildren = true; } else if (node.isAncestorOf(mFormatNode)) { mFormatNode = node; mFormatChildren = true; } else { // Two independent nodes; format their closest common ancestor. // Later we could consider having a small number of independent nodes // and formatting those, and only switching to formatting the common ancestor // when the number of individual nodes gets large. mFormatChildren = true; mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node); } } } /** * Creates an "undo recording" session by calling the undoableAction runnable * under an undo session. *
* This also automatically starts an edit XML session, as if * {@link #wrapEditXmlModel(Runnable)} had been called. ** You can nest several calls to {@link #wrapUndoEditXmlModel(String, Runnable)}, only one * recording session will be created. * * @param label The label for the undo operation. Can be null. Ideally we should really try * to put something meaningful if possible. * @param undoableAction the action to be run as a single undoable unit */ public void wrapUndoEditXmlModel(String label, Runnable undoableAction) { assert label != null : "All undoable actions should have a label"; wrapEditXmlModel(undoableAction, label == null ? "" : label); //$NON-NLS-1$ } /** * Returns true when the runnable of {@link #wrapEditXmlModel(Runnable)} is currently * being executed. This means it is safe to actually edit the XML model. * * @return true if the XML model is already locked for edits */ public boolean isEditXmlModelPending() { return mIsEditXmlModelPending > 0; } /** * Returns the XML {@link Document} or null if we can't get it */ public final Document getXmlDocument(IStructuredModel model) { if (model == null) { AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$ return null; } if (model instanceof IDOMModel) { IDOMModel dom_model = (IDOMModel) model; return dom_model.getDocument(); } return null; } /** * Returns the {@link IProject} for the edited file. */ @Nullable public IProject getProject() { IFile file = getInputFile(); if (file != null) { return file.getProject(); } return null; } /** * Returns the {@link AndroidTargetData} for the edited file. */ @Nullable public AndroidTargetData getTargetData() { IProject project = getProject(); if (project != null) { Sdk currentSdk = Sdk.getCurrent(); if (currentSdk != null) { IAndroidTarget target = currentSdk.getTarget(project); if (target != null) { return currentSdk.getTargetData(target); } } } IEditorInput input = getEditorInput(); if (input instanceof IURIEditorInput) { IURIEditorInput urlInput = (IURIEditorInput) input; Sdk currentSdk = Sdk.getCurrent(); if (currentSdk != null) { try { String path = AdtUtils.getFile(urlInput.getURI().toURL()).getPath(); IAndroidTarget[] targets = currentSdk.getTargets(); for (IAndroidTarget target : targets) { if (path.startsWith(target.getLocation())) { return currentSdk.getTargetData(target); } } } catch (MalformedURLException e) { // File might be in some other weird random location we can't // handle: Just ignore these } } } return null; } /** * Shows the editor range corresponding to the given XML node. This will * front the editor and select the text range. * * @param xmlNode The DOM node to be shown. The DOM node should be an XML * node from the existing XML model used by the structured XML * editor; it will not do attribute matching to find a * "corresponding" element in the document from some foreign DOM * tree. * @return True if the node was shown. */ public boolean show(Node xmlNode) { if (xmlNode instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion)xmlNode; IEditorPart textPage = getEditor(mTextPageIndex); if (textPage instanceof StructuredTextEditor) { StructuredTextEditor editor = (StructuredTextEditor) textPage; setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); // Note - we cannot use region.getLength() because that seems to // always return 0. int regionLength = region.getEndOffset() - region.getStartOffset(); editor.selectAndReveal(region.getStartOffset(), regionLength); return true; } } return false; } /** * Selects and reveals the given range in the text editor * * @param start the beginning offset * @param length the length of the region to show * @param frontTab if true, front the tab, otherwise just make the selection but don't * change the active tab */ public void show(int start, int length, boolean frontTab) { IEditorPart textPage = getEditor(mTextPageIndex); if (textPage instanceof StructuredTextEditor) { StructuredTextEditor editor = (StructuredTextEditor) textPage; if (frontTab) { setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); } editor.selectAndReveal(start, length); if (frontTab) { editor.setFocus(); } } } /** * Returns true if this editor has more than one page (usually a graphical view and an * editor) * * @return true if this editor has multiple pages */ public boolean hasMultiplePages() { return getPageCount() > 1; } /** * Get the XML text directly from the editor. * * @param xmlNode The node whose XML text we want to obtain. * @return The XML representation of the {@link Node}, or null if there was an error. */ public String getXmlText(Node xmlNode) { String data = null; IStructuredModel model = getModelForRead(); try { IStructuredDocument document = getStructuredDocument(); if (xmlNode instanceof NodeContainer) { // The easy way to get the source of an SSE XML node. data = ((NodeContainer) xmlNode).getSource(); } else if (xmlNode instanceof IndexedRegion && document != null) { // Try harder. IndexedRegion region = (IndexedRegion) xmlNode; int start = region.getStartOffset(); int end = region.getEndOffset(); if (end > start) { data = document.get(start, end - start); } } } catch (BadLocationException e) { // the region offset was invalid. ignore. } finally { model.releaseFromRead(); } return data; } /** * Formats the text around the given caret range, using the current Eclipse * XML formatter settings. * * @param begin The starting offset of the range to be reformatted. * @param end The ending offset of the range to be reformatted. */ public void reformatRegion(int begin, int end) { ISourceViewer textViewer = getStructuredSourceViewer(); // Clamp text range to valid offsets. IDocument document = textViewer.getDocument(); int documentLength = document.getLength(); end = Math.min(end, documentLength); begin = Math.min(begin, end); if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { // Workarounds which only apply to the builtin Eclipse formatter: // // It turns out the XML formatter does *NOT* format things correctly if you // select just a region of text. You *MUST* also include the leading whitespace // on the line, or it will dedent all the content to column 0. Therefore, // we must figure out the offset of the start of the line that contains the // beginning of the tag. try { IRegion lineInformation = document.getLineInformationOfOffset(begin); if (lineInformation != null) { int lineBegin = lineInformation.getOffset(); if (lineBegin != begin) { begin = lineBegin; } else if (begin > 0) { // Trick #2: It turns out that, if an XML element starts in column 0, // then the XML formatter will NOT indent it (even if its parent is // indented). If you on the other hand include the end of the previous // line (the newline), THEN the formatter also correctly inserts the // element. Therefore, we adjust the beginning range to include the // previous line (if we are not already in column 0 of the first line) // in the case where the element starts the line. begin--; } } } catch (BadLocationException e) { // This cannot happen because we already clamped the offsets AdtPlugin.log(e, e.toString()); } } if (textViewer instanceof StructuredTextViewer) { StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; int operation = ISourceViewer.FORMAT; boolean canFormat = structuredTextViewer.canDoOperation(operation); if (canFormat) { StyledText textWidget = textViewer.getTextWidget(); textWidget.setSelection(begin, end); boolean oldIgnore = mIgnoreXmlUpdate; try { // Formatting does not affect the XML model so ignore notifications // about model edits from this mIgnoreXmlUpdate = true; structuredTextViewer.doOperation(operation); } finally { mIgnoreXmlUpdate = oldIgnore; } textWidget.setSelection(0, 0); } } } /** * Invokes content assist in this editor at the given offset * * @param offset the offset to invoke content assist at, or -1 to leave * caret alone */ public void invokeContentAssist(int offset) { ISourceViewer textViewer = getStructuredSourceViewer(); if (textViewer instanceof StructuredTextViewer) { StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; int operation = ISourceViewer.CONTENTASSIST_PROPOSALS; boolean allowed = structuredTextViewer.canDoOperation(operation); if (allowed) { if (offset != -1) { StyledText textWidget = textViewer.getTextWidget(); // Clamp text range to valid offsets. IDocument document = textViewer.getDocument(); int documentLength = document.getLength(); offset = Math.max(0, Math.min(offset, documentLength)); textWidget.setSelection(offset, offset); } structuredTextViewer.doOperation(operation); } } } /** * Formats the XML region corresponding to the given node. * * @param node The node to be formatted. */ public void reformatNode(Node node) { if (mIsCreatingPage) { return; } if (node instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion) node; int begin = region.getStartOffset(); int end = region.getEndOffset(); reformatRegion(begin, end); } } /** * Formats the XML document according to the user's XML formatting settings. */ public void reformatDocument() { ISourceViewer textViewer = getStructuredSourceViewer(); if (textViewer instanceof StructuredTextViewer) { StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer; int operation = StructuredTextViewer.FORMAT_DOCUMENT; boolean canFormat = structuredTextViewer.canDoOperation(operation); if (canFormat) { boolean oldIgnore = mIgnoreXmlUpdate; try { // Formatting does not affect the XML model so ignore notifications // about model edits from this mIgnoreXmlUpdate = true; structuredTextViewer.doOperation(operation); } finally { mIgnoreXmlUpdate = oldIgnore; } } } } /** * Returns the indentation String of the given node. * * @param xmlNode The node whose indentation we want. * @return The indent-string of the given node, or "" if the indentation for some reason could * not be computed. */ public String getIndent(Node xmlNode) { return getIndent(getStructuredDocument(), xmlNode); } /** * Returns the indentation String of the given node. * * @param document The Eclipse document containing the XML * @param xmlNode The node whose indentation we want. * @return The indent-string of the given node, or "" if the indentation for some reason could * not be computed. */ public static String getIndent(IDocument document, Node xmlNode) { if (xmlNode instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion)xmlNode; int startOffset = region.getStartOffset(); return getIndentAtOffset(document, startOffset); } return ""; //$NON-NLS-1$ } /** * Returns the indentation String at the line containing the given offset * * @param document the document containing the offset * @param offset The offset of a character on a line whose indentation we seek * @return The indent-string of the given node, or "" if the indentation for some * reason could not be computed. */ public static String getIndentAtOffset(IDocument document, int offset) { try { IRegion lineInformation = document.getLineInformationOfOffset(offset); if (lineInformation != null) { int lineBegin = lineInformation.getOffset(); if (lineBegin != offset) { String prefix = document.get(lineBegin, offset - lineBegin); // It's possible that the tag whose indentation we seek is not // at the beginning of the line. In that case we'll just return // the indentation of the line itself. for (int i = 0; i < prefix.length(); i++) { if (!Character.isWhitespace(prefix.charAt(i))) { return prefix.substring(0, i); } } return prefix; } } } catch (BadLocationException e) { AdtPlugin.log(e, "Could not obtain indentation"); //$NON-NLS-1$ } return ""; //$NON-NLS-1$ } /** * Returns the active {@link AndroidXmlEditor}, provided it matches the given source * viewer * * @param viewer the source viewer to ensure the active editor is associated with * @return the active editor provided it matches the given source viewer or null. */ public static AndroidXmlEditor fromTextViewer(ITextViewer viewer) { IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (wwin != null) { // Try the active editor first. IWorkbenchPage page = wwin.getActivePage(); if (page != null) { IEditorPart editor = page.getActiveEditor(); if (editor instanceof AndroidXmlEditor) { ISourceViewer ssviewer = ((AndroidXmlEditor) editor).getStructuredSourceViewer(); if (ssviewer == viewer) { return (AndroidXmlEditor) editor; } } } // If that didn't work, try all the editors for (IWorkbenchPage page2 : wwin.getPages()) { if (page2 != null) { for (IEditorReference editorRef : page2.getEditorReferences()) { IEditorPart editor = editorRef.getEditor(false /*restore*/); if (editor instanceof AndroidXmlEditor) { ISourceViewer ssviewer = ((AndroidXmlEditor) editor).getStructuredSourceViewer(); if (ssviewer == viewer) { return (AndroidXmlEditor) editor; } } } } } } return null; } /** Called when this editor is activated */ public void activated() { if (getActivePage() == mTextPageIndex) { updateActionBindings(); } } /** Called when this editor is deactivated */ public void deactivated() { } /** * Listen to changes in the underlying XML model in the structured editor. */ private class XmlModelStateListener implements IModelStateListener { /** * A model is about to be changed. This typically is initiated by one * client of the model, to signal a large change and/or a change to the * model's ID or base Location. A typical use might be if a client might * want to suspend processing until all changes have been made. *
* This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelAboutToBeChanged(IStructuredModel model) { // pass } /** * Signals that the changes foretold by modelAboutToBeChanged have been * made. A typical use might be to refresh, or to resume processing that * was suspended as a result of modelAboutToBeChanged. * * This AndroidXmlEditor implementation calls the xmlModelChanged callback. */ @Override public void modelChanged(IStructuredModel model) { if (mIgnoreXmlUpdate) { return; } xmlModelChanged(getXmlDocument(model)); } /** * Notifies that a model's dirty state has changed, and passes that state * in isDirty. A model becomes dirty when any change is made, and becomes * not-dirty when the model is saved. * * This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) { // pass } /** * A modelDeleted means the underlying resource has been deleted. The * model itself is not removed from model management until all have * released it. Note: baseLocation is not (necessarily) changed in this * event, but may not be accurate. * * This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelResourceDeleted(IStructuredModel model) { // pass } /** * A model has been renamed or copied (as in saveAs..). In the renamed * case, the two parameters are the same instance, and only contain the * new info for id and base location. * * This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) { // pass } /** * This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelAboutToBeReinitialized(IStructuredModel structuredModel) { // pass } /** * This AndroidXmlEditor implementation of IModelChangedListener is empty. */ @Override public void modelReinitialized(IStructuredModel structuredModel) { // pass } } }