1 /* 2 * Copyright (C) 2008 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.internal.editors.IconFactory; 21 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor.UiEditorActions; 22 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; 23 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 24 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; 25 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; 26 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 27 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand; 28 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementEditPart; 29 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory; 30 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction; 31 import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction; 32 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 34 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration; 35 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 36 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile; 37 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; 38 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 39 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 40 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 41 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 42 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 43 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 44 import com.android.layoutlib.api.ILayoutBridge; 45 import com.android.layoutlib.api.ILayoutLog; 46 import com.android.layoutlib.api.ILayoutResult; 47 import com.android.layoutlib.api.IProjectCallback; 48 import com.android.layoutlib.api.IResourceValue; 49 import com.android.layoutlib.api.IXmlPullParser; 50 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; 51 import com.android.sdklib.IAndroidTarget; 52 53 import org.eclipse.core.resources.IFile; 54 import org.eclipse.core.resources.IFolder; 55 import org.eclipse.core.resources.IProject; 56 import org.eclipse.core.resources.IResource; 57 import org.eclipse.core.runtime.CoreException; 58 import org.eclipse.core.runtime.IProgressMonitor; 59 import org.eclipse.core.runtime.IStatus; 60 import org.eclipse.core.runtime.Status; 61 import org.eclipse.core.runtime.jobs.Job; 62 import org.eclipse.draw2d.geometry.Rectangle; 63 import org.eclipse.gef.DefaultEditDomain; 64 import org.eclipse.gef.EditPart; 65 import org.eclipse.gef.EditPartViewer; 66 import org.eclipse.gef.GraphicalViewer; 67 import org.eclipse.gef.SelectionManager; 68 import org.eclipse.gef.dnd.TemplateTransferDragSourceListener; 69 import org.eclipse.gef.dnd.TemplateTransferDropTargetListener; 70 import org.eclipse.gef.editparts.ScalableFreeformRootEditPart; 71 import org.eclipse.gef.palette.PaletteRoot; 72 import org.eclipse.gef.requests.CreationFactory; 73 import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette; 74 import org.eclipse.gef.ui.parts.SelectionSynchronizer; 75 import org.eclipse.jface.action.Action; 76 import org.eclipse.jface.action.IMenuListener; 77 import org.eclipse.jface.action.IMenuManager; 78 import org.eclipse.jface.action.MenuManager; 79 import org.eclipse.jface.action.Separator; 80 import org.eclipse.jface.dialogs.Dialog; 81 import org.eclipse.jface.viewers.ISelection; 82 import org.eclipse.swt.SWT; 83 import org.eclipse.swt.dnd.Clipboard; 84 import org.eclipse.swt.graphics.ImageData; 85 import org.eclipse.swt.graphics.PaletteData; 86 import org.eclipse.swt.layout.FillLayout; 87 import org.eclipse.swt.layout.GridData; 88 import org.eclipse.swt.layout.GridLayout; 89 import org.eclipse.swt.widgets.Composite; 90 import org.eclipse.ui.IEditorInput; 91 import org.eclipse.ui.PartInitException; 92 import org.eclipse.ui.ide.IDE; 93 import org.eclipse.ui.part.FileEditorInput; 94 95 import java.awt.image.BufferedImage; 96 import java.awt.image.DataBufferInt; 97 import java.awt.image.Raster; 98 import java.io.File; 99 import java.io.FileOutputStream; 100 import java.io.IOException; 101 import java.io.InputStream; 102 import java.io.PrintStream; 103 import java.util.ArrayList; 104 import java.util.HashMap; 105 import java.util.List; 106 import java.util.Map; 107 108 /** 109 * Graphical layout editor, based on GEF. 110 * <p/> 111 * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/ 112 * <p/> 113 * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html 114 * 115 * @since GLE1 116 */ 117 public class GraphicalLayoutEditor extends GraphicalEditorWithPalette 118 implements IGraphicalLayoutEditor, IConfigListener, ILayoutReloadListener { 119 120 121 /** Reference to the layout editor */ 122 private final LayoutEditor mLayoutEditor; 123 124 /** reference to the file being edited. */ 125 private IFile mEditedFile; 126 127 private Clipboard mClipboard; 128 private Composite mParent; 129 private ConfigurationComposite mConfigComposite; 130 131 private PaletteRoot mPaletteRoot; 132 133 /** The {@link FolderConfiguration} being edited. */ 134 private FolderConfiguration mEditedConfig; 135 136 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes; 137 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes; 138 private ProjectCallback mProjectCallback; 139 private ILayoutLog mLogger; 140 141 private boolean mNeedsXmlReload = false; 142 private boolean mNeedsRecompute = false; 143 144 /** Listener to update the root node if the target of the file is changed because of a 145 * SDK location change or a project target change */ 146 private ITargetChangeListener mTargetListener = new ITargetChangeListener() { 147 public void onProjectTargetChange(IProject changedProject) { 148 if (changedProject == getLayoutEditor().getProject()) { 149 onTargetsLoaded(); 150 } 151 } 152 153 public void onTargetsLoaded() { 154 // because the SDK changed we must reset the configured framework resource. 155 mConfiguredFrameworkRes = null; 156 157 mConfigComposite.updateUIFromResources(); 158 159 // updateUiFromFramework will reset language/region combo, so we must call 160 // setConfiguration after, or the settext on language/region will be lost. 161 if (mEditedConfig != null) { 162 setConfiguration(mEditedConfig, false /*force*/); 163 } 164 165 // make sure we remove the custom view loader, since its parent class loader is the 166 // bridge class loader. 167 mProjectCallback = null; 168 169 recomputeLayout(); 170 } 171 }; 172 173 private final Runnable mConditionalRecomputeRunnable = new Runnable() { 174 public void run() { 175 if (mLayoutEditor.isGraphicalEditorActive()) { 176 recomputeLayout(); 177 } else { 178 mNeedsRecompute = true; 179 } 180 } 181 }; 182 183 private final Runnable mUiUpdateFromResourcesRunnable = new Runnable() { 184 public void run() { 185 mConfigComposite.updateUIFromResources(); 186 } 187 }; 188 189 GraphicalLayoutEditor(LayoutEditor layoutEditor)190 public GraphicalLayoutEditor(LayoutEditor layoutEditor) { 191 mLayoutEditor = layoutEditor; 192 setEditDomain(new DefaultEditDomain(this)); 193 setPartName("Layout"); 194 195 AdtPlugin.getDefault().addTargetListener(mTargetListener); 196 } 197 198 // ------------------------------------ 199 // Methods overridden from base classes 200 //------------------------------------ 201 202 @Override createPartControl(Composite parent)203 public void createPartControl(Composite parent) { 204 mParent = parent; 205 GridLayout gl; 206 207 mClipboard = new Clipboard(parent.getDisplay()); 208 209 parent.setLayout(gl = new GridLayout(1, false)); 210 gl.marginHeight = gl.marginWidth = 0; 211 212 // create the top part for the configuration control 213 mConfigComposite = new ConfigurationComposite(this, parent, SWT.NONE); 214 215 // create a new composite that will contain the standard editor controls. 216 Composite editorParent = new Composite(parent, SWT.NONE); 217 editorParent.setLayoutData(new GridData(GridData.FILL_BOTH)); 218 editorParent.setLayout(new FillLayout()); 219 super.createPartControl(editorParent); 220 } 221 222 @Override dispose()223 public void dispose() { 224 if (mTargetListener != null) { 225 AdtPlugin.getDefault().removeTargetListener(mTargetListener); 226 mTargetListener = null; 227 } 228 229 LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this); 230 231 if (mClipboard != null) { 232 mClipboard.dispose(); 233 mClipboard = null; 234 } 235 236 super.dispose(); 237 } 238 239 /** 240 * Returns the selection synchronizer object. 241 * The synchronizer can be used to sync the selection of 2 or more EditPartViewers. 242 * <p/> 243 * This is changed from protected to public so that the outline can use it. 244 * 245 * @return the synchronizer 246 */ 247 @Override getSelectionSynchronizer()248 public SelectionSynchronizer getSelectionSynchronizer() { 249 return super.getSelectionSynchronizer(); 250 } 251 252 /** 253 * Returns the edit domain. 254 * <p/> 255 * This is changed from protected to public so that the outline can use it. 256 * 257 * @return the edit domain 258 */ 259 @Override getEditDomain()260 public DefaultEditDomain getEditDomain() { 261 return super.getEditDomain(); 262 } 263 264 /* (non-Javadoc) 265 * Creates the palette root. 266 */ 267 @Override getPaletteRoot()268 protected PaletteRoot getPaletteRoot() { 269 mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot, 270 mLayoutEditor.getTargetData()); 271 return mPaletteRoot; 272 } 273 getClipboard()274 public Clipboard getClipboard() { 275 return mClipboard; 276 } 277 278 /** 279 * Save operation in the Graphical Layout Editor. 280 * <p/> 281 * In our workflow, the model is owned by the Structured XML Editor. 282 * The graphical layout editor just displays it -- thus we don't really 283 * save anything here. 284 * <p/> 285 * This must NOT call the parent editor part. At the contrary, the parent editor 286 * part will call this *after* having done the actual save operation. 287 * <p/> 288 * The only action this editor must do is mark the undo command stack as 289 * being no longer dirty. 290 */ 291 @Override doSave(IProgressMonitor monitor)292 public void doSave(IProgressMonitor monitor) { 293 getCommandStack().markSaveLocation(); 294 firePropertyChange(PROP_DIRTY); 295 } 296 297 @Override configurePaletteViewer()298 protected void configurePaletteViewer() { 299 super.configurePaletteViewer(); 300 301 // Create a drag source listener on an edit part that is a viewer. 302 // What this does is use DND with a TemplateTransfer type which is actually 303 // the PaletteTemplateEntry held in the PaletteRoot. 304 TemplateTransferDragSourceListener dragSource = 305 new TemplateTransferDragSourceListener(getPaletteViewer()); 306 307 // Create a drag source on the palette viewer. 308 // See the drag target associated with the GraphicalViewer in configureGraphicalViewer. 309 getPaletteViewer().addDragSourceListener(dragSource); 310 } 311 312 /* (non-javadoc) 313 * Configure the graphical viewer before it receives its contents. 314 */ 315 @Override configureGraphicalViewer()316 protected void configureGraphicalViewer() { 317 super.configureGraphicalViewer(); 318 319 GraphicalViewer viewer = getGraphicalViewer(); 320 viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay())); 321 viewer.setRootEditPart(new ScalableFreeformRootEditPart()); 322 323 // Disable the following -- we don't drag *from* the GraphicalViewer yet: 324 // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer)); 325 326 viewer.addDropTargetListener(new DropListener(viewer)); 327 } 328 329 class DropListener extends TemplateTransferDropTargetListener { DropListener(EditPartViewer viewer)330 public DropListener(EditPartViewer viewer) { 331 super(viewer); 332 } 333 334 // TODO explain 335 @Override getFactory(final Object template)336 protected CreationFactory getFactory(final Object template) { 337 return new CreationFactory() { 338 public Object getNewObject() { 339 // We don't know the newly created EditPart since "creating" new 340 // elements is done by ElementCreateCommand.execute() directly by 341 // manipulating the XML elements.. 342 return null; 343 } 344 345 public Object getObjectType() { 346 return template; 347 } 348 349 }; 350 } 351 } 352 353 /* (non-javadoc) 354 * Set the contents of the GraphicalViewer after it has been created. 355 */ 356 @Override 357 protected void initializeGraphicalViewer() { 358 GraphicalViewer viewer = getGraphicalViewer(); 359 viewer.setContents(getModel()); 360 361 IEditorInput input = getEditorInput(); 362 if (input instanceof FileEditorInput) { 363 FileEditorInput fileInput = (FileEditorInput)input; 364 mEditedFile = fileInput.getFile(); 365 366 mConfigComposite.updateUIFromResources(); 367 368 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this); 369 } else { 370 // really this shouldn't happen! Log it in case it happens 371 mEditedFile = null; 372 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s", 373 input.toString()); 374 } 375 } 376 377 /* (non-javadoc) 378 * Sets the graphicalViewer for this EditorPart. 379 * @param viewer the graphical viewer 380 */ 381 @Override 382 protected void setGraphicalViewer(GraphicalViewer viewer) { 383 super.setGraphicalViewer(viewer); 384 385 // TODO: viewer.setKeyHandler() 386 viewer.setContextMenu(createContextMenu(viewer)); 387 } 388 389 /** 390 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node 391 * created by {@link ElementCreateCommand#execute()}. 392 * 393 * @param uiNodeModel The {@link UiElementNode} to select. 394 */ 395 public void selectModel(UiElementNode uiNodeModel) { 396 GraphicalViewer viewer = getGraphicalViewer(); 397 398 // Give focus to the graphical viewer (in case the outline has it) 399 viewer.getControl().forceFocus(); 400 401 Object editPart = viewer.getEditPartRegistry().get(uiNodeModel); 402 403 if (editPart instanceof EditPart) { 404 viewer.select((EditPart)editPart); 405 } 406 } 407 408 409 //-------------- 410 // Local methods 411 //-------------- 412 413 public LayoutEditor getLayoutEditor() { 414 return mLayoutEditor; 415 } 416 417 private MenuManager createContextMenu(GraphicalViewer viewer) { 418 MenuManager menuManager = new MenuManager(); 419 menuManager.setRemoveAllWhenShown(true); 420 menuManager.addMenuListener(new ActionMenuListener(viewer)); 421 422 return menuManager; 423 } 424 425 private class ActionMenuListener implements IMenuListener { 426 private final GraphicalViewer mViewer; 427 428 public ActionMenuListener(GraphicalViewer viewer) { 429 mViewer = viewer; 430 } 431 432 /** 433 * The menu is about to be shown. The menu manager has already been 434 * requested to remove any existing menu item. This method gets the 435 * tree selection and if it is of the appropriate type it re-creates 436 * the necessary actions. 437 */ 438 public void menuAboutToShow(IMenuManager manager) { 439 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); 440 441 // filter selected items and only keep those we can handle 442 for (Object obj : mViewer.getSelectedEditParts()) { 443 if (obj instanceof UiElementEditPart) { 444 UiElementEditPart part = (UiElementEditPart) obj; 445 UiElementNode uiNode = part.getUiNode(); 446 if (uiNode != null) { 447 selected.add(uiNode); 448 } 449 } 450 } 451 452 if (selected.size() > 0) { 453 doCreateMenuAction(manager, mViewer, selected); 454 } 455 } 456 } 457 458 private void doCreateMenuAction(IMenuManager manager, 459 final GraphicalViewer viewer, 460 final ArrayList<UiElementNode> selected) { 461 if (selected != null) { 462 boolean hasXml = false; 463 for (UiElementNode uiNode : selected) { 464 if (uiNode.getXmlNode() != null) { 465 hasXml = true; 466 break; 467 } 468 } 469 470 if (hasXml) { 471 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(), 472 null, selected, true /* cut */)); 473 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(), 474 null, selected, false /* cut */)); 475 476 // Can't paste with more than one element selected (the selection is the target) 477 if (selected.size() <= 1) { 478 // Paste is not valid if it would add a second element on a terminal element 479 // which parent is a document -- an XML document can only have one child. This 480 // means paste is valid if the current UI node can have children or if the 481 // parent is not a document. 482 UiElementNode ui_root = selected.get(0).getUiRoot(); 483 if (ui_root.getDescriptor().hasChildren() || 484 !(ui_root.getUiParent() instanceof UiDocumentNode)) { 485 manager.add(new PasteAction(mLayoutEditor, getClipboard(), 486 selected.get(0))); 487 } 488 } 489 manager.add(new Separator()); 490 } 491 } 492 493 // Append "add" and "remove" actions. They do the same thing as the add/remove 494 // buttons on the side. 495 IconFactory factory = IconFactory.getInstance(); 496 497 final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions(); 498 499 // "Add" makes sense only if there's 0 or 1 item selected since the 500 // one selected item becomes the target. 501 if (selected == null || selected.size() <= 1) { 502 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$ 503 @Override 504 public void run() { 505 UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0) 506 : null; 507 uiActions.doAdd(node, viewer.getControl().getShell()); 508 } 509 }); 510 } 511 512 if (selected != null) { 513 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$ 514 @Override 515 public void run() { 516 uiActions.doRemove(selected, viewer.getControl().getShell()); 517 } 518 }); 519 520 manager.add(new Separator()); 521 522 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$ 523 @Override 524 public void run() { 525 uiActions.doUp(selected); 526 } 527 }); 528 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$ 529 @Override 530 public void run() { 531 uiActions.doDown(selected); 532 } 533 }); 534 } 535 536 } 537 538 /** 539 * Sets the UI for the edition of a new file. 540 * @param configuration the configuration of the new file. 541 */ 542 public void editNewFile(FolderConfiguration configuration) { 543 // update the configuration UI 544 setConfiguration(configuration, true /*force*/); 545 546 // enable the create button if the current and edited config are not equals 547 mConfigComposite.setEnabledCreate( 548 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false); 549 550 reloadConfigurationUi(false /*notifyListener*/); 551 } 552 553 public Rectangle getBounds() { 554 return mConfigComposite.getScreenBounds(); 555 } 556 557 /** 558 * Renders an Android View described by a {@link ViewElementDescriptor}. 559 * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and 560 * <code>layout_height</code>, and use the class name for the <code>text</code> attribute. 561 * @param descriptor the descriptor for the class to render. 562 * @return an ImageData containing the rendering or <code>null</code> if rendering failed. 563 */ 564 public ImageData renderWidget(ViewElementDescriptor descriptor) { 565 if (mEditedFile == null) { 566 return null; 567 } 568 569 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); 570 if (target == null) { 571 return null; 572 } 573 574 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 575 if (data == null) { 576 return null; 577 } 578 579 LayoutBridge bridge = data.getLayoutBridge(); 580 581 if (bridge.bridge != null) { // bridge can never be null. 582 ResourceManager resManager = ResourceManager.getInstance(); 583 584 ProjectCallback projectCallback = null; 585 Map<String, Map<String, IResourceValue>> configuredProjectResources = null; 586 if (mEditedFile != null) { 587 ProjectResources projectRes = resManager.getProjectResources( 588 mEditedFile.getProject()); 589 projectCallback = new ProjectCallback(bridge.classLoader, 590 projectRes, mEditedFile.getProject()); 591 592 // get the configured resources for the project 593 // get the resources of the file's project. 594 if (mConfiguredProjectRes == null && projectRes != null) { 595 // make sure they are loaded 596 projectRes.loadAll(); 597 598 // get the project resource values based on the current config 599 mConfiguredProjectRes = projectRes.getConfiguredResources( 600 mConfigComposite.getCurrentConfig()); 601 } 602 603 configuredProjectResources = mConfiguredProjectRes; 604 } else { 605 // we absolutely need a Map of configured project resources. 606 configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>(); 607 } 608 609 // get the framework resources 610 Map<String, Map<String, IResourceValue>> frameworkResources = 611 getConfiguredFrameworkResources(); 612 613 if (configuredProjectResources != null && frameworkResources != null) { 614 // get the selected theme 615 String theme = mConfigComposite.getTheme(); 616 if (theme != null) { 617 // Render a single object as described by the ViewElementDescriptor. 618 WidgetPullParser parser = new WidgetPullParser(descriptor); 619 ILayoutResult result = computeLayout(bridge, parser, 620 null /* projectKey */, 621 1 /* width */, 1 /* height */, true /* renderFullSize */, 622 160 /*density*/, 160.f /*xdpi*/, 160.f /*ydpi*/, theme, 623 mConfigComposite.isProjectTheme(), 624 configuredProjectResources, frameworkResources, projectCallback, 625 null /* logger */); 626 627 // update the UiElementNode with the layout info. 628 if (result.getSuccess() == ILayoutResult.SUCCESS) { 629 BufferedImage largeImage = result.getImage(); 630 631 // we need to resize it to the actual widget size, and convert it into 632 // an SWT image object. 633 int width = result.getRootView().getRight(); 634 int height = result.getRootView().getBottom(); 635 Raster raster = largeImage.getData(new java.awt.Rectangle(width, height)); 636 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData(); 637 638 ImageData imageData = new ImageData(width, height, 32, 639 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); 640 641 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); 642 643 return imageData; 644 } 645 } 646 } 647 } 648 return null; 649 } 650 651 /** 652 * Reloads this editor, by getting the new model from the {@link LayoutEditor}. 653 */ 654 public void reloadEditor() { 655 GraphicalViewer viewer = getGraphicalViewer(); 656 viewer.setContents(getModel()); 657 658 IEditorInput input = mLayoutEditor.getEditorInput(); 659 setInput(input); 660 661 if (input instanceof FileEditorInput) { 662 FileEditorInput fileInput = (FileEditorInput)input; 663 mEditedFile = fileInput.getFile(); 664 } else { 665 // really this shouldn't happen! Log it in case it happens 666 mEditedFile = null; 667 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s", 668 input.toString()); 669 } 670 } 671 672 /** 673 * Callback for XML model changed. Only update/recompute the layout if the editor is visible 674 */ 675 public void onXmlModelChanged() { 676 if (mLayoutEditor.isGraphicalEditorActive()) { 677 doXmlReload(true /* force */); 678 recomputeLayout(); 679 } else { 680 mNeedsXmlReload = true; 681 } 682 } 683 684 /** 685 * Actually performs the XML reload 686 * @see #onXmlModelChanged() 687 */ 688 private void doXmlReload(boolean force) { 689 if (force || mNeedsXmlReload) { 690 GraphicalViewer viewer = getGraphicalViewer(); 691 692 // try to preserve the selection before changing the content 693 SelectionManager selMan = viewer.getSelectionManager(); 694 ISelection selection = selMan.getSelection(); 695 696 try { 697 viewer.setContents(getModel()); 698 } finally { 699 selMan.setSelection(selection); 700 } 701 702 mNeedsXmlReload = false; 703 } 704 } 705 706 /** 707 * Update the UI controls state with a given {@link FolderConfiguration}. 708 * <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect 709 * <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>, 710 * the UI control is not modified. However if the value in the control is not the default value, 711 * a warning icon is shown. 712 * @param config The {@link FolderConfiguration} to set. 713 * @param force Whether the UI should be changed to exactly match the received configuration. 714 */ 715 void setConfiguration(FolderConfiguration config, boolean force) { 716 mEditedConfig = config; 717 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 718 719 mConfigComposite.setConfiguration(config, force); 720 721 } 722 723 724 public UiDocumentNode getModel() { 725 return mLayoutEditor.getUiRootNode(); 726 } 727 728 public void reloadPalette() { 729 PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData()); 730 } 731 732 public void reloadConfigurationUi(boolean notifyListener) { 733 // enable the clipping button if it's supported. 734 Sdk currentSdk = Sdk.getCurrent(); 735 if (currentSdk != null) { 736 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 737 AndroidTargetData data = currentSdk.getTargetData(target); 738 if (data != null) { 739 LayoutBridge bridge = data.getLayoutBridge(); 740 mConfigComposite.reloadDevices(notifyListener); 741 mConfigComposite.setClippingSupport(bridge.apiLevel >= 4); 742 } 743 } 744 } 745 746 /** 747 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. 748 * <p/>If there is no match, notify the user. 749 */ 750 public void onConfigurationChange() { 751 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 752 753 if (mEditedFile == null || mEditedConfig == null) { 754 return; 755 } 756 757 // get the resources of the file's project. 758 ProjectResources resources = ResourceManager.getInstance().getProjectResources( 759 mEditedFile.getProject()); 760 761 // from the resources, look for a matching file 762 ResourceFile match = null; 763 if (resources != null) { 764 match = resources.getMatchingFile(mEditedFile.getName(), 765 ResourceFolderType.LAYOUT, 766 mConfigComposite.getCurrentConfig()); 767 } 768 769 if (match != null) { 770 if (match.getFile().equals(mEditedFile) == false) { 771 try { 772 IDE.openEditor( 773 getSite().getWorkbenchWindow().getActivePage(), 774 match.getFile().getIFile()); 775 776 // we're done! 777 return; 778 } catch (PartInitException e) { 779 // FIXME: do something! 780 } 781 } 782 783 // at this point, we have not opened a new file. 784 785 // update the configuration icons with the new edited config. 786 setConfiguration(mEditedConfig, false /*force*/); 787 788 // enable the create button if the current and edited config are not equals 789 mConfigComposite.setEnabledCreate( 790 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false); 791 792 // Even though the layout doesn't change, the config changed, and referenced 793 // resources need to be updated. 794 recomputeLayout(); 795 } else { 796 // enable the Create button 797 mConfigComposite.setEnabledCreate(true); 798 799 // display the error. 800 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig(); 801 String message = String.format( 802 "No resources match the configuration\n \n\t%1$s\n \nChange the configuration or create:\n \n\tres/%2$s/%3$s\n \nYou can also click the 'Create' button above.", 803 currentConfig.toDisplayString(), 804 currentConfig.getFolderName(ResourceFolderType.LAYOUT, 805 Sdk.getCurrent().getTarget(mEditedFile.getProject())), 806 mEditedFile.getName()); 807 showErrorInEditor(message); 808 } 809 } 810 811 public void onThemeChange() { 812 recomputeLayout(); 813 } 814 815 public void OnClippingChange() { 816 recomputeLayout(); 817 } 818 819 820 public void onCreate() { 821 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mParent.getShell(), 822 mEditedFile.getName(), 823 Sdk.getCurrent().getTarget(mEditedFile.getProject()), 824 mConfigComposite.getCurrentConfig()); 825 if (dialog.open() == Dialog.OK) { 826 final FolderConfiguration config = new FolderConfiguration(); 827 dialog.getConfiguration(config); 828 829 createAlternateLayout(config); 830 } 831 } 832 833 /** 834 * Recomputes the layout with the help of layoutlib. 835 */ 836 public void recomputeLayout() { 837 doXmlReload(false /* force */); 838 try { 839 // check that the resource exists. If the file is opened but the project is closed 840 // or deleted for some reason (changed from outside of eclipse), then this will 841 // return false; 842 if (mEditedFile.exists() == false) { 843 String message = String.format("Resource '%1$s' does not exist.", 844 mEditedFile.getFullPath().toString()); 845 846 showErrorInEditor(message); 847 848 return; 849 } 850 851 IProject iProject = mEditedFile.getProject(); 852 853 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { 854 String message = String.format("%1$s is out of sync. Please refresh.", 855 mEditedFile.getName()); 856 857 showErrorInEditor(message); 858 859 // also print it in the error console. 860 AdtPlugin.printErrorToConsole(iProject.getName(), message); 861 return; 862 } 863 864 Sdk currentSdk = Sdk.getCurrent(); 865 if (currentSdk != null) { 866 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 867 if (target == null) { 868 showErrorInEditor("The project target is not set."); 869 return; 870 } 871 872 AndroidTargetData data = currentSdk.getTargetData(target); 873 if (data == null) { 874 // It can happen that the workspace refreshes while the SDK is loading its 875 // data, which could trigger a redraw of the opened layout if some resources 876 // changed while Eclipse is closed. 877 // In this case data could be null, but this is not an error. 878 // We can just silently return, as all the opened editors are automatically 879 // refreshed once the SDK finishes loading. 880 if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) { 881 showErrorInEditor(String.format( 882 "The project target (%s) was not properly loaded.", 883 target.getName())); 884 } 885 return; 886 } 887 888 // check there is actually a model (maybe the file is empty). 889 UiDocumentNode model = getModel(); 890 891 if (model.getUiChildren().size() == 0) { 892 showErrorInEditor("No Xml content. Go to the Outline view and add nodes."); 893 return; 894 } 895 896 LayoutBridge bridge = data.getLayoutBridge(); 897 898 if (bridge.bridge != null) { // bridge can never be null. 899 ResourceManager resManager = ResourceManager.getInstance(); 900 901 ProjectResources projectRes = resManager.getProjectResources(iProject); 902 if (projectRes == null) { 903 return; 904 } 905 906 // get the resources of the file's project. 907 Map<String, Map<String, IResourceValue>> configuredProjectRes = 908 getConfiguredProjectResources(); 909 910 // get the framework resources 911 Map<String, Map<String, IResourceValue>> frameworkResources = 912 getConfiguredFrameworkResources(); 913 914 if (configuredProjectRes != null && frameworkResources != null) { 915 if (mProjectCallback == null) { 916 mProjectCallback = new ProjectCallback( 917 bridge.classLoader, projectRes, iProject); 918 } 919 920 if (mLogger == null) { 921 mLogger = new ILayoutLog() { 922 public void error(String message) { 923 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message); 924 } 925 926 public void error(Throwable error) { 927 String message = error.getMessage(); 928 if (message == null) { 929 message = error.getClass().getName(); 930 } 931 932 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream()); 933 error.printStackTrace(ps); 934 } 935 936 public void warning(String message) { 937 AdtPlugin.printToConsole(mEditedFile.getName(), message); 938 } 939 }; 940 } 941 942 // get the selected theme 943 String theme = mConfigComposite.getTheme(); 944 if (theme != null) { 945 946 // Compute the layout 947 UiElementPullParser parser = new UiElementPullParser(getModel()); 948 Rectangle rect = getBounds(); 949 boolean isProjectTheme = mConfigComposite.isProjectTheme(); 950 951 int density = mConfigComposite.getDensity().getDpiValue(); 952 float xdpi = mConfigComposite.getXDpi(); 953 float ydpi = mConfigComposite.getYDpi(); 954 955 ILayoutResult result = computeLayout(bridge, parser, 956 iProject /* projectKey */, 957 rect.width, rect.height, !mConfigComposite.getClipping(), 958 density, xdpi, ydpi, 959 theme, isProjectTheme, 960 configuredProjectRes, frameworkResources, mProjectCallback, 961 mLogger); 962 963 // update the UiElementNode with the layout info. 964 if (result.getSuccess() == ILayoutResult.SUCCESS) { 965 model.setEditData(result.getImage()); 966 967 updateNodeWithBounds(result.getRootView()); 968 } else { 969 String message = result.getErrorMessage(); 970 971 // Reset the edit data for all the nodes. 972 resetNodeBounds(model); 973 974 if (message != null) { 975 // set the error in the top element. 976 model.setEditData(message); 977 } 978 } 979 980 model.refreshUi(); 981 } 982 } 983 } else { 984 // SDK is loaded but not the layout library! 985 String message = null; 986 // check whether the bridge managed to load, or not 987 if (bridge.status == LoadStatus.LOADING) { 988 message = String.format( 989 "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.", 990 mEditedFile.getName()); 991 } else { 992 message = String.format("Eclipse failed to load the framework information and the Layout library!"); 993 } 994 showErrorInEditor(message); 995 } 996 } else { 997 String message = String.format( 998 "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", 999 mEditedFile.getName()); 1000 1001 showErrorInEditor(message); 1002 } 1003 } finally { 1004 // no matter the result, we are done doing the recompute based on the latest 1005 // resource/code change. 1006 mNeedsRecompute = false; 1007 } 1008 } 1009 1010 private void showErrorInEditor(String message) { 1011 // get the model to display the error directly in the editor 1012 UiDocumentNode model = getModel(); 1013 1014 // Reset the edit data for all the nodes. 1015 resetNodeBounds(model); 1016 1017 if (message != null) { 1018 // set the error in the top element. 1019 model.setEditData(message); 1020 } 1021 1022 model.refreshUi(); 1023 } 1024 1025 private void resetNodeBounds(UiElementNode node) { 1026 node.setEditData(null); 1027 1028 List<UiElementNode> children = node.getUiChildren(); 1029 for (UiElementNode child : children) { 1030 resetNodeBounds(child); 1031 } 1032 } 1033 1034 private void updateNodeWithBounds(ILayoutViewInfo r) { 1035 if (r != null) { 1036 // update the node itself, as the viewKey is the XML node in this implementation. 1037 Object viewKey = r.getViewKey(); 1038 if (viewKey instanceof UiElementNode) { 1039 Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(), 1040 r.getRight()-r.getLeft(), r.getBottom() - r.getTop()); 1041 1042 ((UiElementNode)viewKey).setEditData(bounds); 1043 } 1044 1045 // and then its children. 1046 ILayoutViewInfo[] children = r.getChildren(); 1047 if (children != null) { 1048 for (ILayoutViewInfo child : children) { 1049 updateNodeWithBounds(child); 1050 } 1051 } 1052 } 1053 } 1054 1055 /* 1056 * (non-Javadoc) 1057 * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean) 1058 * 1059 * Called when the file changes triggered a redraw of the layout 1060 */ 1061 public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) { 1062 boolean recompute = rChange; 1063 1064 if (resChange) { 1065 recompute = true; 1066 1067 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache. 1068 1069 // force a reparse in case a value XML file changed. 1070 mConfiguredProjectRes = null; 1071 1072 // clear the cache in the bridge in case a bitmap/9-patch changed. 1073 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); 1074 if (target != null) { 1075 1076 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 1077 if (data != null) { 1078 LayoutBridge bridge = data.getLayoutBridge(); 1079 1080 if (bridge.bridge != null) { 1081 bridge.bridge.clearCaches(mEditedFile.getProject()); 1082 } 1083 } 1084 } 1085 1086 mParent.getDisplay().asyncExec(mUiUpdateFromResourcesRunnable); 1087 } 1088 1089 if (codeChange) { 1090 // only recompute if the custom view loader was used to load some code. 1091 if (mProjectCallback != null && mProjectCallback.isUsed()) { 1092 mProjectCallback = null; 1093 recompute = true; 1094 } 1095 } 1096 1097 if (recompute) { 1098 mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable); 1099 } 1100 } 1101 1102 /** 1103 * Responds to a page change that made the Graphical editor page the activated page. 1104 */ 1105 public void activated() { 1106 if (mNeedsRecompute || mNeedsXmlReload) { 1107 recomputeLayout(); 1108 } 1109 } 1110 1111 /** 1112 * Responds to a page change that made the Graphical editor page the deactivated page 1113 */ 1114 public void deactivated() { 1115 // nothing to be done here for now. 1116 } 1117 1118 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() { 1119 if (mConfiguredFrameworkRes == null) { 1120 ProjectResources frameworkRes = getFrameworkResources(); 1121 1122 if (frameworkRes == null) { 1123 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); 1124 } else { 1125 // get the framework resource values based on the current config 1126 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( 1127 mConfigComposite.getCurrentConfig()); 1128 } 1129 } 1130 1131 return mConfiguredFrameworkRes; 1132 } 1133 1134 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() { 1135 if (mConfiguredProjectRes == null) { 1136 ProjectResources project = getProjectResources(); 1137 1138 // make sure they are loaded 1139 project.loadAll(); 1140 1141 // get the project resource values based on the current config 1142 mConfiguredProjectRes = project.getConfiguredResources( 1143 mConfigComposite.getCurrentConfig()); 1144 } 1145 1146 return mConfiguredProjectRes; 1147 } 1148 1149 /** 1150 * Returns a {@link ProjectResources} for the framework resources. 1151 * @return the framework resources or null if not found. 1152 */ 1153 public ProjectResources getFrameworkResources() { 1154 if (mEditedFile != null) { 1155 Sdk currentSdk = Sdk.getCurrent(); 1156 if (currentSdk != null) { 1157 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 1158 1159 if (target != null) { 1160 AndroidTargetData data = currentSdk.getTargetData(target); 1161 1162 if (data != null) { 1163 return data.getFrameworkResources(); 1164 } 1165 } 1166 } 1167 } 1168 1169 return null; 1170 } 1171 1172 public ProjectResources getProjectResources() { 1173 if (mEditedFile != null) { 1174 ResourceManager manager = ResourceManager.getInstance(); 1175 return manager.getProjectResources(mEditedFile.getProject()); 1176 } 1177 1178 return null; 1179 } 1180 1181 /** 1182 * Creates a new layout file from the specified {@link FolderConfiguration}. 1183 */ 1184 private void createAlternateLayout(final FolderConfiguration config) { 1185 new Job("Create Alternate Resource") { 1186 @Override 1187 protected IStatus run(IProgressMonitor monitor) { 1188 // get the folder name 1189 String folderName = config.getFolderName(ResourceFolderType.LAYOUT, 1190 Sdk.getCurrent().getTarget(mEditedFile.getProject())); 1191 try { 1192 1193 // look to see if it exists. 1194 // get the res folder 1195 IFolder res = (IFolder)mEditedFile.getParent().getParent(); 1196 String path = res.getLocation().toOSString(); 1197 1198 File newLayoutFolder = new File(path + File.separator + folderName); 1199 if (newLayoutFolder.isFile()) { 1200 // this should not happen since aapt would have complained 1201 // before, but if one disable the automatic build, this could 1202 // happen. 1203 String message = String.format("File 'res/%1$s' is in the way!", 1204 folderName); 1205 1206 AdtPlugin.displayError("Layout Creation", message); 1207 1208 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); 1209 } else if (newLayoutFolder.exists() == false) { 1210 // create it. 1211 newLayoutFolder.mkdir(); 1212 } 1213 1214 // now create the file 1215 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + 1216 File.separator + mEditedFile.getName()); 1217 1218 newLayoutFile.createNewFile(); 1219 1220 InputStream input = mEditedFile.getContents(); 1221 1222 FileOutputStream fos = new FileOutputStream(newLayoutFile); 1223 1224 byte[] data = new byte[512]; 1225 int count; 1226 while ((count = input.read(data)) != -1) { 1227 fos.write(data, 0, count); 1228 } 1229 1230 input.close(); 1231 fos.close(); 1232 1233 // refreshes the res folder to show up the new 1234 // layout folder (if needed) and the file. 1235 // We use a progress monitor to catch the end of the refresh 1236 // to trigger the edit of the new file. 1237 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { 1238 public void done() { 1239 mParent.getDisplay().asyncExec(new Runnable() { 1240 public void run() { 1241 onConfigurationChange(); 1242 } 1243 }); 1244 } 1245 1246 public void beginTask(String name, int totalWork) { 1247 // pass 1248 } 1249 1250 public void internalWorked(double work) { 1251 // pass 1252 } 1253 1254 public boolean isCanceled() { 1255 // pass 1256 return false; 1257 } 1258 1259 public void setCanceled(boolean value) { 1260 // pass 1261 } 1262 1263 public void setTaskName(String name) { 1264 // pass 1265 } 1266 1267 public void subTask(String name) { 1268 // pass 1269 } 1270 1271 public void worked(int work) { 1272 // pass 1273 } 1274 }); 1275 } catch (IOException e2) { 1276 String message = String.format( 1277 "Failed to create File 'res/%1$s/%2$s' : %3$s", 1278 folderName, mEditedFile.getName(), e2.getMessage()); 1279 1280 AdtPlugin.displayError("Layout Creation", message); 1281 1282 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 1283 message, e2); 1284 } catch (CoreException e2) { 1285 String message = String.format( 1286 "Failed to create File 'res/%1$s/%2$s' : %3$s", 1287 folderName, mEditedFile.getName(), e2.getMessage()); 1288 1289 AdtPlugin.displayError("Layout Creation", message); 1290 1291 return e2.getStatus(); 1292 } 1293 1294 return Status.OK_STATUS; 1295 1296 } 1297 }.schedule(); 1298 } 1299 1300 /** 1301 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on 1302 * the implementation API level. 1303 */ 1304 @SuppressWarnings("deprecation") 1305 private static ILayoutResult computeLayout(LayoutBridge bridge, 1306 IXmlPullParser layoutDescription, Object projectKey, 1307 int screenWidth, int screenHeight, boolean renderFullSize, 1308 int density, float xdpi, float ydpi, 1309 String themeName, boolean isProjectTheme, 1310 Map<String, Map<String, IResourceValue>> projectResources, 1311 Map<String, Map<String, IResourceValue>> frameworkResources, 1312 IProjectCallback projectCallback, ILayoutLog logger) { 1313 1314 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) { 1315 // newest API with support for "render full height" 1316 // TODO: link boolean to UI. 1317 return bridge.bridge.computeLayout(layoutDescription, 1318 projectKey, screenWidth, screenHeight, renderFullSize, 1319 density, xdpi, ydpi, 1320 themeName, isProjectTheme, 1321 projectResources, frameworkResources, projectCallback, 1322 logger); 1323 } else if (bridge.apiLevel == 3) { 1324 // newer api with density support. 1325 return bridge.bridge.computeLayout(layoutDescription, 1326 projectKey, screenWidth, screenHeight, density, xdpi, ydpi, 1327 themeName, isProjectTheme, 1328 projectResources, frameworkResources, projectCallback, 1329 logger); 1330 } else if (bridge.apiLevel == 2) { 1331 // api with boolean for separation of project/framework theme 1332 return bridge.bridge.computeLayout(layoutDescription, 1333 projectKey, screenWidth, screenHeight, themeName, isProjectTheme, 1334 projectResources, frameworkResources, projectCallback, 1335 logger); 1336 } else { 1337 // oldest api with no density/dpi, and project theme boolean mixed 1338 // into the theme name. 1339 1340 // change the string if it's a custom theme to make sure we can 1341 // differentiate them 1342 if (isProjectTheme) { 1343 themeName = "*" + themeName; //$NON-NLS-1$ 1344 } 1345 1346 return bridge.bridge.computeLayout(layoutDescription, 1347 projectKey, screenWidth, screenHeight, themeName, 1348 projectResources, frameworkResources, projectCallback, 1349 logger); 1350 } 1351 } 1352 } 1353