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.ide.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 24 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider; 25 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; 26 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 27 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 28 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 29 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage; 31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage; 32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 34 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 35 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 36 import com.android.sdklib.IAndroidTarget; 37 38 import org.eclipse.core.resources.IFile; 39 import org.eclipse.core.resources.IProject; 40 import org.eclipse.core.runtime.IProgressMonitor; 41 import org.eclipse.core.runtime.IStatus; 42 import org.eclipse.core.runtime.NullProgressMonitor; 43 import org.eclipse.jface.text.source.ISourceViewer; 44 import org.eclipse.ui.IEditorInput; 45 import org.eclipse.ui.IEditorPart; 46 import org.eclipse.ui.IFileEditorInput; 47 import org.eclipse.ui.IPartListener; 48 import org.eclipse.ui.IShowEditorInput; 49 import org.eclipse.ui.IWorkbenchPage; 50 import org.eclipse.ui.IWorkbenchPart; 51 import org.eclipse.ui.IWorkbenchPartSite; 52 import org.eclipse.ui.PartInitException; 53 import org.eclipse.ui.part.FileEditorInput; 54 import org.eclipse.ui.views.contentoutline.IContentOutlinePage; 55 import org.eclipse.ui.views.properties.IPropertySheetPage; 56 import org.w3c.dom.Document; 57 import org.w3c.dom.Node; 58 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.Set; 62 63 /** 64 * Multi-page form editor for /res/layout XML files. 65 */ 66 public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, IPartListener { 67 68 public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$ 69 70 /** Root node of the UI element hierarchy */ 71 private UiDocumentNode mUiRootNode; 72 73 private GraphicalEditorPart mGraphicalEditor; 74 private int mGraphicalEditorIndex; 75 /** Implementation of the {@link IContentOutlinePage} for this editor */ 76 private IContentOutlinePage mOutline; 77 /** Custom implementation of {@link IPropertySheetPage} for this editor */ 78 private IPropertySheetPage mPropertyPage; 79 80 private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap = 81 new HashMap<String, ElementDescriptor>(); 82 83 84 /** 85 * Flag indicating if the replacement file is due to a config change. 86 * If false, it means the new file is due to an "open action" from the user. 87 */ 88 private boolean mNewFileOnConfigChange = false; 89 90 /** 91 * Creates the form editor for resources XML files. 92 */ 93 public LayoutEditor() { 94 super(false /* addTargetListener */); 95 } 96 97 /** 98 * Returns the {@link RulesEngine} associated with this editor 99 * 100 * @return the {@link RulesEngine} associated with this editor. 101 */ 102 public RulesEngine getRulesEngine() { 103 return mGraphicalEditor.getRulesEngine(); 104 } 105 106 /** 107 * Returns the {@link GraphicalEditorPart} associated with this editor 108 * 109 * @return the {@link GraphicalEditorPart} associated with this editor 110 */ 111 public GraphicalEditorPart getGraphicalEditor() { 112 return mGraphicalEditor; 113 } 114 115 /** 116 * @return The root node of the UI element hierarchy 117 */ 118 @Override 119 public UiDocumentNode getUiRootNode() { 120 return mUiRootNode; 121 } 122 123 public void setNewFileOnConfigChange(boolean state) { 124 mNewFileOnConfigChange = state; 125 } 126 127 // ---- Base Class Overrides ---- 128 129 @Override 130 public void dispose() { 131 getSite().getPage().removePartListener(this); 132 133 super.dispose(); 134 } 135 136 /** 137 * Save the XML. 138 * <p/> 139 * The actual save operation is done in the super class by committing 140 * all data to the XML model and then having the Structured XML Editor 141 * save the XML. 142 * <p/> 143 * Here we just need to tell the graphical editor that the model has 144 * been saved. 145 */ 146 @Override 147 public void doSave(IProgressMonitor monitor) { 148 super.doSave(monitor); 149 if (mGraphicalEditor != null) { 150 mGraphicalEditor.doSave(monitor); 151 } 152 } 153 154 /** 155 * Returns whether the "save as" operation is supported by this editor. 156 * <p/> 157 * Save-As is a valid operation for the ManifestEditor since it acts on a 158 * single source file. 159 * 160 * @see IEditorPart 161 */ 162 @Override 163 public boolean isSaveAsAllowed() { 164 return true; 165 } 166 167 /** 168 * Create the various form pages. 169 */ 170 @Override 171 protected void createFormPages() { 172 try { 173 // get the file being edited so that it can be passed to the layout editor. 174 IFile editedFile = null; 175 IEditorInput input = getEditorInput(); 176 if (input instanceof FileEditorInput) { 177 FileEditorInput fileInput = (FileEditorInput)input; 178 editedFile = fileInput.getFile(); 179 } else { 180 AdtPlugin.log(IStatus.ERROR, 181 "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$ 182 input.toString()); 183 } 184 185 // It is possible that the Layout Editor already exits if a different version 186 // of the same layout is being opened (either through "open" action from 187 // the user, or through a configuration change in the configuration selector.) 188 if (mGraphicalEditor == null) { 189 190 // Instantiate GLE v2 191 mGraphicalEditor = new GraphicalEditorPart(this); 192 193 mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput()); 194 setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle()); 195 196 mGraphicalEditor.openFile(editedFile); 197 } else { 198 if (mNewFileOnConfigChange) { 199 mGraphicalEditor.changeFileOnNewConfig(editedFile); 200 mNewFileOnConfigChange = false; 201 } else { 202 mGraphicalEditor.replaceFile(editedFile); 203 } 204 } 205 206 // put in place the listener to handle layout recompute only when needed. 207 getSite().getPage().addPartListener(this); 208 } catch (PartInitException e) { 209 AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ 210 } 211 } 212 213 @Override 214 protected void postCreatePages() { 215 super.postCreatePages(); 216 217 // Optional: set the default page. Eventually a default page might be 218 // restored by selectDefaultPage() later based on the last page used by the user. 219 // For example, to make the last page the default one (rather than the first page), 220 // uncomment this line: 221 // setActivePage(getPageCount() - 1); 222 } 223 224 /* (non-java doc) 225 * Change the tab/title name to include the name of the layout. 226 */ 227 @Override 228 protected void setInput(IEditorInput input) { 229 super.setInput(input); 230 handleNewInput(input); 231 } 232 233 /* 234 * (non-Javadoc) 235 * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput) 236 */ 237 @Override 238 protected void setInputWithNotify(IEditorInput input) { 239 super.setInputWithNotify(input); 240 handleNewInput(input); 241 } 242 243 /** 244 * Called to replace the current {@link IEditorInput} with another one. 245 * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're 246 * opening a different configuration of the same layout. 247 */ 248 public void showEditorInput(IEditorInput editorInput) { 249 if (getEditorInput().equals(editorInput)) { 250 return; 251 } 252 253 // save the current editor input. 254 doSave(new NullProgressMonitor()); 255 256 // get the current page 257 int currentPage = getActivePage(); 258 259 // remove the pages, except for the graphical editor, which will be dynamically adapted 260 // to the new model. 261 // page after the graphical editor: 262 int count = getPageCount(); 263 for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) { 264 removePage(i); 265 } 266 // pages before the graphical editor 267 for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) { 268 removePage(i); 269 } 270 271 // set the current input. 272 setInputWithNotify(editorInput); 273 274 // re-create or reload the pages with the default page shown as the previous active page. 275 createAndroidPages(); 276 selectDefaultPage(Integer.toString(currentPage)); 277 278 // When changing an input file of an the editor, the titlebar is not refreshed to 279 // show the new path/to/file being edited. So we force a refresh 280 firePropertyChange(IWorkbenchPart.PROP_TITLE); 281 } 282 283 /** Performs a complete refresh of the XML model */ 284 public void refreshXmlModel() { 285 Document xmlDoc = mUiRootNode.getXmlDocument(); 286 287 initUiRootNode(true /*force*/); 288 mUiRootNode.loadFromXmlNode(xmlDoc); 289 // update the model first, since it is used by the viewers. 290 super.xmlModelChanged(xmlDoc); 291 292 if (mGraphicalEditor != null) { 293 mGraphicalEditor.onXmlModelChanged(); 294 } 295 } 296 297 /** 298 * Processes the new XML Model, which XML root node is given. 299 * 300 * @param xml_doc The XML document, if available, or null if none exists. 301 */ 302 @Override 303 protected void xmlModelChanged(Document xml_doc) { 304 if (mIgnoreXmlUpdate) { 305 return; 306 } 307 308 // init the ui root on demand 309 initUiRootNode(false /*force*/); 310 311 mUiRootNode.loadFromXmlNode(xml_doc); 312 313 // update the model first, since it is used by the viewers. 314 super.xmlModelChanged(xml_doc); 315 316 if (mGraphicalEditor != null) { 317 mGraphicalEditor.onXmlModelChanged(); 318 } 319 } 320 321 /** 322 * Tells the graphical editor to recompute its layout. 323 */ 324 public void recomputeLayout() { 325 mGraphicalEditor.recomputeLayout(); 326 } 327 328 @Override 329 public boolean supportsFormatOnGuiEdit() { 330 return true; 331 } 332 333 /** 334 * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. 335 */ 336 @SuppressWarnings("unchecked") 337 @Override 338 public Object getAdapter(Class adapter) { 339 // For the outline, force it to come from the Graphical Editor. 340 // This fixes the case where a layout file is opened in XML view first and the outline 341 // gets stuck in the XML outline. 342 if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) { 343 344 if (mOutline == null && mGraphicalEditor != null) { 345 mOutline = new OutlinePage(mGraphicalEditor); 346 } 347 348 return mOutline; 349 } 350 351 if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) { 352 if (mPropertyPage == null) { 353 mPropertyPage = new PropertySheetPage(); 354 } 355 356 return mPropertyPage; 357 } 358 359 // return default 360 return super.getAdapter(adapter); 361 } 362 363 @Override 364 protected void pageChange(int newPageIndex) { 365 if (getCurrentPage() == mTextPageIndex && 366 newPageIndex == mGraphicalEditorIndex) { 367 // You're switching from the XML editor to the WYSIWYG editor; 368 // look at the caret position and figure out which node it corresponds to 369 // (if any) and if found, select the corresponding visual element. 370 ISourceViewer textViewer = getStructuredSourceViewer(); 371 int caretOffset = textViewer.getTextWidget().getCaretOffset(); 372 if (caretOffset >= 0) { 373 Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset); 374 if (node != null && mGraphicalEditor != null) { 375 mGraphicalEditor.select(node); 376 } 377 } 378 } 379 380 super.pageChange(newPageIndex); 381 382 if (mGraphicalEditor != null) { 383 if (newPageIndex == mGraphicalEditorIndex) { 384 mGraphicalEditor.activated(); 385 } else { 386 mGraphicalEditor.deactivated(); 387 } 388 } 389 } 390 391 // ----- IPartListener Methods ---- 392 393 public void partActivated(IWorkbenchPart part) { 394 if (part == this) { 395 if (mGraphicalEditor != null) { 396 if (getActivePage() == mGraphicalEditorIndex) { 397 mGraphicalEditor.activated(); 398 } else { 399 mGraphicalEditor.deactivated(); 400 } 401 } 402 } 403 } 404 405 public void partBroughtToTop(IWorkbenchPart part) { 406 partActivated(part); 407 } 408 409 public void partClosed(IWorkbenchPart part) { 410 // pass 411 } 412 413 public void partDeactivated(IWorkbenchPart part) { 414 if (part == this) { 415 if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) { 416 mGraphicalEditor.deactivated(); 417 } 418 } 419 } 420 421 public void partOpened(IWorkbenchPart part) { 422 /* 423 * We used to automatically bring the outline and the property sheet to view 424 * when opening the editor. This behavior has always been a mixed bag and not 425 * exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to 426 * improve on that, so right now let's comment this out. 427 */ 428 //EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */); 429 //EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */); 430 } 431 432 // ---- Local Methods ---- 433 434 /** 435 * Returns true if the Graphics editor page is visible. This <b>must</b> be 436 * called from the UI thread. 437 */ 438 public boolean isGraphicalEditorActive() { 439 IWorkbenchPartSite workbenchSite = getSite(); 440 IWorkbenchPage workbenchPage = workbenchSite.getPage(); 441 442 // check if the editor is visible in the workbench page 443 if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) { 444 // and then if the page of the editor is visible (not to be confused with 445 // the workbench page) 446 return mGraphicalEditorIndex == getActivePage(); 447 } 448 449 return false; 450 } 451 452 @Override 453 public void initUiRootNode(boolean force) { 454 // The root UI node is always created, even if there's no corresponding XML node. 455 if (mUiRootNode == null || force) { 456 // get the target data from the opened file (and its project) 457 AndroidTargetData data = getTargetData(); 458 459 Document doc = null; 460 if (mUiRootNode != null) { 461 doc = mUiRootNode.getXmlDocument(); 462 } 463 464 DocumentDescriptor desc; 465 if (data == null) { 466 desc = new DocumentDescriptor("temp", null /*children*/); 467 } else { 468 desc = data.getLayoutDescriptors().getDescriptor(); 469 } 470 471 // get the descriptors from the data. 472 mUiRootNode = (UiDocumentNode) desc.createUiNode(); 473 mUiRootNode.setEditor(this); 474 475 mUiRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() { 476 477 public ElementDescriptor getDescriptor(String xmlLocalName) { 478 479 ElementDescriptor desc = mUnknownDescriptorMap.get(xmlLocalName); 480 481 if (desc == null) { 482 desc = createUnknownDescriptor(xmlLocalName); 483 mUnknownDescriptorMap.put(xmlLocalName, desc); 484 } 485 486 return desc; 487 } 488 }); 489 490 onDescriptorsChanged(doc); 491 } 492 } 493 494 /** 495 * Creates a new {@link ViewElementDescriptor} for an unknown XML local name 496 * (i.e. one that was not mapped by the current descriptors). 497 * <p/> 498 * Since we deal with layouts, we returns either a descriptor for a custom view 499 * or one for the base View. 500 * 501 * @param xmlLocalName The XML local name to match. 502 * @return A non-null {@link ViewElementDescriptor}. 503 */ 504 private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) { 505 ViewElementDescriptor desc = null; 506 IEditorInput editorInput = getEditorInput(); 507 if (editorInput instanceof IFileEditorInput) { 508 IFileEditorInput fileInput = (IFileEditorInput)editorInput; 509 IProject project = fileInput.getFile().getProject(); 510 511 // Check if we can find a custom view specific to this project. 512 // This only works if there's an actual matching custom class in the project. 513 desc = CustomViewDescriptorService.getInstance().getDescriptor(project, xmlLocalName); 514 515 if (desc == null) { 516 // If we didn't find a custom view, create a synthetic one using the 517 // the base View descriptor as a model. 518 // This is a layout after all, so every XML node should represent 519 // a view. 520 521 Sdk currentSdk = Sdk.getCurrent(); 522 if (currentSdk != null) { 523 IAndroidTarget target = currentSdk.getTarget(project); 524 if (target != null) { 525 AndroidTargetData data = currentSdk.getTargetData(target); 526 if (data != null) { 527 // data can be null when the target is still loading 528 ViewElementDescriptor viewDesc = 529 data.getLayoutDescriptors().getBaseViewDescriptor(); 530 531 desc = new ViewElementDescriptor( 532 xmlLocalName, // xml local name 533 xmlLocalName, // ui_name 534 xmlLocalName, // canonical class name 535 null, // tooltip 536 null, // sdk_url 537 viewDesc.getAttributes(), 538 viewDesc.getLayoutAttributes(), 539 null, // children 540 false /* mandatory */); 541 desc.setSuperClass(viewDesc); 542 } 543 } 544 } 545 } 546 } 547 548 if (desc == null) { 549 // We can only arrive here if the SDK's android target has not finished 550 // loading. Just create a dummy descriptor with no attributes to be able 551 // to continue. 552 desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName); 553 } 554 return desc; 555 } 556 557 private void onDescriptorsChanged(Document document) { 558 559 mUnknownDescriptorMap.clear(); 560 561 if (document != null) { 562 mUiRootNode.loadFromXmlNode(document); 563 } else { 564 mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument()); 565 } 566 567 if (mGraphicalEditor != null) { 568 mGraphicalEditor.onTargetChange(); 569 mGraphicalEditor.reloadPalette(); 570 } 571 } 572 573 /** 574 * Handles a new input, and update the part name. 575 * @param input the new input. 576 */ 577 private void handleNewInput(IEditorInput input) { 578 if (input instanceof FileEditorInput) { 579 FileEditorInput fileInput = (FileEditorInput) input; 580 IFile file = fileInput.getFile(); 581 setPartName(String.format("%1$s", 582 file.getName())); 583 } 584 } 585 586 /** 587 * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN. 588 * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info. 589 */ 590 public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) { 591 ViewElementDescriptor desc = null; 592 593 AndroidTargetData data = getTargetData(); 594 if (data != null) { 595 LayoutDescriptors layoutDesc = data.getLayoutDescriptors(); 596 if (layoutDesc != null) { 597 DocumentDescriptor docDesc = layoutDesc.getDescriptor(); 598 if (docDesc != null) { 599 desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null); 600 } 601 } 602 } 603 604 if (desc == null) { 605 // We failed to find a descriptor for the given FQCN. 606 // Let's consider custom classes and create one as needed. 607 desc = createUnknownDescriptor(fqcn); 608 } 609 610 return desc; 611 } 612 613 /** 614 * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches 615 * the requested FQCN. 616 * 617 * @param fqcn The target View FQCN to find. 618 * @param descriptors A list of children descriptors to iterate through. 619 * @param visited A set we use to remember which descriptors have already been visited, 620 * necessary since the view descriptor hierarchy is cyclic. 621 * @return Either a matching {@link ViewElementDescriptor} or null. 622 */ 623 private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn, 624 ElementDescriptor[] descriptors, 625 Set<ElementDescriptor> visited) { 626 if (visited == null) { 627 visited = new HashSet<ElementDescriptor>(); 628 } 629 630 if (descriptors != null) { 631 for (ElementDescriptor desc : descriptors) { 632 if (visited.add(desc)) { 633 // Set.add() returns true if this a new element that was added to the set. 634 // That means we haven't visited this descriptor yet. 635 // We want a ViewElementDescriptor with a matching FQCN. 636 if (desc instanceof ViewElementDescriptor && 637 fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) { 638 return (ViewElementDescriptor) desc; 639 } 640 641 // Visit its children 642 ViewElementDescriptor vd = 643 internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited); 644 if (vd != null) { 645 return vd; 646 } 647 } 648 } 649 } 650 651 return null; 652 } 653 } 654