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