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.layout; 18 19 import com.android.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.android.annotations.VisibleForTesting; 22 import com.android.annotations.VisibleForTesting.Visibility; 23 import com.android.ide.eclipse.adt.AdtConstants; 24 import com.android.ide.eclipse.adt.AdtPlugin; 25 import com.android.ide.eclipse.adt.internal.editors.XmlEditorMultiOutline; 26 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate; 27 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; 28 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 29 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 30 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider; 31 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; 32 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 33 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 34 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 35 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 36 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar; 37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; 38 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage; 39 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; 40 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 41 import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; 42 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 43 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 44 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 45 import com.android.resources.ResourceFolderType; 46 import com.android.sdklib.IAndroidTarget; 47 48 import org.eclipse.core.resources.IFile; 49 import org.eclipse.core.resources.IProject; 50 import org.eclipse.core.runtime.IProgressMonitor; 51 import org.eclipse.core.runtime.IStatus; 52 import org.eclipse.core.runtime.NullProgressMonitor; 53 import org.eclipse.core.runtime.jobs.IJobChangeEvent; 54 import org.eclipse.core.runtime.jobs.Job; 55 import org.eclipse.core.runtime.jobs.JobChangeAdapter; 56 import org.eclipse.jface.text.source.ISourceViewer; 57 import org.eclipse.jface.viewers.ISelection; 58 import org.eclipse.jface.viewers.ISelectionChangedListener; 59 import org.eclipse.jface.viewers.SelectionChangedEvent; 60 import org.eclipse.ui.IActionBars; 61 import org.eclipse.ui.IEditorInput; 62 import org.eclipse.ui.IEditorPart; 63 import org.eclipse.ui.IFileEditorInput; 64 import org.eclipse.ui.IPartListener; 65 import org.eclipse.ui.ISelectionListener; 66 import org.eclipse.ui.ISelectionService; 67 import org.eclipse.ui.IShowEditorInput; 68 import org.eclipse.ui.IWorkbenchPage; 69 import org.eclipse.ui.IWorkbenchPart; 70 import org.eclipse.ui.IWorkbenchPartSite; 71 import org.eclipse.ui.IWorkbenchWindow; 72 import org.eclipse.ui.PartInitException; 73 import org.eclipse.ui.forms.editor.IFormPage; 74 import org.eclipse.ui.part.FileEditorInput; 75 import org.eclipse.ui.views.contentoutline.IContentOutlinePage; 76 import org.eclipse.ui.views.properties.IPropertySheetPage; 77 import org.eclipse.wst.sse.ui.StructuredTextEditor; 78 import org.w3c.dom.Document; 79 import org.w3c.dom.Node; 80 81 import java.util.HashMap; 82 import java.util.HashSet; 83 import java.util.Set; 84 85 /** 86 * Multi-page form editor for /res/layout XML files. 87 */ 88 public class LayoutEditorDelegate extends CommonXmlDelegate 89 implements IShowEditorInput, IPartListener, CommonXmlDelegate.IActionContributorDelegate { 90 91 public static class Creator implements IDelegateCreator { 92 @Override 93 @SuppressWarnings("unchecked") 94 public LayoutEditorDelegate createForFile( 95 CommonXmlEditor delegator, 96 ResourceFolderType type) { 97 if (ResourceFolderType.LAYOUT == type) { 98 return new LayoutEditorDelegate(delegator); 99 } 100 101 return null; 102 } 103 } 104 105 /** 106 * Old standalone-editor ID. 107 * Use {@link CommonXmlEditor#ID} instead. 108 */ 109 public static final String LEGACY_EDITOR_ID = 110 AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$ 111 112 /** Root node of the UI element hierarchy */ 113 private UiDocumentNode mUiDocRootNode; 114 115 private GraphicalEditorPart mGraphicalEditor; 116 private int mGraphicalEditorIndex; 117 118 /** Implementation of the {@link IContentOutlinePage} for this editor */ 119 private OutlinePage mLayoutOutline; 120 121 /** The XML editor outline */ 122 private IContentOutlinePage mEditorOutline; 123 124 /** Multiplexing outline, used for multi-page editors that have their own outline */ 125 private XmlEditorMultiOutline mMultiOutline; 126 127 /** 128 * Temporary flag set by the editor caret listener which is used to cause 129 * the next getAdapter(IContentOutlinePage.class) call to return the editor 130 * outline rather than the multi-outline. See the {@link #delegateGetAdapter} 131 * method for details. 132 */ 133 private boolean mCheckOutlineAdapter; 134 135 /** Custom implementation of {@link IPropertySheetPage} for this editor */ 136 private IPropertySheetPage mPropertyPage; 137 138 private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap = 139 new HashMap<String, ElementDescriptor>(); 140 141 142 /** 143 * Flag indicating if the replacement file is due to a config change. 144 * If false, it means the new file is due to an "open action" from the user. 145 */ 146 private boolean mNewFileOnConfigChange = false; 147 148 /** 149 * Checks whether an editor part is an instance of {@link CommonXmlEditor} 150 * with an associated {@link LayoutEditorDelegate} delegate. 151 * 152 * @param editorPart An editor part. Can be null. 153 * @return The {@link LayoutEditorDelegate} delegate associated with the editor or null. 154 */ 155 public static @Nullable LayoutEditorDelegate fromEditor(@Nullable IEditorPart editorPart) { 156 if (editorPart instanceof CommonXmlEditor) { 157 CommonXmlDelegate delegate = ((CommonXmlEditor) editorPart).getDelegate(); 158 if (delegate instanceof LayoutEditorDelegate) { 159 return ((LayoutEditorDelegate) delegate); 160 } 161 } else if (editorPart instanceof GraphicalEditorPart) { 162 GraphicalEditorPart part = (GraphicalEditorPart) editorPart; 163 return part.getEditorDelegate(); 164 } 165 return null; 166 } 167 168 /** 169 * Creates the form editor for resources XML files. 170 */ 171 @VisibleForTesting(visibility=Visibility.PRIVATE) 172 protected LayoutEditorDelegate(CommonXmlEditor editor) { 173 super(editor, new LayoutContentAssist()); 174 // Note that LayoutEditor has its own listeners and does not 175 // need to call editor.addDefaultTargetListener(). 176 } 177 178 /** 179 * Returns the {@link RulesEngine} associated with this editor 180 * 181 * @return the {@link RulesEngine} associated with this editor. 182 */ 183 public RulesEngine getRulesEngine() { 184 return mGraphicalEditor.getRulesEngine(); 185 } 186 187 /** 188 * Returns the {@link GraphicalEditorPart} associated with this editor 189 * 190 * @return the {@link GraphicalEditorPart} associated with this editor 191 */ 192 public GraphicalEditorPart getGraphicalEditor() { 193 return mGraphicalEditor; 194 } 195 196 /** 197 * @return The root node of the UI element hierarchy 198 */ 199 @Override 200 public UiDocumentNode getUiRootNode() { 201 return mUiDocRootNode; 202 } 203 204 public void setNewFileOnConfigChange(boolean state) { 205 mNewFileOnConfigChange = state; 206 } 207 208 // ---- Base Class Overrides ---- 209 210 @Override 211 public void dispose() { 212 super.dispose(); 213 if (mGraphicalEditor != null) { 214 mGraphicalEditor.dispose(); 215 mGraphicalEditor = null; 216 } 217 getEditor().getSite().getPage().removePartListener(this); 218 } 219 220 /** 221 * Save the XML. 222 * <p/> 223 * Clients must NOT call this directly. Instead they should always 224 * call {@link CommonXmlEditor#doSave(IProgressMonitor)} so that th 225 * editor super class can commit the data properly. 226 * <p/> 227 * Here we just need to tell the graphical editor that the model has 228 * been saved. 229 */ 230 @Override 231 public void delegateDoSave(IProgressMonitor monitor) { 232 super.delegateDoSave(monitor); 233 if (mGraphicalEditor != null) { 234 mGraphicalEditor.doSave(monitor); 235 } 236 } 237 238 /** 239 * Create the various form pages. 240 */ 241 @Override 242 public void delegateCreateFormPages() { 243 try { 244 // get the file being edited so that it can be passed to the layout editor. 245 IFile editedFile = null; 246 IEditorInput input = getEditor().getEditorInput(); 247 if (input instanceof FileEditorInput) { 248 FileEditorInput fileInput = (FileEditorInput)input; 249 editedFile = fileInput.getFile(); 250 } else { 251 AdtPlugin.log(IStatus.ERROR, 252 "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$ 253 input.toString()); 254 } 255 256 // It is possible that the Layout Editor already exits if a different version 257 // of the same layout is being opened (either through "open" action from 258 // the user, or through a configuration change in the configuration selector.) 259 if (mGraphicalEditor == null) { 260 261 // Instantiate GLE v2 262 mGraphicalEditor = new GraphicalEditorPart(this); 263 264 mGraphicalEditorIndex = getEditor().addPage(mGraphicalEditor, 265 getEditor().getEditorInput()); 266 getEditor().setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle()); 267 268 mGraphicalEditor.openFile(editedFile); 269 } else { 270 if (mNewFileOnConfigChange) { 271 mGraphicalEditor.changeFileOnNewConfig(editedFile); 272 mNewFileOnConfigChange = false; 273 } else { 274 mGraphicalEditor.replaceFile(editedFile); 275 } 276 } 277 278 // put in place the listener to handle layout recompute only when needed. 279 getEditor().getSite().getPage().addPartListener(this); 280 } catch (PartInitException e) { 281 AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ 282 } 283 } 284 285 @Override 286 public void delegatePostCreatePages() { 287 // Optional: set the default page. Eventually a default page might be 288 // restored by selectDefaultPage() later based on the last page used by the user. 289 // For example, to make the last page the default one (rather than the first page), 290 // uncomment this line: 291 // setActivePage(getPageCount() - 1); 292 } 293 294 /* (non-java doc) 295 * Change the tab/title name to include the name of the layout. 296 */ 297 @Override 298 public void delegateSetInput(IEditorInput input) { 299 handleNewInput(input); 300 } 301 302 /* 303 * (non-Javadoc) 304 * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput) 305 */ 306 public void delegateSetInputWithNotify(IEditorInput input) { 307 handleNewInput(input); 308 } 309 310 /** 311 * Called to replace the current {@link IEditorInput} with another one. 312 * <p/>This is used when {@link LayoutEditorMatchingStrategy} returned <code>true</code> which means we're 313 * opening a different configuration of the same layout. 314 */ 315 @Override 316 public void showEditorInput(IEditorInput editorInput) { 317 if (getEditor().getEditorInput().equals(editorInput)) { 318 return; 319 } 320 321 // Save the current editor input. This must be called on the editor itself 322 // since it's the base editor that commits pending changes. 323 getEditor().doSave(new NullProgressMonitor()); 324 325 // Get the current page 326 int currentPage = getEditor().getActivePage(); 327 328 // Remove the pages, except for the graphical editor, which will be dynamically adapted 329 // to the new model. 330 // page after the graphical editor: 331 int count = getEditor().getPageCount(); 332 for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) { 333 getEditor().removePage(i); 334 } 335 // Pages before the graphical editor 336 for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) { 337 getEditor().removePage(i); 338 } 339 340 // Set the current input. We're in the delegate, the input must 341 // be set into the actual editor instance. 342 getEditor().setInputWithNotify(editorInput); 343 344 // Re-create or reload the pages with the default page shown as the previous active page. 345 getEditor().createAndroidPages(); 346 getEditor().selectDefaultPage(Integer.toString(currentPage)); 347 348 // When changing an input file of an the editor, the titlebar is not refreshed to 349 // show the new path/to/file being edited. So we force a refresh 350 getEditor().firePropertyChange(IWorkbenchPart.PROP_TITLE); 351 } 352 353 /** Performs a complete refresh of the XML model */ 354 public void refreshXmlModel() { 355 Document xmlDoc = mUiDocRootNode.getXmlDocument(); 356 357 delegateInitUiRootNode(true /*force*/); 358 mUiDocRootNode.loadFromXmlNode(xmlDoc); 359 360 // Update the model first, since it is used by the viewers. 361 // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's 362 // a no-op. Instead call onXmlModelChanged on the graphical editor. 363 364 if (mGraphicalEditor != null) { 365 mGraphicalEditor.onXmlModelChanged(); 366 } 367 } 368 369 /** 370 * Processes the new XML Model, which XML root node is given. 371 * 372 * @param xml_doc The XML document, if available, or null if none exists. 373 */ 374 @Override 375 public void delegateXmlModelChanged(Document xml_doc) { 376 // init the ui root on demand 377 delegateInitUiRootNode(false /*force*/); 378 379 mUiDocRootNode.loadFromXmlNode(xml_doc); 380 381 // Update the model first, since it is used by the viewers. 382 // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's 383 // a no-op. Instead call onXmlModelChanged on the graphical editor. 384 385 if (mGraphicalEditor != null) { 386 mGraphicalEditor.onXmlModelChanged(); 387 } 388 } 389 390 /** 391 * Tells the graphical editor to recompute its layout. 392 */ 393 public void recomputeLayout() { 394 mGraphicalEditor.recomputeLayout(); 395 } 396 397 /** 398 * Does this editor participate in the "format GUI editor changes" option? 399 * 400 * @return true since this editor supports automatically formatting XML 401 * affected by GUI changes 402 */ 403 @Override 404 public boolean delegateSupportsFormatOnGuiEdit() { 405 return true; 406 } 407 408 @Override 409 public Job delegateRunLint() { 410 Job job = super.delegateRunLint(); 411 412 if (job != null) { 413 job.addJobChangeListener(new JobChangeAdapter() { 414 @Override 415 public void done(IJobChangeEvent event) { 416 GraphicalEditorPart graphicalEditor = getGraphicalEditor(); 417 if (graphicalEditor != null) { 418 LayoutActionBar bar = graphicalEditor.getLayoutActionBar(); 419 if (!bar.isDisposed()) { 420 bar.updateErrorIndicator(); 421 } 422 } 423 } 424 }); 425 } 426 return job; 427 } 428 429 430 /** 431 * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. 432 */ 433 @Override 434 public Object delegateGetAdapter(Class<?> adapter) { 435 if (adapter == IContentOutlinePage.class) { 436 // Somebody has requested the outline. Eclipse can only have a single outline page, 437 // even for a multi-part editor: 438 // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917 439 // To work around this we use PDE's workaround of having a single multiplexing 440 // outline which switches its contents between the outline pages we register 441 // for it, and then on page switch we notify it to update itself. 442 443 // There is one complication: The XML editor outline listens for the editor 444 // selection and uses this to automatically expand its tree children and show 445 // the current node containing the caret as selected. Unfortunately, this 446 // listener code contains this: 447 // 448 // /* Bug 136310, unless this page is that part's 449 // * IContentOutlinePage, ignore the selection change */ 450 // if (part.getAdapter(IContentOutlinePage.class) == this) { 451 // 452 // This means that when we return the multiplexing outline from this getAdapter 453 // method, the outline no longer updates to track the selection. 454 // To work around this, we use the following hack^H^H^H^H technique: 455 // - Add a selection listener *before* requesting the editor outline, such 456 // that the selection listener is told about the impending selection event 457 // right before the editor outline hears about it. Set the flag 458 // mCheckOutlineAdapter to true. (We also only set it if the editor view 459 // itself is active.) 460 // - In this getAdapter method, when somebody requests the IContentOutline.class, 461 // see if mCheckOutlineAdapter to see if this request is *likely* coming 462 // from the XML editor outline. If so, make sure it is by actually looking 463 // at the signature of the caller. If it's the editor outline, then return 464 // the editor outline instance itself rather than the multiplexing outline. 465 if (mCheckOutlineAdapter && mEditorOutline != null) { 466 mCheckOutlineAdapter = false; 467 // Make *sure* this is really the editor outline calling in case 468 // future versions of Eclipse changes the sequencing or dispatch of selection 469 // events: 470 StackTraceElement[] frames = new Throwable().fillInStackTrace().getStackTrace(); 471 if (frames.length > 2) { 472 StackTraceElement frame = frames[2]; 473 if (frame.getClassName().equals( 474 "org.eclipse.wst.sse.ui.internal.contentoutline." + //$NON-NLS-1$ 475 "ConfigurableContentOutlinePage$PostSelectionServiceListener")) { //$NON-NLS-1$ 476 return mEditorOutline; 477 } 478 } 479 } 480 481 // Use a multiplexing outline: workaround for 482 // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917 483 if (mMultiOutline == null || mMultiOutline.isDisposed()) { 484 mMultiOutline = new XmlEditorMultiOutline(); 485 mMultiOutline.addSelectionChangedListener(new ISelectionChangedListener() { 486 @Override 487 public void selectionChanged(SelectionChangedEvent event) { 488 ISelection selection = event.getSelection(); 489 getEditor().getSite().getSelectionProvider().setSelection(selection); 490 SelectionManager manager = 491 mGraphicalEditor.getCanvasControl().getSelectionManager(); 492 manager.setSelection(selection); 493 } 494 }); 495 updateOutline(getEditor().getActivePageInstance()); 496 } 497 498 return mMultiOutline; 499 } 500 501 if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) { 502 if (mPropertyPage == null) { 503 mPropertyPage = new PropertySheetPage(mGraphicalEditor); 504 } 505 506 return mPropertyPage; 507 } 508 509 // return default 510 return super.delegateGetAdapter(adapter); 511 } 512 513 /** 514 * Update the contents of the outline to show either the XML editor outline 515 * or the layout editor graphical outline depending on which tab is visible 516 */ 517 private void updateOutline(IFormPage page) { 518 if (mMultiOutline == null) { 519 return; 520 } 521 522 IContentOutlinePage outline; 523 CommonXmlEditor editor = getEditor(); 524 if (!editor.isEditorPageActive()) { 525 outline = getGraphicalOutline(); 526 } else { 527 // Use plain XML editor outline instead 528 if (mEditorOutline == null) { 529 StructuredTextEditor structuredTextEditor = editor.getStructuredTextEditor(); 530 if (structuredTextEditor != null) { 531 IWorkbenchWindow window = editor.getSite().getWorkbenchWindow(); 532 ISelectionService service = window.getSelectionService(); 533 service.addPostSelectionListener(new ISelectionListener() { 534 @Override 535 public void selectionChanged(IWorkbenchPart part, ISelection selection) { 536 if (getEditor().isEditorPageActive()) { 537 mCheckOutlineAdapter = true; 538 } 539 } 540 }); 541 542 mEditorOutline = (IContentOutlinePage) structuredTextEditor.getAdapter( 543 IContentOutlinePage.class); 544 } 545 } 546 547 outline = mEditorOutline; 548 } 549 550 mMultiOutline.setPageActive(outline); 551 } 552 553 /** 554 * Returns the graphical outline associated with the layout editor 555 * 556 * @return the outline page, never null 557 */ 558 @NonNull 559 public OutlinePage getGraphicalOutline() { 560 if (mLayoutOutline == null) { 561 mLayoutOutline = new OutlinePage(mGraphicalEditor); 562 } 563 564 return mLayoutOutline; 565 } 566 567 @Override 568 public void delegatePageChange(int newPageIndex) { 569 if (getEditor().getCurrentPage() == getEditor().getTextPageIndex() && 570 newPageIndex == mGraphicalEditorIndex) { 571 // You're switching from the XML editor to the WYSIWYG editor; 572 // look at the caret position and figure out which node it corresponds to 573 // (if any) and if found, select the corresponding visual element. 574 ISourceViewer textViewer = getEditor().getStructuredSourceViewer(); 575 int caretOffset = textViewer.getTextWidget().getCaretOffset(); 576 if (caretOffset >= 0) { 577 Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset); 578 if (node != null && mGraphicalEditor != null) { 579 mGraphicalEditor.select(node); 580 } 581 } 582 } 583 584 super.delegatePageChange(newPageIndex); 585 586 if (mGraphicalEditor != null) { 587 if (newPageIndex == mGraphicalEditorIndex) { 588 mGraphicalEditor.activated(); 589 } else { 590 mGraphicalEditor.deactivated(); 591 } 592 } 593 } 594 595 @Override 596 public void delegatePostPageChange(int newPageIndex) { 597 super.delegatePostPageChange(newPageIndex); 598 599 IFormPage page = getEditor().getActivePageInstance(); 600 updateOutline(page); 601 } 602 603 @Override 604 public IFormPage delegatePostSetActivePage(IFormPage superReturned, String pageIndex) { 605 IFormPage page = superReturned; 606 if (page != null) { 607 updateOutline(page); 608 } 609 610 return page; 611 } 612 613 // ----- IActionContributorDelegate methods ---- 614 615 @Override 616 public void setActiveEditor(IEditorPart part, IActionBars bars) { 617 if (mGraphicalEditor != null) { 618 LayoutCanvas canvas = mGraphicalEditor.getCanvasControl(); 619 if (canvas != null) { 620 canvas.updateGlobalActions(bars); 621 } 622 } 623 } 624 625 626 // ----- IPartListener Methods ---- 627 628 @Override 629 public void partActivated(IWorkbenchPart part) { 630 if (part == getEditor()) { 631 if (mGraphicalEditor != null) { 632 if (getEditor().getActivePage() == mGraphicalEditorIndex) { 633 mGraphicalEditor.activated(); 634 } else { 635 mGraphicalEditor.deactivated(); 636 } 637 } 638 } 639 } 640 641 @Override 642 public void partBroughtToTop(IWorkbenchPart part) { 643 partActivated(part); 644 } 645 646 @Override 647 public void partClosed(IWorkbenchPart part) { 648 // pass 649 } 650 651 @Override 652 public void partDeactivated(IWorkbenchPart part) { 653 if (part == getEditor()) { 654 if (mGraphicalEditor != null && getEditor().getActivePage() == mGraphicalEditorIndex) { 655 mGraphicalEditor.deactivated(); 656 } 657 } 658 } 659 660 @Override 661 public void partOpened(IWorkbenchPart part) { 662 /* 663 * We used to automatically bring the outline and the property sheet to view 664 * when opening the editor. This behavior has always been a mixed bag and not 665 * exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to 666 * improve on that, so right now let's comment this out. 667 */ 668 //EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */); 669 //EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */); 670 } 671 672 // ---- Local Methods ---- 673 674 /** 675 * Returns true if the Graphics editor page is visible. This <b>must</b> be 676 * called from the UI thread. 677 */ 678 public boolean isGraphicalEditorActive() { 679 IWorkbenchPartSite workbenchSite = getEditor().getSite(); 680 IWorkbenchPage workbenchPage = workbenchSite.getPage(); 681 682 // check if the editor is visible in the workbench page 683 if (workbenchPage.isPartVisible(getEditor()) 684 && workbenchPage.getActiveEditor() == getEditor()) { 685 // and then if the page of the editor is visible (not to be confused with 686 // the workbench page) 687 return mGraphicalEditorIndex == getEditor().getActivePage(); 688 } 689 690 return false; 691 } 692 693 @Override 694 public void delegateInitUiRootNode(boolean force) { 695 // The root UI node is always created, even if there's no corresponding XML node. 696 if (mUiDocRootNode == null || force) { 697 // get the target data from the opened file (and its project) 698 AndroidTargetData data = getEditor().getTargetData(); 699 700 Document doc = null; 701 if (mUiDocRootNode != null) { 702 doc = mUiDocRootNode.getXmlDocument(); 703 } 704 705 DocumentDescriptor desc; 706 if (data == null) { 707 desc = new DocumentDescriptor("temp", null /*children*/); 708 } else { 709 desc = data.getLayoutDescriptors().getDescriptor(); 710 } 711 712 // get the descriptors from the data. 713 mUiDocRootNode = (UiDocumentNode) desc.createUiNode(); 714 super.setUiRootNode(mUiDocRootNode); 715 mUiDocRootNode.setEditor(getEditor()); 716 717 mUiDocRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() { 718 @Override 719 public ElementDescriptor getDescriptor(String xmlLocalName) { 720 ElementDescriptor unknown = mUnknownDescriptorMap.get(xmlLocalName); 721 if (unknown == null) { 722 unknown = createUnknownDescriptor(xmlLocalName); 723 mUnknownDescriptorMap.put(xmlLocalName, unknown); 724 } 725 726 return unknown; 727 } 728 }); 729 730 onDescriptorsChanged(doc); 731 } 732 } 733 734 /** 735 * Creates a new {@link ViewElementDescriptor} for an unknown XML local name 736 * (i.e. one that was not mapped by the current descriptors). 737 * <p/> 738 * Since we deal with layouts, we returns either a descriptor for a custom view 739 * or one for the base View. 740 * 741 * @param xmlLocalName The XML local name to match. 742 * @return A non-null {@link ViewElementDescriptor}. 743 */ 744 private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) { 745 ViewElementDescriptor desc = null; 746 IEditorInput editorInput = getEditor().getEditorInput(); 747 if (editorInput instanceof IFileEditorInput) { 748 IFileEditorInput fileInput = (IFileEditorInput)editorInput; 749 IProject project = fileInput.getFile().getProject(); 750 751 // Check if we can find a custom view specific to this project. 752 // This only works if there's an actual matching custom class in the project. 753 desc = CustomViewDescriptorService.getInstance().getDescriptor(project, xmlLocalName); 754 755 if (desc == null) { 756 // If we didn't find a custom view, create a synthetic one using the 757 // the base View descriptor as a model. 758 // This is a layout after all, so every XML node should represent 759 // a view. 760 761 Sdk currentSdk = Sdk.getCurrent(); 762 if (currentSdk != null) { 763 IAndroidTarget target = currentSdk.getTarget(project); 764 if (target != null) { 765 AndroidTargetData data = currentSdk.getTargetData(target); 766 if (data != null) { 767 // data can be null when the target is still loading 768 ViewElementDescriptor viewDesc = 769 data.getLayoutDescriptors().getBaseViewDescriptor(); 770 771 desc = new ViewElementDescriptor( 772 xmlLocalName, // xml local name 773 xmlLocalName, // ui_name 774 xmlLocalName, // canonical class name 775 null, // tooltip 776 null, // sdk_url 777 viewDesc.getAttributes(), 778 viewDesc.getLayoutAttributes(), 779 null, // children 780 false /* mandatory */); 781 desc.setSuperClass(viewDesc); 782 } 783 } 784 } 785 } 786 } 787 788 if (desc == null) { 789 // We can only arrive here if the SDK's android target has not finished 790 // loading. Just create a dummy descriptor with no attributes to be able 791 // to continue. 792 desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName); 793 } 794 return desc; 795 } 796 797 private void onDescriptorsChanged(Document document) { 798 799 mUnknownDescriptorMap.clear(); 800 801 if (document != null) { 802 mUiDocRootNode.loadFromXmlNode(document); 803 } else { 804 mUiDocRootNode.reloadFromXmlNode(mUiDocRootNode.getXmlDocument()); 805 } 806 807 if (mGraphicalEditor != null) { 808 mGraphicalEditor.onTargetChange(); 809 mGraphicalEditor.reloadPalette(); 810 } 811 } 812 813 /** 814 * Handles a new input, and update the part name. 815 * @param input the new input. 816 */ 817 private void handleNewInput(IEditorInput input) { 818 if (input instanceof FileEditorInput) { 819 FileEditorInput fileInput = (FileEditorInput) input; 820 IFile file = fileInput.getFile(); 821 getEditor().setPartName(String.format("%1$s", file.getName())); 822 } 823 } 824 825 /** 826 * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN. 827 * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info. 828 */ 829 public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) { 830 ViewElementDescriptor desc = null; 831 832 AndroidTargetData data = getEditor().getTargetData(); 833 if (data != null) { 834 LayoutDescriptors layoutDesc = data.getLayoutDescriptors(); 835 if (layoutDesc != null) { 836 DocumentDescriptor docDesc = layoutDesc.getDescriptor(); 837 if (docDesc != null) { 838 desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null); 839 } 840 } 841 } 842 843 if (desc == null) { 844 // We failed to find a descriptor for the given FQCN. 845 // Let's consider custom classes and create one as needed. 846 desc = createUnknownDescriptor(fqcn); 847 } 848 849 return desc; 850 } 851 852 /** 853 * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches 854 * the requested FQCN. 855 * 856 * @param fqcn The target View FQCN to find. 857 * @param descriptors A list of children descriptors to iterate through. 858 * @param visited A set we use to remember which descriptors have already been visited, 859 * necessary since the view descriptor hierarchy is cyclic. 860 * @return Either a matching {@link ViewElementDescriptor} or null. 861 */ 862 private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn, 863 ElementDescriptor[] descriptors, 864 Set<ElementDescriptor> visited) { 865 if (visited == null) { 866 visited = new HashSet<ElementDescriptor>(); 867 } 868 869 if (descriptors != null) { 870 for (ElementDescriptor desc : descriptors) { 871 if (visited.add(desc)) { 872 // Set.add() returns true if this a new element that was added to the set. 873 // That means we haven't visited this descriptor yet. 874 // We want a ViewElementDescriptor with a matching FQCN. 875 if (desc instanceof ViewElementDescriptor && 876 fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) { 877 return (ViewElementDescriptor) desc; 878 } 879 880 // Visit its children 881 ViewElementDescriptor vd = 882 internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited); 883 if (vd != null) { 884 return vd; 885 } 886 } 887 } 888 } 889 890 return null; 891 } 892 } 893