2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle1;
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.ExplodedRenderingHelper;
22 import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor;
23 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
24 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
25 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
26 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
27 import com.android.ide.eclipse.adt.internal.editors.layout.WidgetPullParser;
28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor.UiEditorActions;
29 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
30 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
31 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
32 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle;
33 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;
34 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
35 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;
36 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementEditPart;
37 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory;
38 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory.IOutlineProvider;
39 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction;
40 import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction;
41 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
42 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
43 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
44 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
45 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
46 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
47 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
48 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
49 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
50 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
51 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
52 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
53 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
54 import com.android.layoutlib.api.ILayoutBridge;
55 import com.android.layoutlib.api.ILayoutLog;
56 import com.android.layoutlib.api.ILayoutResult;
57 import com.android.layoutlib.api.IProjectCallback;
58 import com.android.layoutlib.api.IResourceValue;
59 import com.android.layoutlib.api.IXmlPullParser;
60 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
61 import com.android.sdklib.IAndroidTarget;
63 import org.eclipse.core.resources.IFile;
64 import org.eclipse.core.resources.IFolder;
65 import org.eclipse.core.resources.IProject;
66 import org.eclipse.core.resources.IResource;
67 import org.eclipse.core.runtime.CoreException;
68 import org.eclipse.core.runtime.IProgressMonitor;
69 import org.eclipse.core.runtime.IStatus;
70 import org.eclipse.core.runtime.Status;
71 import org.eclipse.core.runtime.jobs.Job;
72 import org.eclipse.draw2d.geometry.Rectangle;
73 import org.eclipse.gef.DefaultEditDomain;
74 import org.eclipse.gef.EditPart;
75 import org.eclipse.gef.EditPartViewer;
76 import org.eclipse.gef.GraphicalViewer;
77 import org.eclipse.gef.SelectionManager;
78 import org.eclipse.gef.dnd.TemplateTransferDragSourceListener;
79 import org.eclipse.gef.dnd.TemplateTransferDropTargetListener;
80 import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
81 import org.eclipse.gef.palette.PaletteRoot;
82 import org.eclipse.gef.requests.CreationFactory;
83 import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette;
84 import org.eclipse.gef.ui.parts.SelectionSynchronizer;
85 import org.eclipse.jface.action.Action;
86 import org.eclipse.jface.action.IMenuListener;
87 import org.eclipse.jface.action.IMenuManager;
88 import org.eclipse.jface.action.MenuManager;
89 import org.eclipse.jface.action.Separator;
90 import org.eclipse.jface.dialogs.Dialog;
91 import org.eclipse.jface.viewers.ISelection;
92 import org.eclipse.swt.SWT;
93 import org.eclipse.swt.dnd.Clipboard;
94 import org.eclipse.swt.graphics.ImageData;
95 import org.eclipse.swt.graphics.PaletteData;
96 import org.eclipse.swt.layout.FillLayout;
97 import org.eclipse.swt.layout.GridData;
98 import org.eclipse.swt.layout.GridLayout;
99 import org.eclipse.swt.widgets.Composite;
100 import org.eclipse.ui.IEditorInput;
101 import org.eclipse.ui.PartInitException;
102 import org.eclipse.ui.ide.IDE;
103 import org.eclipse.ui.part.FileEditorInput;
105 import java.awt.image.BufferedImage;
106 import java.awt.image.DataBufferInt;
107 import java.awt.image.Raster;
109 import java.io.FileOutputStream;
110 import java.io.IOException;
111 import java.io.InputStream;
112 import java.io.PrintStream;
113 import java.util.ArrayList;
114 import java.util.HashMap;
115 import java.util.List;
116 import java.util.Map;
119 * Graphical layout editor, based on GEF.
121 * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/
123 * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
127 public class GraphicalLayoutEditor extends GraphicalEditorWithPalette
128 implements IGraphicalLayoutEditor, IConfigListener, ILayoutReloadListener, IOutlineProvider {
131 /** Reference to the layout editor */
132 private final LayoutEditor mLayoutEditor;
134 /** reference to the file being edited. */
135 private IFile mEditedFile;
137 private Clipboard mClipboard;
138 private Composite mParent;
139 private ConfigurationComposite mConfigComposite;
141 private PaletteRoot mPaletteRoot;
143 /** The {@link FolderConfiguration} being edited. */
144 private FolderConfiguration mEditedConfig;
146 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
147 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
148 private ProjectCallback mProjectCallback;
149 private ILayoutLog mLogger;
151 private boolean mUseExplodeMode;
152 private boolean mUseOutlineMode;
153 private boolean mNeedsXmlReload = false;
154 private boolean mNeedsRecompute = false;
156 /** Listener to update the root node if the target of the file is changed because of a
157 * SDK location change or a project target change */
158 private ITargetChangeListener mTargetListener = new TargetChangeListener() {
160 public IProject getProject() {
161 return getLayoutEditor().getProject();
165 public void reload() {
166 // because the SDK changed we must reset the configured framework resource.
167 mConfiguredFrameworkRes = null;
169 mConfigComposite.updateUIFromResources();
171 // updateUiFromFramework will reset language/region combo, so we must call
172 // setConfiguration after, or the settext on language/region will be lost.
173 if (mEditedConfig != null) {
174 setConfiguration(mEditedConfig, false /*force*/);
177 // make sure we remove the custom view loader, since its parent class loader is the
178 // bridge class loader.
179 mProjectCallback = null;
185 private final Runnable mConditionalRecomputeRunnable = new Runnable() {
187 if (mLayoutEditor.isGraphicalEditorActive()) {
190 mNeedsRecompute = true;
195 private final Runnable mUiUpdateFromResourcesRunnable = new Runnable() {
197 mConfigComposite.updateUIFromResources();
201 public GraphicalLayoutEditor(LayoutEditor layoutEditor) {
202 mLayoutEditor = layoutEditor;
203 setEditDomain(new DefaultEditDomain(this));
204 setPartName("Layout");
206 AdtPlugin.getDefault().addTargetListener(mTargetListener);
209 // ------------------------------------
210 // Methods overridden from base classes
211 //------------------------------------
214 public void createPartControl(Composite parent) {
218 mClipboard = new Clipboard(parent.getDisplay());
220 parent.setLayout(gl = new GridLayout(1, false));
221 gl.marginHeight = gl.marginWidth = 0;
223 // create the top part for the configuration control
225 CustomToggle[] toggles = new CustomToggle[] {
229 "Displays extra margins in the layout."
232 public void onSelected(boolean newState) {
233 mUseExplodeMode = newState;
240 "Shows the outline of all views in the layout."
243 public void onSelected(boolean newState) {
244 mUseOutlineMode = newState;
250 mConfigComposite = new ConfigurationComposite(this, toggles, parent, SWT.NONE);
252 // create a new composite that will contain the standard editor controls.
253 Composite editorParent = new Composite(parent, SWT.NONE);
254 editorParent.setLayoutData(new GridData(GridData.FILL_BOTH));
255 editorParent.setLayout(new FillLayout());
256 super.createPartControl(editorParent);
260 public void dispose() {
261 if (mTargetListener != null) {
262 AdtPlugin.getDefault().removeTargetListener(mTargetListener);
263 mTargetListener = null;
266 LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this);
268 if (mClipboard != null) {
269 mClipboard.dispose();
277 * Returns the selection synchronizer object.
278 * The synchronizer can be used to sync the selection of 2 or more EditPartViewers.
280 * This is changed from protected to public so that the outline can use it.
282 * @return the synchronizer
285 public SelectionSynchronizer getSelectionSynchronizer() {
286 return super.getSelectionSynchronizer();
290 * Returns the edit domain.
292 * This is changed from protected to public so that the outline can use it.
294 * @return the edit domain
297 public DefaultEditDomain getEditDomain() {
298 return super.getEditDomain();
302 * Creates the palette root.
305 protected PaletteRoot getPaletteRoot() {
306 mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot,
307 mLayoutEditor.getTargetData());
311 public Clipboard getClipboard() {
316 * Save operation in the Graphical Layout Editor.
318 * In our workflow, the model is owned by the Structured XML Editor.
319 * The graphical layout editor just displays it -- thus we don't really
320 * save anything here.
322 * This must NOT call the parent editor part. At the contrary, the parent editor
323 * part will call this *after* having done the actual save operation.
325 * The only action this editor must do is mark the undo command stack as
326 * being no longer dirty.
329 public void doSave(IProgressMonitor monitor) {
330 getCommandStack().markSaveLocation();
331 firePropertyChange(PROP_DIRTY);
335 protected void configurePaletteViewer() {
336 super.configurePaletteViewer();
338 // Create a drag source listener on an edit part that is a viewer.
339 // What this does is use DND with a TemplateTransfer type which is actually
340 // the PaletteTemplateEntry held in the PaletteRoot.
341 TemplateTransferDragSourceListener dragSource =
342 new TemplateTransferDragSourceListener(getPaletteViewer());
344 // Create a drag source on the palette viewer.
345 // See the drag target associated with the GraphicalViewer in configureGraphicalViewer.
346 getPaletteViewer().addDragSourceListener(dragSource);
350 * Configure the graphical viewer before it receives its contents.
353 protected void configureGraphicalViewer() {
354 super.configureGraphicalViewer();
356 GraphicalViewer viewer = getGraphicalViewer();
357 viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay(), this));
358 viewer.setRootEditPart(new ScalableFreeformRootEditPart());
360 // Disable the following -- we don't drag *from* the GraphicalViewer yet:
361 // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
363 viewer.addDropTargetListener(new DropListener(viewer));
366 class DropListener extends TemplateTransferDropTargetListener {
367 public DropListener(EditPartViewer viewer) {
373 protected CreationFactory getFactory(final Object template) {
374 return new CreationFactory() {
375 public Object getNewObject() {
376 // We don't know the newly created EditPart since "creating" new
377 // elements is done by ElementCreateCommand.execute() directly by
378 // manipulating the XML elements..
382 public Object getObjectType() {
391 * Set the contents of the GraphicalViewer after it has been created.
394 protected void initializeGraphicalViewer() {
395 GraphicalViewer viewer = getGraphicalViewer();
396 viewer.setContents(getModel());
398 IEditorInput input = getEditorInput();
399 if (input instanceof FileEditorInput) {
400 FileEditorInput fileInput = (FileEditorInput)input;
401 mEditedFile = fileInput.getFile();
403 mConfigComposite.updateUIFromResources();
405 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this);
407 // really this shouldn't happen! Log it in case it happens
409 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
415 * Sets the graphicalViewer for this EditorPart.
416 * @param viewer the graphical viewer
419 protected void setGraphicalViewer(GraphicalViewer viewer) {
420 super.setGraphicalViewer(viewer);
422 // TODO: viewer.setKeyHandler()
423 viewer.setContextMenu(createContextMenu(viewer));
427 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node
428 * created by {@link ElementCreateCommand#execute()}.
430 * @param uiNodeModel The {@link UiElementNode} to select.
432 public void selectModel(UiElementNode uiNodeModel) {
433 GraphicalViewer viewer = getGraphicalViewer();
435 // Give focus to the graphical viewer (in case the outline has it)
436 viewer.getControl().forceFocus();
438 Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);
440 if (editPart instanceof EditPart) {
441 viewer.select((EditPart)editPart);
450 public LayoutEditor getLayoutEditor() {
451 return mLayoutEditor;
454 private MenuManager createContextMenu(GraphicalViewer viewer) {
455 MenuManager menuManager = new MenuManager();
456 menuManager.setRemoveAllWhenShown(true);
457 menuManager.addMenuListener(new ActionMenuListener(viewer));
462 private class ActionMenuListener implements IMenuListener {
463 private final GraphicalViewer mViewer;
465 public ActionMenuListener(GraphicalViewer viewer) {
470 * The menu is about to be shown. The menu manager has already been
471 * requested to remove any existing menu item. This method gets the
472 * tree selection and if it is of the appropriate type it re-creates
473 * the necessary actions.
475 public void menuAboutToShow(IMenuManager manager) {
476 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
478 // filter selected items and only keep those we can handle
479 for (Object obj : mViewer.getSelectedEditParts()) {
480 if (obj instanceof UiElementEditPart) {
481 UiElementEditPart part = (UiElementEditPart) obj;
482 UiElementNode uiNode = part.getUiNode();
483 if (uiNode != null) {
484 selected.add(uiNode);
489 if (selected.size() > 0) {
490 doCreateMenuAction(manager, mViewer, selected);
495 private void doCreateMenuAction(IMenuManager manager,
496 final GraphicalViewer viewer,
497 final ArrayList<UiElementNode> selected) {
498 if (selected != null) {
499 boolean hasXml = false;
500 for (UiElementNode uiNode : selected) {
501 if (uiNode.getXmlNode() != null) {
508 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
509 null, selected, true /* cut */));
510 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
511 null, selected, false /* cut */));
513 // Can't paste with more than one element selected (the selection is the target)
514 if (selected.size() <= 1) {
515 // Paste is not valid if it would add a second element on a terminal element
516 // which parent is a document -- an XML document can only have one child. This
517 // means paste is valid if the current UI node can have children or if the
518 // parent is not a document.
519 UiElementNode ui_root = selected.get(0).getUiRoot();
520 if (ui_root.getDescriptor().hasChildren() ||
521 !(ui_root.getUiParent() instanceof UiDocumentNode)) {
522 manager.add(new PasteAction(mLayoutEditor, getClipboard(),
526 manager.add(new Separator());
530 // Append "add" and "remove" actions. They do the same thing as the add/remove
531 // buttons on the side.
532 IconFactory factory = IconFactory.getInstance();
534 final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions();
536 // "Add" makes sense only if there's 0 or 1 item selected since the
537 // one selected item becomes the target.
538 if (selected == null || selected.size() <= 1) {
539 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$
542 UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0)
544 uiActions.doAdd(node, viewer.getControl().getShell());
549 if (selected != null) {
550 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$
553 uiActions.doRemove(selected, viewer.getControl().getShell());
557 manager.add(new Separator());
559 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$
562 uiActions.doUp(selected);
565 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$
568 uiActions.doDown(selected);
576 * Sets the UI for the edition of a new file.
577 * @param configuration the configuration of the new file.
579 public void editNewFile(FolderConfiguration configuration) {
580 // update the configuration UI
581 setConfiguration(configuration, true /*force*/);
583 // enable the create button if the current and edited config are not equals
584 mConfigComposite.setEnabledCreate(
585 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false);
587 reloadConfigurationUi(false /*notifyListener*/);
590 public Rectangle getBounds() {
591 return mConfigComposite.getScreenBounds();
595 * Renders an Android View described by a {@link ViewElementDescriptor}.
596 * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and
597 * <code>layout_height</code>, and use the class name for the <code>text</code> attribute.
598 * @param descriptor the descriptor for the class to render.
599 * @return an ImageData containing the rendering or <code>null</code> if rendering failed.
601 public ImageData renderWidget(ViewElementDescriptor descriptor) {
602 if (mEditedFile == null) {
606 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
607 if (target == null) {
611 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
616 LayoutBridge bridge = data.getLayoutBridge();
618 if (bridge.bridge != null) { // bridge can never be null.
619 ResourceManager resManager = ResourceManager.getInstance();
621 ProjectCallback projectCallback = null;
622 Map<String, Map<String, IResourceValue>> configuredProjectResources = null;
623 if (mEditedFile != null) {
624 ProjectResources projectRes = resManager.getProjectResources(
625 mEditedFile.getProject());
626 projectCallback = new ProjectCallback(bridge.classLoader,
627 projectRes, mEditedFile.getProject());
629 // get the configured resources for the project
630 // get the resources of the file's project.
631 if (mConfiguredProjectRes == null && projectRes != null) {
632 // make sure they are loaded
633 projectRes.loadAll();
635 // get the project resource values based on the current config
636 mConfiguredProjectRes = projectRes.getConfiguredResources(
637 mConfigComposite.getCurrentConfig());
640 configuredProjectResources = mConfiguredProjectRes;
642 // we absolutely need a Map of configured project resources.
643 configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>();
646 // get the framework resources
647 Map<String, Map<String, IResourceValue>> frameworkResources =
648 getConfiguredFrameworkResources();
650 if (configuredProjectResources != null && frameworkResources != null) {
651 // get the selected theme
652 String theme = mConfigComposite.getTheme();
654 // Render a single object as described by the ViewElementDescriptor.
655 WidgetPullParser parser = new WidgetPullParser(descriptor);
656 ILayoutResult result = computeLayout(bridge, parser,
657 null /* projectKey */,
658 1 /* width */, 1 /* height */, true /* renderFullSize */,
659 160 /*density*/, 160.f /*xdpi*/, 160.f /*ydpi*/, theme,
660 mConfigComposite.isProjectTheme(),
661 configuredProjectResources, frameworkResources, projectCallback,
664 // update the UiElementNode with the layout info.
665 if (result.getSuccess() == ILayoutResult.SUCCESS) {
666 BufferedImage largeImage = result.getImage();
668 // we need to resize it to the actual widget size, and convert it into
669 // an SWT image object.
670 int width = result.getRootView().getRight();
671 int height = result.getRootView().getBottom();
672 Raster raster = largeImage.getData(new java.awt.Rectangle(width, height));
673 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
675 ImageData imageData = new ImageData(width, height, 32,
676 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
678 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
689 * Reloads this editor, by getting the new model from the {@link LayoutEditor}.
691 public void reloadEditor() {
692 GraphicalViewer viewer = getGraphicalViewer();
693 viewer.setContents(getModel());
695 IEditorInput input = mLayoutEditor.getEditorInput();
698 if (input instanceof FileEditorInput) {
699 FileEditorInput fileInput = (FileEditorInput)input;
700 mEditedFile = fileInput.getFile();
702 // really this shouldn't happen! Log it in case it happens
704 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
710 * Callback for XML model changed. Only update/recompute the layout if the editor is visible
712 public void onXmlModelChanged() {
713 if (mLayoutEditor.isGraphicalEditorActive()) {
714 doXmlReload(true /* force */);
717 mNeedsXmlReload = true;
722 * Actually performs the XML reload
723 * @see #onXmlModelChanged()
725 private void doXmlReload(boolean force) {
726 if (force || mNeedsXmlReload) {
727 GraphicalViewer viewer = getGraphicalViewer();
729 // try to preserve the selection before changing the content
730 SelectionManager selMan = viewer.getSelectionManager();
731 ISelection selection = selMan.getSelection();
734 viewer.setContents(getModel());
736 selMan.setSelection(selection);
739 mNeedsXmlReload = false;
744 * Update the UI controls state with a given {@link FolderConfiguration}.
745 * <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect
746 * <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>,
747 * the UI control is not modified. However if the value in the control is not the default value,
748 * a warning icon is shown.
749 * @param config The {@link FolderConfiguration} to set.
750 * @param force Whether the UI should be changed to exactly match the received configuration.
752 void setConfiguration(FolderConfiguration config, boolean force) {
753 mEditedConfig = config;
754 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
756 mConfigComposite.setConfiguration(config, force);
761 public UiDocumentNode getModel() {
762 return mLayoutEditor.getUiRootNode();
765 public void reloadPalette() {
766 PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData());
769 public void reloadConfigurationUi(boolean notifyListener) {
770 // enable the clipping button if it's supported.
771 Sdk currentSdk = Sdk.getCurrent();
772 if (currentSdk != null) {
773 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
774 AndroidTargetData data = currentSdk.getTargetData(target);
776 LayoutBridge bridge = data.getLayoutBridge();
777 mConfigComposite.reloadDevices(notifyListener);
778 mConfigComposite.setClippingSupport(bridge.apiLevel >= 4);
784 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.
785 * <p/>If there is no match, notify the user.
787 public void onConfigurationChange() {
788 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
790 if (mEditedFile == null || mEditedConfig == null) {
794 // get the resources of the file's project.
795 ProjectResources resources = ResourceManager.getInstance().getProjectResources(
796 mEditedFile.getProject());
798 // from the resources, look for a matching file
799 ResourceFile match = null;
800 if (resources != null) {
801 match = resources.getMatchingFile(mEditedFile.getName(),
802 ResourceFolderType.LAYOUT,
803 mConfigComposite.getCurrentConfig());
807 if (match.getFile().equals(mEditedFile) == false) {
810 getSite().getWorkbenchWindow().getActivePage(),
811 match.getFile().getIFile());
815 } catch (PartInitException e) {
816 // FIXME: do something!
820 // at this point, we have not opened a new file.
822 // update the configuration icons with the new edited config.
823 setConfiguration(mEditedConfig, false /*force*/);
825 // enable the create button if the current and edited config are not equals
826 mConfigComposite.setEnabledCreate(
827 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false);
829 // Even though the layout doesn't change, the config changed, and referenced
830 // resources need to be updated.
833 // enable the Create button
834 mConfigComposite.setEnabledCreate(true);
836 // display the error.
837 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();
838 String message = String.format(
839 "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.",
840 currentConfig.toDisplayString(),
841 currentConfig.getFolderName(ResourceFolderType.LAYOUT,
842 Sdk.getCurrent().getTarget(mEditedFile.getProject())),
843 mEditedFile.getName());
844 showErrorInEditor(message);
848 public void onThemeChange() {
852 public void onClippingChange() {
857 public void onCreate() {
858 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mParent.getShell(),
859 mEditedFile.getName(),
860 Sdk.getCurrent().getTarget(mEditedFile.getProject()),
861 mConfigComposite.getCurrentConfig());
862 if (dialog.open() == Dialog.OK) {
863 final FolderConfiguration config = new FolderConfiguration();
864 dialog.getConfiguration(config);
866 createAlternateLayout(config);
871 * Recomputes the layout with the help of layoutlib.
873 public void recomputeLayout() {
874 doXmlReload(false /* force */);
876 // check that the resource exists. If the file is opened but the project is closed
877 // or deleted for some reason (changed from outside of eclipse), then this will
879 if (mEditedFile.exists() == false) {
880 String message = String.format("Resource '%1$s' does not exist.",
881 mEditedFile.getFullPath().toString());
883 showErrorInEditor(message);
888 IProject iProject = mEditedFile.getProject();
890 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
891 String message = String.format("%1$s is out of sync. Please refresh.",
892 mEditedFile.getName());
894 showErrorInEditor(message);
896 // also print it in the error console.
897 AdtPlugin.printErrorToConsole(iProject.getName(), message);
901 Sdk currentSdk = Sdk.getCurrent();
902 if (currentSdk != null) {
903 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
904 if (target == null) {
905 showErrorInEditor("The project target is not set.");
909 AndroidTargetData data = currentSdk.getTargetData(target);
911 // It can happen that the workspace refreshes while the SDK is loading its
912 // data, which could trigger a redraw of the opened layout if some resources
913 // changed while Eclipse is closed.
914 // In this case data could be null, but this is not an error.
915 // We can just silently return, as all the opened editors are automatically
916 // refreshed once the SDK finishes loading.
917 if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) {
918 showErrorInEditor(String.format(
919 "The project target (%s) was not properly loaded.",
925 // check there is actually a model (maybe the file is empty).
926 UiDocumentNode model = getModel();
928 if (model.getUiChildren().size() == 0) {
929 showErrorInEditor("No Xml content. Go to the Outline view and add nodes.");
933 LayoutBridge bridge = data.getLayoutBridge();
935 if (bridge.bridge != null) { // bridge can never be null.
936 ResourceManager resManager = ResourceManager.getInstance();
938 ProjectResources projectRes = resManager.getProjectResources(iProject);
939 if (projectRes == null) {
943 // get the resources of the file's project.
944 Map<String, Map<String, IResourceValue>> configuredProjectRes =
945 getConfiguredProjectResources();
947 // get the framework resources
948 Map<String, Map<String, IResourceValue>> frameworkResources =
949 getConfiguredFrameworkResources();
951 if (configuredProjectRes != null && frameworkResources != null) {
952 if (mProjectCallback == null) {
953 mProjectCallback = new ProjectCallback(
954 bridge.classLoader, projectRes, iProject);
957 if (mLogger == null) {
958 mLogger = new ILayoutLog() {
959 public void error(String message) {
960 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);
963 public void error(Throwable error) {
964 String message = error.getMessage();
965 if (message == null) {
966 message = error.getClass().getName();
969 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());
970 error.printStackTrace(ps);
973 public void warning(String message) {
974 AdtPlugin.printToConsole(mEditedFile.getName(), message);
979 // get the selected theme
980 String theme = mConfigComposite.getTheme();
982 // Compute the layout
983 Rectangle rect = getBounds();
985 int width = rect.width;
986 int height = rect.height;
987 if (mUseExplodeMode) {
988 // compute how many padding in x and y will bump the screen size
989 ExplodedRenderingHelper helper = new ExplodedRenderingHelper(
990 getModel(), iProject);
992 // there are 2 paddings for each view
993 // left and right, or top and bottom.
994 int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;
996 width += helper.getWidthPadding() * paddingValue;
997 height += helper.getHeightPadding() * paddingValue;
1000 int density = mConfigComposite.getDensity().getDpiValue();
1001 float xdpi = mConfigComposite.getXDpi();
1002 float ydpi = mConfigComposite.getYDpi();
1003 boolean isProjectTheme = mConfigComposite.isProjectTheme();
1005 UiElementPullParser parser = new UiElementPullParser(getModel(),
1006 mUseExplodeMode, density, xdpi, iProject);
1008 ILayoutResult result = computeLayout(bridge, parser,
1009 iProject /* projectKey */,
1010 width, height, !mConfigComposite.getClipping(),
1011 density, xdpi, ydpi,
1012 theme, isProjectTheme,
1013 configuredProjectRes, frameworkResources, mProjectCallback,
1016 // update the UiElementNode with the layout info.
1017 if (result.getSuccess() == ILayoutResult.SUCCESS) {
1018 model.setEditData(result.getImage());
1020 updateNodeWithBounds(result.getRootView());
1022 String message = result.getErrorMessage();
1024 // Reset the edit data for all the nodes.
1025 resetNodeBounds(model);
1027 if (message != null) {
1028 // set the error in the top element.
1029 model.setEditData(message);
1037 // SDK is loaded but not the layout library!
1038 String message = null;
1039 // check whether the bridge managed to load, or not
1040 if (bridge.status == LoadStatus.LOADING) {
1041 message = String.format(
1042 "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
1043 mEditedFile.getName());
1045 message = String.format("Eclipse failed to load the framework information and the Layout library!");
1047 showErrorInEditor(message);
1050 String message = String.format(
1051 "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
1052 mEditedFile.getName());
1054 showErrorInEditor(message);
1057 // no matter the result, we are done doing the recompute based on the latest
1058 // resource/code change.
1059 mNeedsRecompute = false;
1063 private void showErrorInEditor(String message) {
1064 // get the model to display the error directly in the editor
1065 UiDocumentNode model = getModel();
1067 // Reset the edit data for all the nodes.
1068 resetNodeBounds(model);
1070 if (message != null) {
1071 // set the error in the top element.
1072 model.setEditData(message);
1078 private void resetNodeBounds(UiElementNode node) {
1079 node.setEditData(null);
1081 List<UiElementNode> children = node.getUiChildren();
1082 for (UiElementNode child : children) {
1083 resetNodeBounds(child);
1087 private void updateNodeWithBounds(ILayoutViewInfo r) {
1089 // update the node itself, as the viewKey is the XML node in this implementation.
1090 Object viewKey = r.getViewKey();
1091 if (viewKey instanceof UiElementNode) {
1092 Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(),
1093 r.getRight()-r.getLeft(), r.getBottom() - r.getTop());
1095 ((UiElementNode)viewKey).setEditData(bounds);
1098 // and then its children.
1099 ILayoutViewInfo[] children = r.getChildren();
1100 if (children != null) {
1101 for (ILayoutViewInfo child : children) {
1102 updateNodeWithBounds(child);
1110 * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean)
1112 * Called when the file changes triggered a redraw of the layout
1114 public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) {
1115 boolean recompute = rChange;
1120 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.
1122 // force a reparse in case a value XML file changed.
1123 mConfiguredProjectRes = null;
1125 // clear the cache in the bridge in case a bitmap/9-patch changed.
1126 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
1127 if (target != null) {
1129 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
1131 LayoutBridge bridge = data.getLayoutBridge();
1133 if (bridge.bridge != null) {
1134 bridge.bridge.clearCaches(mEditedFile.getProject());
1139 mParent.getDisplay().asyncExec(mUiUpdateFromResourcesRunnable);
1143 // only recompute if the custom view loader was used to load some code.
1144 if (mProjectCallback != null && mProjectCallback.isUsed()) {
1145 mProjectCallback = null;
1151 mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable);
1156 * Responds to a page change that made the Graphical editor page the activated page.
1158 public void activated() {
1159 if (mNeedsRecompute || mNeedsXmlReload) {
1165 * Responds to a page change that made the Graphical editor page the deactivated page
1167 public void deactivated() {
1168 // nothing to be done here for now.
1171 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {
1172 if (mConfiguredFrameworkRes == null) {
1173 ProjectResources frameworkRes = getFrameworkResources();
1175 if (frameworkRes == null) {
1176 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
1178 // get the framework resource values based on the current config
1179 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
1180 mConfigComposite.getCurrentConfig());
1184 return mConfiguredFrameworkRes;
1187 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {
1188 if (mConfiguredProjectRes == null) {
1189 ProjectResources project = getProjectResources();
1191 // make sure they are loaded
1194 // get the project resource values based on the current config
1195 mConfiguredProjectRes = project.getConfiguredResources(
1196 mConfigComposite.getCurrentConfig());
1199 return mConfiguredProjectRes;
1203 * Returns a {@link ProjectResources} for the framework resources.
1204 * @return the framework resources or null if not found.
1206 public ProjectResources getFrameworkResources() {
1207 if (mEditedFile != null) {
1208 Sdk currentSdk = Sdk.getCurrent();
1209 if (currentSdk != null) {
1210 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
1212 if (target != null) {
1213 AndroidTargetData data = currentSdk.getTargetData(target);
1216 return data.getFrameworkResources();
1225 public ProjectResources getProjectResources() {
1226 if (mEditedFile != null) {
1227 ResourceManager manager = ResourceManager.getInstance();
1228 return manager.getProjectResources(mEditedFile.getProject());
1235 * Creates a new layout file from the specified {@link FolderConfiguration}.
1237 private void createAlternateLayout(final FolderConfiguration config) {
1238 new Job("Create Alternate Resource") {
1240 protected IStatus run(IProgressMonitor monitor) {
1241 // get the folder name
1242 String folderName = config.getFolderName(ResourceFolderType.LAYOUT,
1243 Sdk.getCurrent().getTarget(mEditedFile.getProject()));
1246 // look to see if it exists.
1247 // get the res folder
1248 IFolder res = (IFolder)mEditedFile.getParent().getParent();
1249 String path = res.getLocation().toOSString();
1251 File newLayoutFolder = new File(path + File.separator + folderName);
1252 if (newLayoutFolder.isFile()) {
1253 // this should not happen since aapt would have complained
1254 // before, but if one disable the automatic build, this could
1256 String message = String.format("File 'res/%1$s' is in the way!",
1259 AdtPlugin.displayError("Layout Creation", message);
1261 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
1262 } else if (newLayoutFolder.exists() == false) {
1264 newLayoutFolder.mkdir();
1267 // now create the file
1268 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
1269 File.separator + mEditedFile.getName());
1271 newLayoutFile.createNewFile();
1273 InputStream input = mEditedFile.getContents();
1275 FileOutputStream fos = new FileOutputStream(newLayoutFile);
1277 byte[] data = new byte[512];
1279 while ((count = input.read(data)) != -1) {
1280 fos.write(data, 0, count);
1286 // refreshes the res folder to show up the new
1287 // layout folder (if needed) and the file.
1288 // We use a progress monitor to catch the end of the refresh
1289 // to trigger the edit of the new file.
1290 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
1291 public void done() {
1292 mParent.getDisplay().asyncExec(new Runnable() {
1294 onConfigurationChange();
1299 public void beginTask(String name, int totalWork) {
1303 public void internalWorked(double work) {
1307 public boolean isCanceled() {
1312 public void setCanceled(boolean value) {
1316 public void setTaskName(String name) {
1320 public void subTask(String name) {
1324 public void worked(int work) {
1328 } catch (IOException e2) {
1329 String message = String.format(
1330 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1331 folderName, mEditedFile.getName(), e2.getMessage());
1333 AdtPlugin.displayError("Layout Creation", message);
1335 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
1337 } catch (CoreException e2) {
1338 String message = String.format(
1339 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1340 folderName, mEditedFile.getName(), e2.getMessage());
1342 AdtPlugin.displayError("Layout Creation", message);
1344 return e2.getStatus();
1347 return Status.OK_STATUS;
1354 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
1355 * the implementation API level.
1357 @SuppressWarnings("deprecation")
1358 private static ILayoutResult computeLayout(LayoutBridge bridge,
1359 IXmlPullParser layoutDescription, Object projectKey,
1360 int screenWidth, int screenHeight, boolean renderFullSize,
1361 int density, float xdpi, float ydpi,
1362 String themeName, boolean isProjectTheme,
1363 Map<String, Map<String, IResourceValue>> projectResources,
1364 Map<String, Map<String, IResourceValue>> frameworkResources,
1365 IProjectCallback projectCallback, ILayoutLog logger) {
1367 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {
1368 // newest API with support for "render full height"
1369 // TODO: link boolean to UI.
1370 return bridge.bridge.computeLayout(layoutDescription,
1371 projectKey, screenWidth, screenHeight, renderFullSize,
1372 density, xdpi, ydpi,
1373 themeName, isProjectTheme,
1374 projectResources, frameworkResources, projectCallback,
1376 } else if (bridge.apiLevel == 3) {
1377 // newer api with density support.
1378 return bridge.bridge.computeLayout(layoutDescription,
1379 projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
1380 themeName, isProjectTheme,
1381 projectResources, frameworkResources, projectCallback,
1383 } else if (bridge.apiLevel == 2) {
1384 // api with boolean for separation of project/framework theme
1385 return bridge.bridge.computeLayout(layoutDescription,
1386 projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
1387 projectResources, frameworkResources, projectCallback,
1390 // oldest api with no density/dpi, and project theme boolean mixed
1391 // into the theme name.
1393 // change the string if it's a custom theme to make sure we can
1394 // differentiate them
1395 if (isProjectTheme) {
1396 themeName = "*" + themeName; //$NON-NLS-1$
1399 return bridge.bridge.computeLayout(layoutDescription,
1400 projectKey, screenWidth, screenHeight, themeName,
1401 projectResources, frameworkResources, projectCallback,
1406 public boolean hasOutline() {
1407 return mUseOutlineMode;