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.ChangeFlags;
30 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
31 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
32 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
33 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle;
34 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;
35 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
36 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;
37 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementEditPart;
38 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory;
39 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory.IOutlineProvider;
40 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction;
41 import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction;
42 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
43 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
44 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
45 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
46 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
47 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
48 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
49 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
50 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
51 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
52 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
53 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
54 import com.android.ide.eclipse.adt.io.IFileWrapper;
55 import com.android.layoutlib.api.ILayoutBridge;
56 import com.android.layoutlib.api.ILayoutLog;
57 import com.android.layoutlib.api.ILayoutResult;
58 import com.android.layoutlib.api.IProjectCallback;
59 import com.android.layoutlib.api.IResourceValue;
60 import com.android.layoutlib.api.IXmlPullParser;
61 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
62 import com.android.sdklib.IAndroidTarget;
64 import org.eclipse.core.resources.IFile;
65 import org.eclipse.core.resources.IFolder;
66 import org.eclipse.core.resources.IProject;
67 import org.eclipse.core.resources.IResource;
68 import org.eclipse.core.runtime.CoreException;
69 import org.eclipse.core.runtime.IProgressMonitor;
70 import org.eclipse.core.runtime.IStatus;
71 import org.eclipse.core.runtime.Status;
72 import org.eclipse.core.runtime.jobs.Job;
73 import org.eclipse.draw2d.geometry.Rectangle;
74 import org.eclipse.gef.DefaultEditDomain;
75 import org.eclipse.gef.EditPart;
76 import org.eclipse.gef.EditPartViewer;
77 import org.eclipse.gef.GraphicalViewer;
78 import org.eclipse.gef.SelectionManager;
79 import org.eclipse.gef.dnd.TemplateTransferDragSourceListener;
80 import org.eclipse.gef.dnd.TemplateTransferDropTargetListener;
81 import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
82 import org.eclipse.gef.palette.PaletteRoot;
83 import org.eclipse.gef.requests.CreationFactory;
84 import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette;
85 import org.eclipse.gef.ui.parts.SelectionSynchronizer;
86 import org.eclipse.jface.action.Action;
87 import org.eclipse.jface.action.IMenuListener;
88 import org.eclipse.jface.action.IMenuManager;
89 import org.eclipse.jface.action.MenuManager;
90 import org.eclipse.jface.action.Separator;
91 import org.eclipse.jface.dialogs.Dialog;
92 import org.eclipse.jface.viewers.ISelection;
93 import org.eclipse.swt.SWT;
94 import org.eclipse.swt.dnd.Clipboard;
95 import org.eclipse.swt.graphics.ImageData;
96 import org.eclipse.swt.graphics.PaletteData;
97 import org.eclipse.swt.layout.FillLayout;
98 import org.eclipse.swt.layout.GridData;
99 import org.eclipse.swt.layout.GridLayout;
100 import org.eclipse.swt.widgets.Composite;
101 import org.eclipse.ui.IEditorInput;
102 import org.eclipse.ui.PartInitException;
103 import org.eclipse.ui.ide.IDE;
104 import org.eclipse.ui.part.FileEditorInput;
106 import java.awt.image.BufferedImage;
107 import java.awt.image.DataBufferInt;
108 import java.awt.image.Raster;
110 import java.io.FileOutputStream;
111 import java.io.IOException;
112 import java.io.InputStream;
113 import java.io.PrintStream;
114 import java.util.ArrayList;
115 import java.util.HashMap;
116 import java.util.List;
117 import java.util.Map;
120 * Graphical layout editor, based on GEF.
122 * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/
124 * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
128 public class GraphicalLayoutEditor extends GraphicalEditorWithPalette
129 implements IGraphicalLayoutEditor, IConfigListener, ILayoutReloadListener, IOutlineProvider {
132 /** Reference to the layout editor */
133 private final LayoutEditor mLayoutEditor;
135 /** reference to the file being edited. */
136 private IFile mEditedFile;
138 private Clipboard mClipboard;
139 private Composite mParent;
140 private ConfigurationComposite mConfigComposite;
142 private PaletteRoot mPaletteRoot;
144 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
145 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
146 private ProjectCallback mProjectCallback;
147 private ILayoutLog mLogger;
149 private boolean mUseExplodeMode;
150 private boolean mUseOutlineMode;
151 private boolean mNeedsXmlReload = false;
152 private boolean mNeedsRecompute = false;
154 /** Listener to update the root node if the target of the file is changed because of a
155 * SDK location change or a project target change */
156 private ITargetChangeListener mTargetListener = new ITargetChangeListener() {
157 public void onProjectTargetChange(IProject changedProject) {
158 if (changedProject != null && changedProject.equals(getProject())) {
163 public void onTargetLoaded(IAndroidTarget target) {
164 IProject project = getProject();
165 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
170 public void onSdkLoaded() {
171 Sdk currentSdk = Sdk.getCurrent();
172 if (currentSdk != null) {
173 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
174 if (target != null) {
175 mConfigComposite.onSdkLoaded(target);
176 onConfigurationChange();
181 private void updateEditor() {
182 mLayoutEditor.commitPages(false /* onSave */);
184 // because the target changed we must reset the configured resources.
185 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
187 // make sure we remove the custom view loader, since its parent class loader is the
188 // bridge class loader.
189 mProjectCallback = null;
191 // recreate the ui root node always, this will also call onTargetChange
192 // on the config composite
193 mLayoutEditor.initUiRootNode(true /*force*/);
196 private IProject getProject() {
197 return getLayoutEditor().getProject();
201 private final Runnable mConditionalRecomputeRunnable = new Runnable() {
203 if (mLayoutEditor.isGraphicalEditorActive()) {
206 mNeedsRecompute = true;
211 private final Runnable mLocaleUpdaterFromUiRunnable = new Runnable() {
213 mConfigComposite.updateLocales();
217 public GraphicalLayoutEditor(LayoutEditor layoutEditor) {
218 mLayoutEditor = layoutEditor;
219 setEditDomain(new DefaultEditDomain(this));
220 setPartName("Layout");
222 AdtPlugin.getDefault().addTargetListener(mTargetListener);
225 // ------------------------------------
226 // Methods overridden from base classes
227 //------------------------------------
230 public void createPartControl(Composite parent) {
234 mClipboard = new Clipboard(parent.getDisplay());
236 parent.setLayout(gl = new GridLayout(1, false));
237 gl.marginHeight = gl.marginWidth = 0;
239 // create the top part for the configuration control
241 CustomToggle[] toggles = new CustomToggle[] {
245 "Displays extra margins in the layout."
248 public void onSelected(boolean newState) {
249 mUseExplodeMode = newState;
256 "Shows the outline of all views in the layout."
259 public void onSelected(boolean newState) {
260 mUseOutlineMode = newState;
266 mConfigComposite = new ConfigurationComposite(this, toggles, parent, SWT.NONE);
268 // create a new composite that will contain the standard editor controls.
269 Composite editorParent = new Composite(parent, SWT.NONE);
270 editorParent.setLayoutData(new GridData(GridData.FILL_BOTH));
271 editorParent.setLayout(new FillLayout());
272 super.createPartControl(editorParent);
276 public void dispose() {
277 if (mTargetListener != null) {
278 AdtPlugin.getDefault().removeTargetListener(mTargetListener);
279 mTargetListener = null;
282 LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this);
284 if (mClipboard != null) {
285 mClipboard.dispose();
293 * Returns the selection synchronizer object.
294 * The synchronizer can be used to sync the selection of 2 or more EditPartViewers.
296 * This is changed from protected to public so that the outline can use it.
298 * @return the synchronizer
301 public SelectionSynchronizer getSelectionSynchronizer() {
302 return super.getSelectionSynchronizer();
306 * Returns the edit domain.
308 * This is changed from protected to public so that the outline can use it.
310 * @return the edit domain
313 public DefaultEditDomain getEditDomain() {
314 return super.getEditDomain();
318 * Creates the palette root.
321 protected PaletteRoot getPaletteRoot() {
322 mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot,
323 mLayoutEditor.getTargetData());
327 public Clipboard getClipboard() {
332 * Save operation in the Graphical Layout Editor.
334 * In our workflow, the model is owned by the Structured XML Editor.
335 * The graphical layout editor just displays it -- thus we don't really
336 * save anything here.
338 * This must NOT call the parent editor part. At the contrary, the parent editor
339 * part will call this *after* having done the actual save operation.
341 * The only action this editor must do is mark the undo command stack as
342 * being no longer dirty.
345 public void doSave(IProgressMonitor monitor) {
346 getCommandStack().markSaveLocation();
347 firePropertyChange(PROP_DIRTY);
351 protected void configurePaletteViewer() {
352 super.configurePaletteViewer();
354 // Create a drag source listener on an edit part that is a viewer.
355 // What this does is use DND with a TemplateTransfer type which is actually
356 // the PaletteTemplateEntry held in the PaletteRoot.
357 TemplateTransferDragSourceListener dragSource =
358 new TemplateTransferDragSourceListener(getPaletteViewer());
360 // Create a drag source on the palette viewer.
361 // See the drag target associated with the GraphicalViewer in configureGraphicalViewer.
362 getPaletteViewer().addDragSourceListener(dragSource);
366 * Configure the graphical viewer before it receives its contents.
369 protected void configureGraphicalViewer() {
370 super.configureGraphicalViewer();
372 GraphicalViewer viewer = getGraphicalViewer();
373 viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay(), this));
374 viewer.setRootEditPart(new ScalableFreeformRootEditPart());
376 // Disable the following -- we don't drag *from* the GraphicalViewer yet:
377 // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
379 viewer.addDropTargetListener(new DropListener(viewer));
382 class DropListener extends TemplateTransferDropTargetListener {
383 public DropListener(EditPartViewer viewer) {
389 protected CreationFactory getFactory(final Object template) {
390 return new CreationFactory() {
391 public Object getNewObject() {
392 // We don't know the newly created EditPart since "creating" new
393 // elements is done by ElementCreateCommand.execute() directly by
394 // manipulating the XML elements..
398 public Object getObjectType() {
407 * Set the contents of the GraphicalViewer after it has been created.
410 protected void initializeGraphicalViewer() {
411 GraphicalViewer viewer = getGraphicalViewer();
412 viewer.setContents(getModel());
414 IEditorInput input = getEditorInput();
415 if (input instanceof FileEditorInput) {
416 FileEditorInput fileInput = (FileEditorInput)input;
417 mEditedFile = fileInput.getFile();
419 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this);
421 // really this shouldn't happen! Log it in case it happens
423 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
429 * Sets the graphicalViewer for this EditorPart.
430 * @param viewer the graphical viewer
433 protected void setGraphicalViewer(GraphicalViewer viewer) {
434 super.setGraphicalViewer(viewer);
436 // TODO: viewer.setKeyHandler()
437 viewer.setContextMenu(createContextMenu(viewer));
441 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node
442 * created by {@link ElementCreateCommand#execute()}.
444 * @param uiNodeModel The {@link UiElementNode} to select.
446 public void selectModel(UiElementNode uiNodeModel) {
447 GraphicalViewer viewer = getGraphicalViewer();
449 // Give focus to the graphical viewer (in case the outline has it)
450 viewer.getControl().forceFocus();
452 Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);
454 if (editPart instanceof EditPart) {
455 viewer.select((EditPart)editPart);
464 public LayoutEditor getLayoutEditor() {
465 return mLayoutEditor;
468 private MenuManager createContextMenu(GraphicalViewer viewer) {
469 MenuManager menuManager = new MenuManager();
470 menuManager.setRemoveAllWhenShown(true);
471 menuManager.addMenuListener(new ActionMenuListener(viewer));
476 private class ActionMenuListener implements IMenuListener {
477 private final GraphicalViewer mViewer;
479 public ActionMenuListener(GraphicalViewer viewer) {
484 * The menu is about to be shown. The menu manager has already been
485 * requested to remove any existing menu item. This method gets the
486 * tree selection and if it is of the appropriate type it re-creates
487 * the necessary actions.
489 public void menuAboutToShow(IMenuManager manager) {
490 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
492 // filter selected items and only keep those we can handle
493 for (Object obj : mViewer.getSelectedEditParts()) {
494 if (obj instanceof UiElementEditPart) {
495 UiElementEditPart part = (UiElementEditPart) obj;
496 UiElementNode uiNode = part.getUiNode();
497 if (uiNode != null) {
498 selected.add(uiNode);
503 if (selected.size() > 0) {
504 doCreateMenuAction(manager, mViewer, selected);
509 private void doCreateMenuAction(IMenuManager manager,
510 final GraphicalViewer viewer,
511 final ArrayList<UiElementNode> selected) {
512 if (selected != null) {
513 boolean hasXml = false;
514 for (UiElementNode uiNode : selected) {
515 if (uiNode.getXmlNode() != null) {
522 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
523 null, selected, true /* cut */));
524 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
525 null, selected, false /* cut */));
527 // Can't paste with more than one element selected (the selection is the target)
528 if (selected.size() <= 1) {
529 // Paste is not valid if it would add a second element on a terminal element
530 // which parent is a document -- an XML document can only have one child. This
531 // means paste is valid if the current UI node can have children or if the
532 // parent is not a document.
533 UiElementNode ui_root = selected.get(0).getUiRoot();
534 if (ui_root.getDescriptor().hasChildren() ||
535 !(ui_root.getUiParent() instanceof UiDocumentNode)) {
536 manager.add(new PasteAction(mLayoutEditor, getClipboard(),
540 manager.add(new Separator());
544 // Append "add" and "remove" actions. They do the same thing as the add/remove
545 // buttons on the side.
546 IconFactory factory = IconFactory.getInstance();
548 final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions();
550 // "Add" makes sense only if there's 0 or 1 item selected since the
551 // one selected item becomes the target.
552 if (selected == null || selected.size() <= 1) {
553 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$
556 UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0)
558 uiActions.doAdd(node, viewer.getControl().getShell());
563 if (selected != null) {
564 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$
567 uiActions.doRemove(selected, viewer.getControl().getShell());
571 manager.add(new Separator());
573 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$
576 uiActions.doUp(selected);
579 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$
582 uiActions.doDown(selected);
590 * Opens and initialize the editor with a new file.
591 * @param file the file being edited.
593 public void openFile(IFile file) {
595 mConfigComposite.setFile(mEditedFile);
599 * Resets the editor with a replacement file.
600 * @param file the replacement file.
602 public void replaceFile(IFile file) {
606 mConfigComposite.replaceFile(mEditedFile);
610 * Resets the editor with a replacement file coming from a config change in the config
612 * @param file the replacement file.
614 public void changeFileOnNewConfig(IFile file) {
618 mConfigComposite.changeFileOnNewConfig(mEditedFile);
621 public void onTargetChange() {
623 mConfigComposite.onXmlModelLoaded();
624 onConfigurationChange();
627 public void onSdkChange() {
628 Sdk currentSdk = Sdk.getCurrent();
629 if (currentSdk != null) {
630 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
631 if (target != null) {
632 mConfigComposite.onSdkLoaded(target);
633 onConfigurationChange();
639 * Resets the editor's input and the viewer model.
641 private void resetInput() {
642 GraphicalViewer viewer = getGraphicalViewer();
643 viewer.setContents(getModel());
645 IEditorInput input = mLayoutEditor.getEditorInput();
650 public Rectangle getBounds() {
651 return mConfigComposite.getScreenBounds();
655 * Renders an Android View described by a {@link ViewElementDescriptor}.
656 * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and
657 * <code>layout_height</code>, and use the class name for the <code>text</code> attribute.
658 * @param descriptor the descriptor for the class to render.
659 * @return an ImageData containing the rendering or <code>null</code> if rendering failed.
661 public ImageData renderWidget(ViewElementDescriptor descriptor) {
662 if (mEditedFile == null) {
666 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
667 if (target == null) {
671 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
676 LayoutBridge bridge = data.getLayoutBridge();
678 if (bridge.bridge != null) { // bridge can never be null.
679 ResourceManager resManager = ResourceManager.getInstance();
681 ProjectCallback projectCallback = null;
682 Map<String, Map<String, IResourceValue>> configuredProjectResources = null;
683 if (mEditedFile != null) {
684 ProjectResources projectRes = resManager.getProjectResources(
685 mEditedFile.getProject());
686 projectCallback = new ProjectCallback(bridge.classLoader,
687 projectRes, mEditedFile.getProject());
689 // get the configured resources for the project
690 // get the resources of the file's project.
691 if (mConfiguredProjectRes == null && projectRes != null) {
692 // make sure they are loaded
693 projectRes.loadAll();
695 // get the project resource values based on the current config
696 mConfiguredProjectRes = projectRes.getConfiguredResources(
697 mConfigComposite.getCurrentConfig());
700 configuredProjectResources = mConfiguredProjectRes;
702 // we absolutely need a Map of configured project resources.
703 configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>();
706 // get the framework resources
707 Map<String, Map<String, IResourceValue>> frameworkResources =
708 getConfiguredFrameworkResources();
710 if (configuredProjectResources != null && frameworkResources != null) {
711 // get the selected theme
712 String theme = mConfigComposite.getTheme();
714 // Render a single object as described by the ViewElementDescriptor.
715 WidgetPullParser parser = new WidgetPullParser(descriptor);
716 ILayoutResult result = computeLayout(bridge, parser,
717 null /* projectKey */,
718 1 /* width */, 1 /* height */, true /* renderFullSize */,
719 160 /*density*/, 160.f /*xdpi*/, 160.f /*ydpi*/, theme,
720 mConfigComposite.isProjectTheme(),
721 configuredProjectResources, frameworkResources, projectCallback,
724 // post rendering clean up
727 // update the UiElementNode with the layout info.
728 if (result.getSuccess() == ILayoutResult.SUCCESS) {
729 BufferedImage largeImage = result.getImage();
731 // we need to resize it to the actual widget size, and convert it into
732 // an SWT image object.
733 int width = result.getRootView().getRight();
734 int height = result.getRootView().getBottom();
735 Raster raster = largeImage.getData(new java.awt.Rectangle(width, height));
736 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
738 ImageData imageData = new ImageData(width, height, 32,
739 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
741 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
752 * Callback for XML model changed. Only update/recompute the layout if the editor is visible
754 public void onXmlModelChanged() {
755 if (mLayoutEditor.isGraphicalEditorActive()) {
756 doXmlReload(true /* force */);
759 mNeedsXmlReload = true;
764 * Actually performs the XML reload
765 * @see #onXmlModelChanged()
767 private void doXmlReload(boolean force) {
768 if (force || mNeedsXmlReload) {
769 GraphicalViewer viewer = getGraphicalViewer();
771 // try to preserve the selection before changing the content
772 SelectionManager selMan = viewer.getSelectionManager();
773 ISelection selection = selMan.getSelection();
776 viewer.setContents(getModel());
778 selMan.setSelection(selection);
781 mNeedsXmlReload = false;
785 public UiDocumentNode getModel() {
786 return mLayoutEditor.getUiRootNode();
789 public void reloadPalette() {
790 PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData());
794 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.
795 * <p/>If there is no match, notify the user.
797 public void onConfigurationChange() {
798 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
800 if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) {
804 // Before doing the normal process, test for the following case.
805 // - the editor is being opened (or reset for a new input)
806 // - the file being opened is not the best match for any possible configuration
807 // - another random compatible config was chosen in the config composite.
808 // The result is that match will not be the file being edited, but because this is not
809 // due to a config change, we should not trigger opening the actual best match (also,
810 // because the editor is still opening the MatchingStrategy woudln't answer true
811 // and the best match file would open in a different editor).
812 // So the solution is that if the editor is being created, we just call recomputeLayout
813 // without looking for a better matching layout file.
814 if (mLayoutEditor.isCreatingPages()) {
817 // get the resources of the file's project.
818 ProjectResources resources = ResourceManager.getInstance().getProjectResources(
819 mEditedFile.getProject());
821 // from the resources, look for a matching file
822 ResourceFile match = null;
823 if (resources != null) {
824 match = resources.getMatchingFile(mEditedFile.getName(),
825 ResourceFolderType.LAYOUT,
826 mConfigComposite.getCurrentConfig());
830 // since this is coming from Eclipse, this is always an instance of IFileWrapper
831 IFileWrapper iFileWrapper = (IFileWrapper) match.getFile();
832 IFile iFile = iFileWrapper.getIFile();
833 if (iFile.equals(mEditedFile) == false) {
835 // tell the editor that the next replacement file is due to a config change.
836 mLayoutEditor.setNewFileOnConfigChange(true);
838 // ask the IDE to open the replacement file.
839 IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), iFile);
843 } catch (PartInitException e) {
844 // FIXME: do something!
848 // at this point, we have not opened a new file.
850 // Store the state in the current file
851 mConfigComposite.storeState();
853 // Even though the layout doesn't change, the config changed, and referenced
854 // resources need to be updated.
857 // display the error.
858 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();
859 String message = String.format(
860 "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.",
861 currentConfig.toDisplayString(),
862 currentConfig.getFolderName(ResourceFolderType.LAYOUT),
863 mEditedFile.getName());
864 showErrorInEditor(message);
869 public void onThemeChange() {
870 // Store the state in the current file
871 mConfigComposite.storeState();
876 public void onClippingChange() {
881 public void onCreate() {
882 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mParent.getShell(),
883 mEditedFile.getName(), mConfigComposite.getCurrentConfig());
884 if (dialog.open() == Dialog.OK) {
885 final FolderConfiguration config = new FolderConfiguration();
886 dialog.getConfiguration(config);
888 createAlternateLayout(config);
893 * Recomputes the layout with the help of layoutlib.
895 public void recomputeLayout() {
896 doXmlReload(false /* force */);
898 // check that the resource exists. If the file is opened but the project is closed
899 // or deleted for some reason (changed from outside of eclipse), then this will
901 if (mEditedFile.exists() == false) {
902 String message = String.format("Resource '%1$s' does not exist.",
903 mEditedFile.getFullPath().toString());
905 showErrorInEditor(message);
910 IProject iProject = mEditedFile.getProject();
912 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
913 String message = String.format("%1$s is out of sync. Please refresh.",
914 mEditedFile.getName());
916 showErrorInEditor(message);
918 // also print it in the error console.
919 AdtPlugin.printErrorToConsole(iProject.getName(), message);
923 Sdk currentSdk = Sdk.getCurrent();
924 if (currentSdk != null) {
925 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
926 if (target == null) {
927 showErrorInEditor("The project target is not set.");
931 AndroidTargetData data = currentSdk.getTargetData(target);
933 // It can happen that the workspace refreshes while the target is loading its
934 // data, which could trigger a redraw of the opened layout if some resources
935 // changed while Eclipse is closed.
936 // In this case data could be null, but this is not an error.
937 // We can just silently return, as all the opened editors are automatically
938 // refreshed once the SDK finishes loading.
939 LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);
940 switch (targetLoadStatus) {
942 showErrorInEditor(String.format(
943 "The project target (%1$s) is still loading.\n%2$s will refresh automatically once the process is finished.",
944 target.getName(), mEditedFile.getName()));
946 case FAILED: // known failure
947 case LOADED: // success but data isn't loaded?!?!
948 showErrorInEditor(String.format(
949 "The project target (%s) was not properly loaded.",
956 // check there is actually a model (maybe the file is empty).
957 UiDocumentNode model = getModel();
959 if (model.getUiChildren().size() == 0) {
960 showErrorInEditor("No Xml content. Go to the Outline view and add nodes.");
964 LayoutBridge bridge = data.getLayoutBridge();
966 if (bridge.bridge != null) { // bridge can never be null.
967 ResourceManager resManager = ResourceManager.getInstance();
969 ProjectResources projectRes = resManager.getProjectResources(iProject);
970 if (projectRes == null) {
974 // get the resources of the file's project.
975 Map<String, Map<String, IResourceValue>> configuredProjectRes =
976 getConfiguredProjectResources();
978 // get the framework resources
979 Map<String, Map<String, IResourceValue>> frameworkResources =
980 getConfiguredFrameworkResources();
982 if (configuredProjectRes != null && frameworkResources != null) {
983 if (mProjectCallback == null) {
984 mProjectCallback = new ProjectCallback(
985 bridge.classLoader, projectRes, iProject);
988 if (mLogger == null) {
989 mLogger = new ILayoutLog() {
990 public void error(String message) {
991 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);
994 public void error(Throwable error) {
995 String message = error.getMessage();
996 if (message == null) {
997 message = error.getClass().getName();
1000 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());
1001 error.printStackTrace(ps);
1004 public void warning(String message) {
1005 AdtPlugin.printToConsole(mEditedFile.getName(), message);
1010 // get the selected theme
1011 String theme = mConfigComposite.getTheme();
1012 if (theme != null) {
1013 // Compute the layout
1014 Rectangle rect = getBounds();
1016 int width = rect.width;
1017 int height = rect.height;
1018 if (mUseExplodeMode) {
1019 // compute how many padding in x and y will bump the screen size
1020 List<UiElementNode> children = getModel().getUiChildren();
1021 if (children.size() == 1) {
1022 ExplodedRenderingHelper helper = new ExplodedRenderingHelper(
1023 children.get(0).getXmlNode(), iProject);
1025 // there are 2 paddings for each view
1026 // left and right, or top and bottom.
1027 int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;
1029 width += helper.getWidthPadding() * paddingValue;
1030 height += helper.getHeightPadding() * paddingValue;
1034 int density = mConfigComposite.getDensity().getDpiValue();
1035 float xdpi = mConfigComposite.getXDpi();
1036 float ydpi = mConfigComposite.getYDpi();
1037 boolean isProjectTheme = mConfigComposite.isProjectTheme();
1039 UiElementPullParser parser = new UiElementPullParser(getModel(),
1040 mUseExplodeMode, density, xdpi, iProject);
1042 ILayoutResult result = computeLayout(bridge, parser,
1043 iProject /* projectKey */,
1044 width, height, !mConfigComposite.getClipping(),
1045 density, xdpi, ydpi,
1046 theme, isProjectTheme,
1047 configuredProjectRes, frameworkResources, mProjectCallback,
1050 // post-rendering clean up
1053 // update the UiElementNode with the layout info.
1054 if (result.getSuccess() == ILayoutResult.SUCCESS) {
1055 model.setEditData(result.getImage());
1057 updateNodeWithBounds(result.getRootView());
1059 String message = result.getErrorMessage();
1061 // Reset the edit data for all the nodes.
1062 resetNodeBounds(model);
1064 if (message != null) {
1065 // set the error in the top element.
1066 model.setEditData(message);
1074 // SDK is loaded but not the layout library!
1075 String message = null;
1076 // check whether the bridge managed to load, or not
1077 if (bridge.status == LoadStatus.LOADING) {
1078 message = String.format(
1079 "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
1080 mEditedFile.getName());
1082 message = String.format("Eclipse failed to load the framework information and the Layout library!");
1084 showErrorInEditor(message);
1087 String message = String.format(
1088 "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
1089 mEditedFile.getName());
1091 showErrorInEditor(message);
1094 // no matter the result, we are done doing the recompute based on the latest
1095 // resource/code change.
1096 mNeedsRecompute = false;
1100 private void showErrorInEditor(String message) {
1101 // get the model to display the error directly in the editor
1102 UiDocumentNode model = getModel();
1104 // Reset the edit data for all the nodes.
1105 resetNodeBounds(model);
1107 if (message != null) {
1108 // set the error in the top element.
1109 model.setEditData(message);
1115 private void resetNodeBounds(UiElementNode node) {
1116 node.setEditData(null);
1118 List<UiElementNode> children = node.getUiChildren();
1119 for (UiElementNode child : children) {
1120 resetNodeBounds(child);
1124 private void updateNodeWithBounds(ILayoutViewInfo r) {
1126 // update the node itself, as the viewKey is the XML node in this implementation.
1127 Object viewKey = r.getViewKey();
1128 if (viewKey instanceof UiElementNode) {
1129 Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(),
1130 r.getRight()-r.getLeft(), r.getBottom() - r.getTop());
1132 ((UiElementNode)viewKey).setEditData(bounds);
1135 // and then its children.
1136 ILayoutViewInfo[] children = r.getChildren();
1137 if (children != null) {
1138 for (ILayoutViewInfo child : children) {
1139 updateNodeWithBounds(child);
1146 * Called when the file changes triggered a redraw of the layout
1148 public void reloadLayout(ChangeFlags flags, boolean libraryChanged) {
1149 boolean recompute = false;
1153 ProjectResources projectRes = getProjectResources();
1154 if (projectRes != null) {
1155 projectRes.resetDynamicIds();
1159 if (flags.localeList) {
1160 // the locale list *potentially* changed so we update the locale list in the
1161 // config composite.
1162 // However there's no recompute, as it could not be needed (for instance a new layout)
1163 // If a resource that's not a layout changed this will trigger a recompute anyway.
1164 mParent.getDisplay().asyncExec(mLocaleUpdaterFromUiRunnable);
1167 if (flags.resources || (libraryChanged && flags.layout)) {
1170 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.
1172 // force a reparse in case a value XML file changed.
1173 mConfiguredProjectRes = null;
1175 // clear the cache in the bridge in case a bitmap/9-patch changed.
1176 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
1177 if (target != null) {
1179 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
1181 LayoutBridge bridge = data.getLayoutBridge();
1183 if (bridge.bridge != null) {
1184 bridge.bridge.clearCaches(mEditedFile.getProject());
1192 // only recompute if the custom view loader was used to load some code.
1193 if (mProjectCallback != null && mProjectCallback.isUsed()) {
1194 mProjectCallback = null;
1200 mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable);
1205 * Responds to a page change that made the Graphical editor page the activated page.
1207 public void activated() {
1208 if (mNeedsRecompute || mNeedsXmlReload) {
1214 * Responds to a page change that made the Graphical editor page the deactivated page
1216 public void deactivated() {
1217 // nothing to be done here for now.
1220 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {
1221 if (mConfiguredFrameworkRes == null) {
1222 ProjectResources frameworkRes = getFrameworkResources();
1224 if (frameworkRes == null) {
1225 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
1227 // get the framework resource values based on the current config
1228 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
1229 mConfigComposite.getCurrentConfig());
1233 return mConfiguredFrameworkRes;
1236 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {
1237 if (mConfiguredProjectRes == null) {
1238 ProjectResources project = getProjectResources();
1240 // make sure they are loaded
1243 // get the project resource values based on the current config
1244 mConfiguredProjectRes = project.getConfiguredResources(
1245 mConfigComposite.getCurrentConfig());
1248 return mConfiguredProjectRes;
1252 * Returns a {@link ProjectResources} for the framework resources.
1253 * @return the framework resources or null if not found.
1255 public ProjectResources getFrameworkResources() {
1256 if (mEditedFile != null) {
1257 Sdk currentSdk = Sdk.getCurrent();
1258 if (currentSdk != null) {
1259 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
1261 if (target != null) {
1262 AndroidTargetData data = currentSdk.getTargetData(target);
1265 return data.getFrameworkResources();
1274 public ProjectResources getProjectResources() {
1275 if (mEditedFile != null) {
1276 ResourceManager manager = ResourceManager.getInstance();
1277 return manager.getProjectResources(mEditedFile.getProject());
1284 * Creates a new layout file from the specified {@link FolderConfiguration}.
1286 private void createAlternateLayout(final FolderConfiguration config) {
1287 new Job("Create Alternate Resource") {
1289 protected IStatus run(IProgressMonitor monitor) {
1290 // get the folder name
1291 String folderName = config.getFolderName(ResourceFolderType.LAYOUT);
1294 // look to see if it exists.
1295 // get the res folder
1296 IFolder res = (IFolder)mEditedFile.getParent().getParent();
1297 String path = res.getLocation().toOSString();
1299 File newLayoutFolder = new File(path + File.separator + folderName);
1300 if (newLayoutFolder.isFile()) {
1301 // this should not happen since aapt would have complained
1302 // before, but if one disable the automatic build, this could
1304 String message = String.format("File 'res/%1$s' is in the way!",
1307 AdtPlugin.displayError("Layout Creation", message);
1309 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
1310 } else if (newLayoutFolder.exists() == false) {
1312 newLayoutFolder.mkdir();
1315 // now create the file
1316 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
1317 File.separator + mEditedFile.getName());
1319 newLayoutFile.createNewFile();
1321 InputStream input = mEditedFile.getContents();
1323 FileOutputStream fos = new FileOutputStream(newLayoutFile);
1325 byte[] data = new byte[512];
1327 while ((count = input.read(data)) != -1) {
1328 fos.write(data, 0, count);
1334 // refreshes the res folder to show up the new
1335 // layout folder (if needed) and the file.
1336 // We use a progress monitor to catch the end of the refresh
1337 // to trigger the edit of the new file.
1338 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
1339 public void done() {
1340 mParent.getDisplay().asyncExec(new Runnable() {
1342 onConfigurationChange();
1347 public void beginTask(String name, int totalWork) {
1351 public void internalWorked(double work) {
1355 public boolean isCanceled() {
1360 public void setCanceled(boolean value) {
1364 public void setTaskName(String name) {
1368 public void subTask(String name) {
1372 public void worked(int work) {
1376 } catch (IOException e2) {
1377 String message = String.format(
1378 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1379 folderName, mEditedFile.getName(), e2.getMessage());
1381 AdtPlugin.displayError("Layout Creation", message);
1383 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
1385 } catch (CoreException e2) {
1386 String message = String.format(
1387 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1388 folderName, mEditedFile.getName(), e2.getMessage());
1390 AdtPlugin.displayError("Layout Creation", message);
1392 return e2.getStatus();
1395 return Status.OK_STATUS;
1402 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
1403 * the implementation API level.
1405 @SuppressWarnings("deprecation")
1406 private static ILayoutResult computeLayout(LayoutBridge bridge,
1407 IXmlPullParser layoutDescription, Object projectKey,
1408 int screenWidth, int screenHeight, boolean renderFullSize,
1409 int density, float xdpi, float ydpi,
1410 String themeName, boolean isProjectTheme,
1411 Map<String, Map<String, IResourceValue>> projectResources,
1412 Map<String, Map<String, IResourceValue>> frameworkResources,
1413 IProjectCallback projectCallback, ILayoutLog logger) {
1415 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {
1416 // newest API with support for "render full height"
1417 // TODO: link boolean to UI.
1418 return bridge.bridge.computeLayout(layoutDescription,
1419 projectKey, screenWidth, screenHeight, renderFullSize,
1420 density, xdpi, ydpi,
1421 themeName, isProjectTheme,
1422 projectResources, frameworkResources, projectCallback,
1424 } else if (bridge.apiLevel == 3) {
1425 // newer api with density support.
1426 return bridge.bridge.computeLayout(layoutDescription,
1427 projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
1428 themeName, isProjectTheme,
1429 projectResources, frameworkResources, projectCallback,
1431 } else if (bridge.apiLevel == 2) {
1432 // api with boolean for separation of project/framework theme
1433 return bridge.bridge.computeLayout(layoutDescription,
1434 projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
1435 projectResources, frameworkResources, projectCallback,
1438 // oldest api with no density/dpi, and project theme boolean mixed
1439 // into the theme name.
1441 // change the string if it's a custom theme to make sure we can
1442 // differentiate them
1443 if (isProjectTheme) {
1444 themeName = "*" + themeName; //$NON-NLS-1$
1447 return bridge.bridge.computeLayout(layoutDescription,
1448 projectKey, screenWidth, screenHeight, themeName,
1449 projectResources, frameworkResources, projectCallback,
1454 public boolean hasOutline() {
1455 return mUseOutlineMode;