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