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.AdtPlugin; 20 import com.android.ide.eclipse.adt.AndroidConstants; 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.ViewElementDescriptor; 27 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.GraphicalLayoutEditor; 28 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.UiContentOutlinePage; 29 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.UiPropertySheetPage; 30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage2; 31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 32 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage2; 33 import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiActions; 34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 35 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 37 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 38 import com.android.sdklib.IAndroidTarget; 39 40 import org.eclipse.core.resources.IFile; 41 import org.eclipse.core.resources.IProject; 42 import org.eclipse.core.runtime.IProgressMonitor; 43 import org.eclipse.core.runtime.IStatus; 44 import org.eclipse.core.runtime.NullProgressMonitor; 45 import org.eclipse.gef.ui.parts.TreeViewer; 46 import org.eclipse.ui.IEditorInput; 47 import org.eclipse.ui.IEditorPart; 48 import org.eclipse.ui.IFileEditorInput; 49 import org.eclipse.ui.IPartListener; 50 import org.eclipse.ui.IShowEditorInput; 51 import org.eclipse.ui.IWorkbenchPage; 52 import org.eclipse.ui.IWorkbenchPart; 53 import org.eclipse.ui.IWorkbenchPartSite; 54 import org.eclipse.ui.PartInitException; 55 import org.eclipse.ui.part.FileEditorInput; 56 import org.eclipse.ui.views.contentoutline.IContentOutlinePage; 57 import org.eclipse.ui.views.properties.IPropertySheetPage; 58 import org.w3c.dom.Document; 59 60 import java.util.HashMap; 61 62 /** 63 * Multi-page form editor for /res/layout XML files. 64 */ 65 public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, IPartListener { 66 67 public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$ 68 69 /** Root node of the UI element hierarchy */ 70 private UiDocumentNode mUiRootNode; 71 72 private IGraphicalLayoutEditor mGraphicalEditor; 73 private int mGraphicalEditorIndex; 74 /** 75 * Implementation of the {@link IContentOutlinePage} for this editor. 76 * @deprecated Used for backward compatibility with GLE1. 77 */ 78 private UiContentOutlinePage mOutlineForGle1; 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 UiEditorActions mUiEditorActions; 85 86 private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap = 87 new HashMap<String, ElementDescriptor>(); 88 89 90 /** 91 * Flag indicating if the replacement file is due to a config change. 92 * If false, it means the new file is due to an "open action" from the user. 93 */ 94 private boolean mNewFileOnConfigChange = false; 95 96 /** 97 * Creates the form editor for resources XML files. 98 */ 99 public LayoutEditor() { 100 super(false /* addTargetListener */); 101 } 102 103 /** 104 * @return The root node of the UI element hierarchy 105 */ 106 @Override 107 public UiDocumentNode getUiRootNode() { 108 return mUiRootNode; 109 } 110 111 // ---- Base Class Overrides ---- 112 113 @Override 114 public void dispose() { 115 getSite().getPage().removePartListener(this); 116 117 super.dispose(); 118 } 119 120 /** 121 * Save the XML. 122 * <p/> 123 * The actual save operation is done in the super class by committing 124 * all data to the XML model and then having the Structured XML Editor 125 * save the XML. 126 * <p/> 127 * Here we just need to tell the graphical editor that the model has 128 * been saved. 129 */ 130 @Override 131 public void doSave(IProgressMonitor monitor) { 132 super.doSave(monitor); 133 if (mGraphicalEditor != null) { 134 mGraphicalEditor.doSave(monitor); 135 } 136 } 137 138 /** 139 * Returns whether the "save as" operation is supported by this editor. 140 * <p/> 141 * Save-As is a valid operation for the ManifestEditor since it acts on a 142 * single source file. 143 * 144 * @see IEditorPart 145 */ 146 @Override 147 public boolean isSaveAsAllowed() { 148 return true; 149 } 150 151 /** 152 * Create the various form pages. 153 */ 154 @Override 155 protected void createFormPages() { 156 try { 157 // The graphical layout editor is now enabled by default. 158 // In case there's an issue we provide a way to disable it using an 159 // env variable. 160 if (System.getenv("ANDROID_DISABLE_LAYOUT") == null) { //$NON-NLS-1$ 161 // get the file being edited so that it can be passed to the layout editor. 162 IFile editedFile = null; 163 IEditorInput input = getEditorInput(); 164 if (input instanceof FileEditorInput) { 165 FileEditorInput fileInput = (FileEditorInput)input; 166 editedFile = fileInput.getFile(); 167 } else { 168 AdtPlugin.log(IStatus.ERROR, 169 "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$ 170 input.toString()); 171 } 172 173 // It is possible that the Layout Editor already exits if a different version 174 // of the same layout is being opened (either through "open" action from 175 // the user, or through a configuration change in the configuration selector.) 176 if (mGraphicalEditor == null) { 177 178 String useGle2 = System.getenv("USE_GLE2"); //$NON-NLS-1$ 179 180 if (useGle2 != null && !useGle2.equals("0")) { //$NON-NLS-1$ 181 mGraphicalEditor = new GraphicalEditorPart(this); 182 } else { 183 mGraphicalEditor = new GraphicalLayoutEditor(this); 184 } 185 186 mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput()); 187 setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle()); 188 189 mGraphicalEditor.openFile(editedFile); 190 } else { 191 if (mNewFileOnConfigChange) { 192 mGraphicalEditor.changeFileOnNewConfig(editedFile); 193 mNewFileOnConfigChange = false; 194 } else { 195 mGraphicalEditor.replaceFile(editedFile); 196 } 197 } 198 199 // put in place the listener to handle layout recompute only when needed. 200 getSite().getPage().addPartListener(this); 201 } 202 } catch (PartInitException e) { 203 AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ 204 } 205 } 206 207 @Override 208 protected void postCreatePages() { 209 super.postCreatePages(); 210 211 // This is called after the createFormPages() and createTextPage() methods have 212 // been called. Usually we select the first page (e.g. the GLE here) but right 213 // now we're going to temporarily select the last page (the XML text editor) if 214 // GLE1 is being used. That's because GLE1 is mostly useless and being deprecated. 215 // 216 // Note that this sets the default page. Eventually a default page might be 217 // restored by selectDefaultPage() later based on the last page used by the user. 218 // 219 // TODO revert this once GLE2 becomes useful and is the default. 220 221 if (mGraphicalEditor instanceof GraphicalLayoutEditor) { 222 setActivePage(getPageCount() - 1); 223 } 224 } 225 226 /* (non-java doc) 227 * Change the tab/title name to include the name of the layout. 228 */ 229 @Override 230 protected void setInput(IEditorInput input) { 231 super.setInput(input); 232 handleNewInput(input); 233 } 234 235 /* 236 * (non-Javadoc) 237 * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput) 238 */ 239 @Override 240 protected void setInputWithNotify(IEditorInput input) { 241 super.setInputWithNotify(input); 242 handleNewInput(input); 243 } 244 245 /** 246 * Called to replace the current {@link IEditorInput} with another one. 247 * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're 248 * opening a different configuration of the same layout. 249 */ 250 public void showEditorInput(IEditorInput editorInput) { 251 // save the current editor input. 252 doSave(new NullProgressMonitor()); 253 254 // get the current page 255 int currentPage = getActivePage(); 256 257 // remove the pages, except for the graphical editor, which will be dynamically adapted 258 // to the new model. 259 // page after the graphical editor: 260 int count = getPageCount(); 261 for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) { 262 removePage(i); 263 } 264 // pages before the graphical editor 265 for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) { 266 removePage(i); 267 } 268 269 // set the current input. 270 setInputWithNotify(editorInput); 271 272 // re-create or reload the pages with the default page shown as the previous active page. 273 createAndroidPages(); 274 selectDefaultPage(Integer.toString(currentPage)); 275 276 // update the GLE1 outline. The GLE2 outline doesn't need this call anymore. 277 if (mOutlineForGle1 != null) { 278 mOutlineForGle1.reloadModel(); 279 } 280 } 281 282 /** 283 * Processes the new XML Model, which XML root node is given. 284 * 285 * @param xml_doc The XML document, if available, or null if none exists. 286 */ 287 @Override 288 protected void xmlModelChanged(Document xml_doc) { 289 // init the ui root on demand 290 initUiRootNode(false /*force*/); 291 292 mUiRootNode.loadFromXmlNode(xml_doc); 293 294 // update the model first, since it is used by the viewers. 295 super.xmlModelChanged(xml_doc); 296 297 if (mGraphicalEditor != null) { 298 mGraphicalEditor.onXmlModelChanged(); 299 } 300 301 // update the GLE1 outline. The GLE2 outline doesn't need this call anymore. 302 if (mOutlineForGle1 != null) { 303 mOutlineForGle1.reloadModel(); 304 } 305 } 306 307 /** 308 * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. 309 */ 310 @SuppressWarnings("unchecked") 311 @Override 312 public Object getAdapter(Class adapter) { 313 // for the outline, force it to come from the Graphical Editor. 314 // This fixes the case where a layout file is opened in XML view first and the outline 315 // gets stuck in the XML outline. 316 if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) { 317 318 if (mOutline == null && mGraphicalEditor instanceof GraphicalLayoutEditor) { 319 // Create the GLE1 outline. We need to keep a specific reference to it in order 320 // to call its reloadModel() method. The GLE2 outline no longer relies on this 321 // and can be casted to the base interface. 322 mOutlineForGle1 = new UiContentOutlinePage( 323 (GraphicalLayoutEditor) mGraphicalEditor, 324 new TreeViewer()); 325 mOutline = mOutlineForGle1; 326 327 } else if (mOutline == null && mGraphicalEditor instanceof GraphicalEditorPart) { 328 mOutline = new OutlinePage2(); 329 } 330 331 return mOutline; 332 } 333 334 if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) { 335 if (mPropertyPage == null && mGraphicalEditor instanceof GraphicalLayoutEditor) { 336 mPropertyPage = new UiPropertySheetPage(); 337 338 } else if (mPropertyPage == null && mGraphicalEditor instanceof GraphicalEditorPart) { 339 mPropertyPage = new PropertySheetPage2(); 340 } 341 342 return mPropertyPage; 343 } 344 345 // return default 346 return super.getAdapter(adapter); 347 } 348 349 @Override 350 protected void pageChange(int newPageIndex) { 351 super.pageChange(newPageIndex); 352 353 if (mGraphicalEditor != null) { 354 if (newPageIndex == mGraphicalEditorIndex) { 355 mGraphicalEditor.activated(); 356 } else { 357 mGraphicalEditor.deactivated(); 358 } 359 } 360 } 361 362 // ----- IPartListener Methods ---- 363 364 public void partActivated(IWorkbenchPart part) { 365 if (part == this) { 366 if (mGraphicalEditor != null) { 367 if (getActivePage() == mGraphicalEditorIndex) { 368 mGraphicalEditor.activated(); 369 } else { 370 mGraphicalEditor.deactivated(); 371 } 372 } 373 } 374 } 375 376 public void partBroughtToTop(IWorkbenchPart part) { 377 partActivated(part); 378 } 379 380 public void partClosed(IWorkbenchPart part) { 381 // pass 382 } 383 384 public void partDeactivated(IWorkbenchPart part) { 385 if (part == this) { 386 if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) { 387 mGraphicalEditor.deactivated(); 388 } 389 } 390 } 391 392 public void partOpened(IWorkbenchPart part) { 393 /* 394 * We used to automatically bring the outline and the property sheet to view 395 * when opening the editor. This behavior has always been a mixed bag and not 396 * exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to 397 * improve on that, so right now let's comment this out. 398 */ 399 //EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */); 400 //EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */); 401 } 402 403 public class UiEditorActions extends UiActions { 404 405 @Override 406 protected UiDocumentNode getRootNode() { 407 return mUiRootNode; 408 } 409 410 // Select the new item 411 @Override 412 protected void selectUiNode(UiElementNode uiNodeToSelect) { 413 mGraphicalEditor.selectModel(uiNodeToSelect); 414 } 415 416 @Override 417 public void commitPendingXmlChanges() { 418 // Pass. There is nothing to commit before the XML is changed here. 419 } 420 } 421 422 public UiEditorActions getUiEditorActions() { 423 if (mUiEditorActions == null) { 424 mUiEditorActions = new UiEditorActions(); 425 } 426 return mUiEditorActions; 427 } 428 429 // ---- Local Methods ---- 430 431 /** 432 * Returns true if the Graphics editor page is visible. This <b>must</b> be 433 * called from the UI thread. 434 */ 435 public boolean isGraphicalEditorActive() { 436 IWorkbenchPartSite workbenchSite = getSite(); 437 IWorkbenchPage workbenchPage = workbenchSite.getPage(); 438 439 // check if the editor is visible in the workbench page 440 if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) { 441 // and then if the page of the editor is visible (not to be confused with 442 // the workbench page) 443 return mGraphicalEditorIndex == getActivePage(); 444 } 445 446 return false; 447 } 448 449 @Override 450 public void initUiRootNode(boolean force) { 451 // The root UI node is always created, even if there's no corresponding XML node. 452 if (mUiRootNode == null || force) { 453 // get the target data from the opened file (and its project) 454 AndroidTargetData data = getTargetData(); 455 456 Document doc = null; 457 if (mUiRootNode != null) { 458 doc = mUiRootNode.getXmlDocument(); 459 } 460 461 DocumentDescriptor desc; 462 if (data == null) { 463 desc = new DocumentDescriptor("temp", null /*children*/); 464 } else { 465 desc = data.getLayoutDescriptors().getDescriptor(); 466 } 467 468 // get the descriptors from the data. 469 mUiRootNode = (UiDocumentNode) desc.createUiNode(); 470 mUiRootNode.setEditor(this); 471 472 mUiRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() { 473 474 public ElementDescriptor getDescriptor(String xmlLocalName) { 475 476 ElementDescriptor desc = mUnknownDescriptorMap.get(xmlLocalName); 477 478 if (desc == null) { 479 desc = createUnknownDescriptor(xmlLocalName); 480 mUnknownDescriptorMap.put(xmlLocalName, desc); 481 } 482 483 return desc; 484 } 485 }); 486 487 onDescriptorsChanged(doc); 488 } 489 } 490 491 /** 492 * Creates a new {@link ElementDescriptor} for an unknown XML local name 493 * (i.e. one that was not mapped by the current descriptors). 494 * <p/> 495 * Since we deal with layouts, we returns either a descriptor for a custom view 496 * or one for the base View. 497 * 498 * @param xmlLocalName The XML local name to match. 499 * @return A non-null {@link ElementDescriptor}. 500 */ 501 private ElementDescriptor createUnknownDescriptor(String xmlLocalName) { 502 IEditorInput editorInput = getEditorInput(); 503 if (editorInput instanceof IFileEditorInput) { 504 IFileEditorInput fileInput = (IFileEditorInput)editorInput; 505 IProject project = fileInput.getFile().getProject(); 506 507 // Check if we can find a custom view specific to this project. 508 ElementDescriptor desc = CustomViewDescriptorService.getInstance().getDescriptor( 509 project, xmlLocalName); 510 511 if (desc != null) { 512 return desc; 513 } 514 515 // If we didn't find a custom view, reuse the base View descriptor. 516 // This is a layout after all, so every XML node should represent 517 // a view. 518 519 Sdk currentSdk = Sdk.getCurrent(); 520 if (currentSdk != null) { 521 IAndroidTarget target = currentSdk.getTarget(project); 522 if (target != null) { 523 AndroidTargetData data = currentSdk.getTargetData(target); 524 if (data != null) { 525 // data can be null when the target is still loading 526 desc = data.getLayoutDescriptors().getBaseViewDescriptor(); 527 } 528 } 529 } 530 531 if (desc != null) { 532 return desc; 533 } 534 } 535 536 // We get here if the editor input is not of the right type or if the 537 // SDK hasn't finished loading. In either case, return something just 538 // because we should not return null. 539 return new ViewElementDescriptor(xmlLocalName, xmlLocalName); 540 } 541 542 private void onDescriptorsChanged(Document document) { 543 544 mUnknownDescriptorMap.clear(); 545 546 if (document != null) { 547 mUiRootNode.loadFromXmlNode(document); 548 } else { 549 mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument()); 550 } 551 552 if (mOutlineForGle1 != null) { 553 mOutlineForGle1.reloadModel(); 554 } 555 556 if (mGraphicalEditor != null) { 557 mGraphicalEditor.onTargetChange(); 558 mGraphicalEditor.reloadPalette(); 559 } 560 } 561 562 /** 563 * Handles a new input, and update the part name. 564 * @param input the new input. 565 */ 566 private void handleNewInput(IEditorInput input) { 567 if (input instanceof FileEditorInput) { 568 FileEditorInput fileInput = (FileEditorInput) input; 569 IFile file = fileInput.getFile(); 570 setPartName(String.format("%1$s", 571 file.getName())); 572 } 573 } 574 575 public void setNewFileOnConfigChange(boolean state) { 576 mNewFileOnConfigChange = state; 577 } 578 } 579