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