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