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