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