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;
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.ConfigurationComposite.IConfigListener;
25 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
26 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;
27 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementEditPart;
28 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementsEditPartFactory;
29 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction;
30 import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction;
31 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
32 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
33 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
34 import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier;
35 import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier.Density;
36 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
37 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
38 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
39 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
40 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
41 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
42 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
43 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
44 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
45 import com.android.layoutlib.api.ILayoutBridge;
46 import com.android.layoutlib.api.ILayoutLog;
47 import com.android.layoutlib.api.ILayoutResult;
48 import com.android.layoutlib.api.IProjectCallback;
49 import com.android.layoutlib.api.IResourceValue;
50 import com.android.layoutlib.api.IXmlPullParser;
51 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
52 import com.android.sdklib.IAndroidTarget;
54 import org.eclipse.core.resources.IFile;
55 import org.eclipse.core.resources.IFolder;
56 import org.eclipse.core.resources.IProject;
57 import org.eclipse.core.resources.IResource;
58 import org.eclipse.core.runtime.CoreException;
59 import org.eclipse.core.runtime.IProgressMonitor;
60 import org.eclipse.core.runtime.IStatus;
61 import org.eclipse.core.runtime.Status;
62 import org.eclipse.core.runtime.jobs.Job;
63 import org.eclipse.draw2d.geometry.Rectangle;
64 import org.eclipse.gef.DefaultEditDomain;
65 import org.eclipse.gef.EditPart;
66 import org.eclipse.gef.EditPartViewer;
67 import org.eclipse.gef.GraphicalViewer;
68 import org.eclipse.gef.SelectionManager;
69 import org.eclipse.gef.dnd.TemplateTransferDragSourceListener;
70 import org.eclipse.gef.dnd.TemplateTransferDropTargetListener;
71 import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
72 import org.eclipse.gef.palette.PaletteRoot;
73 import org.eclipse.gef.requests.CreationFactory;
74 import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette;
75 import org.eclipse.gef.ui.parts.SelectionSynchronizer;
76 import org.eclipse.jface.action.Action;
77 import org.eclipse.jface.action.IMenuListener;
78 import org.eclipse.jface.action.IMenuManager;
79 import org.eclipse.jface.action.MenuManager;
80 import org.eclipse.jface.action.Separator;
81 import org.eclipse.jface.dialogs.Dialog;
82 import org.eclipse.jface.viewers.ISelection;
83 import org.eclipse.swt.SWT;
84 import org.eclipse.swt.dnd.Clipboard;
85 import org.eclipse.swt.graphics.ImageData;
86 import org.eclipse.swt.graphics.PaletteData;
87 import org.eclipse.swt.layout.FillLayout;
88 import org.eclipse.swt.layout.GridData;
89 import org.eclipse.swt.layout.GridLayout;
90 import org.eclipse.swt.widgets.Composite;
91 import org.eclipse.ui.IEditorInput;
92 import org.eclipse.ui.PartInitException;
93 import org.eclipse.ui.ide.IDE;
94 import org.eclipse.ui.part.FileEditorInput;
96 import java.awt.image.BufferedImage;
97 import java.awt.image.DataBufferInt;
98 import java.awt.image.Raster;
100 import java.io.FileOutputStream;
101 import java.io.IOException;
102 import java.io.InputStream;
103 import java.io.PrintStream;
104 import java.util.ArrayList;
105 import java.util.HashMap;
106 import java.util.List;
107 import java.util.Map;
110 * Graphical layout editor, based on GEF.
112 * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/
114 * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
118 public class GraphicalLayoutEditor extends GraphicalEditorWithPalette
119 implements IGraphicalLayoutEditor, IConfigListener, ILayoutReloadListener {
122 /** Reference to the layout editor */
123 private final LayoutEditor mLayoutEditor;
125 /** reference to the file being edited. */
126 private IFile mEditedFile;
128 private Clipboard mClipboard;
129 private Composite mParent;
130 private ConfigurationComposite mConfigComposite;
132 private PaletteRoot mPaletteRoot;
134 /** The {@link FolderConfiguration} being edited. */
135 private FolderConfiguration mEditedConfig;
137 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
138 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
139 private ProjectCallback mProjectCallback;
140 private ILayoutLog mLogger;
142 private boolean mNeedsXmlReload = false;
143 private boolean mNeedsRecompute = false;
145 /** Listener to update the root node if the target of the file is changed because of a
146 * SDK location change or a project target change */
147 private ITargetChangeListener mTargetListener = new ITargetChangeListener() {
148 public void onProjectTargetChange(IProject changedProject) {
149 if (changedProject == getLayoutEditor().getProject()) {
154 public void onTargetsLoaded() {
155 // because the SDK changed we must reset the configured framework resource.
156 mConfiguredFrameworkRes = null;
158 mConfigComposite.updateUIFromResources();
160 // updateUiFromFramework will reset language/region combo, so we must call
161 // setConfiguration after, or the settext on language/region will be lost.
162 if (mEditedConfig != null) {
163 setConfiguration(mEditedConfig, false /*force*/);
166 // make sure we remove the custom view loader, since its parent class loader is the
167 // bridge class loader.
168 mProjectCallback = null;
174 private final Runnable mConditionalRecomputeRunnable = new Runnable() {
176 if (mLayoutEditor.isGraphicalEditorActive()) {
179 mNeedsRecompute = true;
184 private final Runnable mUiUpdateFromResourcesRunnable = new Runnable() {
186 mConfigComposite.updateUIFromResources();
191 public GraphicalLayoutEditor(LayoutEditor layoutEditor) {
192 mLayoutEditor = layoutEditor;
193 setEditDomain(new DefaultEditDomain(this));
194 setPartName("Layout");
196 AdtPlugin.getDefault().addTargetListener(mTargetListener);
199 // ------------------------------------
200 // Methods overridden from base classes
201 //------------------------------------
204 public void createPartControl(Composite parent) {
208 mClipboard = new Clipboard(parent.getDisplay());
210 parent.setLayout(gl = new GridLayout(1, false));
211 gl.marginHeight = gl.marginWidth = 0;
213 // create the top part for the configuration control
214 mConfigComposite = new ConfigurationComposite(this, parent, SWT.NONE);
216 // create a new composite that will contain the standard editor controls.
217 Composite editorParent = new Composite(parent, SWT.NONE);
218 editorParent.setLayoutData(new GridData(GridData.FILL_BOTH));
219 editorParent.setLayout(new FillLayout());
220 super.createPartControl(editorParent);
224 public void dispose() {
225 if (mTargetListener != null) {
226 AdtPlugin.getDefault().removeTargetListener(mTargetListener);
227 mTargetListener = null;
230 LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this);
232 if (mClipboard != null) {
233 mClipboard.dispose();
241 * Returns the selection synchronizer object.
242 * The synchronizer can be used to sync the selection of 2 or more EditPartViewers.
244 * This is changed from protected to public so that the outline can use it.
246 * @return the synchronizer
249 public SelectionSynchronizer getSelectionSynchronizer() {
250 return super.getSelectionSynchronizer();
254 * Returns the edit domain.
256 * This is changed from protected to public so that the outline can use it.
258 * @return the edit domain
261 public DefaultEditDomain getEditDomain() {
262 return super.getEditDomain();
266 * Creates the palette root.
269 protected PaletteRoot getPaletteRoot() {
270 mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot,
271 mLayoutEditor.getTargetData());
275 public Clipboard getClipboard() {
280 * Save operation in the Graphical Layout Editor.
282 * In our workflow, the model is owned by the Structured XML Editor.
283 * The graphical layout editor just displays it -- thus we don't really
284 * save anything here.
286 * This must NOT call the parent editor part. At the contrary, the parent editor
287 * part will call this *after* having done the actual save operation.
289 * The only action this editor must do is mark the undo command stack as
290 * being no longer dirty.
293 public void doSave(IProgressMonitor monitor) {
294 getCommandStack().markSaveLocation();
295 firePropertyChange(PROP_DIRTY);
299 protected void configurePaletteViewer() {
300 super.configurePaletteViewer();
302 // Create a drag source listener on an edit part that is a viewer.
303 // What this does is use DND with a TemplateTransfer type which is actually
304 // the PaletteTemplateEntry held in the PaletteRoot.
305 TemplateTransferDragSourceListener dragSource =
306 new TemplateTransferDragSourceListener(getPaletteViewer());
308 // Create a drag source on the palette viewer.
309 // See the drag target associated with the GraphicalViewer in configureGraphicalViewer.
310 getPaletteViewer().addDragSourceListener(dragSource);
314 * Configure the graphical viewer before it receives its contents.
317 protected void configureGraphicalViewer() {
318 super.configureGraphicalViewer();
320 GraphicalViewer viewer = getGraphicalViewer();
321 viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay()));
322 viewer.setRootEditPart(new ScalableFreeformRootEditPart());
324 // Disable the following -- we don't drag *from* the GraphicalViewer yet:
325 // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
327 viewer.addDropTargetListener(new DropListener(viewer));
330 class DropListener extends TemplateTransferDropTargetListener {
331 public DropListener(EditPartViewer viewer) {
337 protected CreationFactory getFactory(final Object template) {
338 return new CreationFactory() {
339 public Object getNewObject() {
340 // We don't know the newly created EditPart since "creating" new
341 // elements is done by ElementCreateCommand.execute() directly by
342 // manipulating the XML elements..
346 public Object getObjectType() {
355 * Set the contents of the GraphicalViewer after it has been created.
358 protected void initializeGraphicalViewer() {
359 GraphicalViewer viewer = getGraphicalViewer();
360 viewer.setContents(getModel());
362 IEditorInput input = getEditorInput();
363 if (input instanceof FileEditorInput) {
364 FileEditorInput fileInput = (FileEditorInput)input;
365 mEditedFile = fileInput.getFile();
367 mConfigComposite.updateUIFromResources();
369 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this);
371 // really this shouldn't happen! Log it in case it happens
373 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
379 * Sets the graphicalViewer for this EditorPart.
380 * @param viewer the graphical viewer
383 protected void setGraphicalViewer(GraphicalViewer viewer) {
384 super.setGraphicalViewer(viewer);
386 // TODO: viewer.setKeyHandler()
387 viewer.setContextMenu(createContextMenu(viewer));
391 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node
392 * created by {@link ElementCreateCommand#execute()}.
394 * @param uiNodeModel The {@link UiElementNode} to select.
396 public void selectModel(UiElementNode uiNodeModel) {
397 GraphicalViewer viewer = getGraphicalViewer();
399 // Give focus to the graphical viewer (in case the outline has it)
400 viewer.getControl().forceFocus();
402 Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);
404 if (editPart instanceof EditPart) {
405 viewer.select((EditPart)editPart);
414 public LayoutEditor getLayoutEditor() {
415 return mLayoutEditor;
418 private MenuManager createContextMenu(GraphicalViewer viewer) {
419 MenuManager menuManager = new MenuManager();
420 menuManager.setRemoveAllWhenShown(true);
421 menuManager.addMenuListener(new ActionMenuListener(viewer));
426 private class ActionMenuListener implements IMenuListener {
427 private final GraphicalViewer mViewer;
429 public ActionMenuListener(GraphicalViewer viewer) {
434 * The menu is about to be shown. The menu manager has already been
435 * requested to remove any existing menu item. This method gets the
436 * tree selection and if it is of the appropriate type it re-creates
437 * the necessary actions.
439 public void menuAboutToShow(IMenuManager manager) {
440 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
442 // filter selected items and only keep those we can handle
443 for (Object obj : mViewer.getSelectedEditParts()) {
444 if (obj instanceof UiElementEditPart) {
445 UiElementEditPart part = (UiElementEditPart) obj;
446 UiElementNode uiNode = part.getUiNode();
447 if (uiNode != null) {
448 selected.add(uiNode);
453 if (selected.size() > 0) {
454 doCreateMenuAction(manager, mViewer, selected);
459 private void doCreateMenuAction(IMenuManager manager,
460 final GraphicalViewer viewer,
461 final ArrayList<UiElementNode> selected) {
462 if (selected != null) {
463 boolean hasXml = false;
464 for (UiElementNode uiNode : selected) {
465 if (uiNode.getXmlNode() != null) {
472 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
473 null, selected, true /* cut */));
474 manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
475 null, selected, false /* cut */));
477 // Can't paste with more than one element selected (the selection is the target)
478 if (selected.size() <= 1) {
479 // Paste is not valid if it would add a second element on a terminal element
480 // which parent is a document -- an XML document can only have one child. This
481 // means paste is valid if the current UI node can have children or if the
482 // parent is not a document.
483 UiElementNode ui_root = selected.get(0).getUiRoot();
484 if (ui_root.getDescriptor().hasChildren() ||
485 !(ui_root.getUiParent() instanceof UiDocumentNode)) {
486 manager.add(new PasteAction(mLayoutEditor, getClipboard(),
490 manager.add(new Separator());
494 // Append "add" and "remove" actions. They do the same thing as the add/remove
495 // buttons on the side.
496 IconFactory factory = IconFactory.getInstance();
498 final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions();
500 // "Add" makes sense only if there's 0 or 1 item selected since the
501 // one selected item becomes the target.
502 if (selected == null || selected.size() <= 1) {
503 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$
506 UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0)
508 uiActions.doAdd(node, viewer.getControl().getShell());
513 if (selected != null) {
514 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$
517 uiActions.doRemove(selected, viewer.getControl().getShell());
521 manager.add(new Separator());
523 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$
526 uiActions.doUp(selected);
529 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$
532 uiActions.doDown(selected);
540 * Sets the UI for the edition of a new file.
541 * @param configuration the configuration of the new file.
543 public void editNewFile(FolderConfiguration configuration) {
544 // update the configuration UI
545 setConfiguration(configuration, true /*force*/);
547 // enable the create button if the current and edited config are not equals
548 mConfigComposite.setEnabledCreate(
549 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false);
551 reloadConfigurationUi();
554 public Rectangle getBounds() {
555 return mConfigComposite.getScreenBounds();
559 * Renders an Android View described by a {@link ViewElementDescriptor}.
560 * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and
561 * <code>layout_height</code>, and use the class name for the <code>text</code> attribute.
562 * @param descriptor the descriptor for the class to render.
563 * @return an ImageData containing the rendering or <code>null</code> if rendering failed.
565 public ImageData renderWidget(ViewElementDescriptor descriptor) {
566 if (mEditedFile == null) {
570 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
571 if (target == null) {
575 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
580 LayoutBridge bridge = data.getLayoutBridge();
582 if (bridge.bridge != null) { // bridge can never be null.
583 ResourceManager resManager = ResourceManager.getInstance();
585 ProjectCallback projectCallback = null;
586 Map<String, Map<String, IResourceValue>> configuredProjectResources = null;
587 if (mEditedFile != null) {
588 ProjectResources projectRes = resManager.getProjectResources(
589 mEditedFile.getProject());
590 projectCallback = new ProjectCallback(bridge.classLoader,
591 projectRes, mEditedFile.getProject());
593 // get the configured resources for the project
594 // get the resources of the file's project.
595 if (mConfiguredProjectRes == null && projectRes != null) {
596 // make sure they are loaded
597 projectRes.loadAll();
599 // get the project resource values based on the current config
600 mConfiguredProjectRes = projectRes.getConfiguredResources(
601 mConfigComposite.getCurrentConfig());
604 configuredProjectResources = mConfiguredProjectRes;
606 // we absolutely need a Map of configured project resources.
607 configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>();
610 // get the framework resources
611 Map<String, Map<String, IResourceValue>> frameworkResources =
612 getConfiguredFrameworkResources();
614 if (configuredProjectResources != null && frameworkResources != null) {
615 // get the selected theme
616 String theme = mConfigComposite.getTheme();
618 // Render a single object as described by the ViewElementDescriptor.
619 WidgetPullParser parser = new WidgetPullParser(descriptor);
620 ILayoutResult result = computeLayout(bridge, parser,
621 null /* projectKey */,
622 1 /* width */, 1 /* height */, true /* renderFullSize */,
623 160 /*density*/, 160.f /*xdpi*/, 160.f /*ydpi*/, theme,
624 mConfigComposite.isProjectTheme(),
625 configuredProjectResources, frameworkResources, projectCallback,
628 // update the UiElementNode with the layout info.
629 if (result.getSuccess() == ILayoutResult.SUCCESS) {
630 BufferedImage largeImage = result.getImage();
632 // we need to resize it to the actual widget size, and convert it into
633 // an SWT image object.
634 int width = result.getRootView().getRight();
635 int height = result.getRootView().getBottom();
636 Raster raster = largeImage.getData(new java.awt.Rectangle(width, height));
637 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
639 ImageData imageData = new ImageData(width, height, 32,
640 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
642 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
653 * Reloads this editor, by getting the new model from the {@link LayoutEditor}.
655 public void reloadEditor() {
656 GraphicalViewer viewer = getGraphicalViewer();
657 viewer.setContents(getModel());
659 IEditorInput input = mLayoutEditor.getEditorInput();
662 if (input instanceof FileEditorInput) {
663 FileEditorInput fileInput = (FileEditorInput)input;
664 mEditedFile = fileInput.getFile();
666 // really this shouldn't happen! Log it in case it happens
668 AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
674 * Callback for XML model changed. Only update/recompute the layout if the editor is visible
676 public void onXmlModelChanged() {
677 if (mLayoutEditor.isGraphicalEditorActive()) {
678 doXmlReload(true /* force */);
681 mNeedsXmlReload = true;
686 * Actually performs the XML reload
687 * @see #onXmlModelChanged()
689 private void doXmlReload(boolean force) {
690 if (force || mNeedsXmlReload) {
691 GraphicalViewer viewer = getGraphicalViewer();
693 // try to preserve the selection before changing the content
694 SelectionManager selMan = viewer.getSelectionManager();
695 ISelection selection = selMan.getSelection();
698 viewer.setContents(getModel());
700 selMan.setSelection(selection);
703 mNeedsXmlReload = false;
708 * Update the UI controls state with a given {@link FolderConfiguration}.
709 * <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect
710 * <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>,
711 * the UI control is not modified. However if the value in the control is not the default value,
712 * a warning icon is shown.
713 * @param config The {@link FolderConfiguration} to set.
714 * @param force Whether the UI should be changed to exactly match the received configuration.
716 void setConfiguration(FolderConfiguration config, boolean force) {
717 mEditedConfig = config;
718 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
720 mConfigComposite.setConfiguration(config, force);
725 public UiDocumentNode getModel() {
726 return mLayoutEditor.getUiRootNode();
729 public void reloadPalette() {
730 PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData());
733 public void reloadConfigurationUi() {
734 // enable the clipping button if it's supported.
735 Sdk currentSdk = Sdk.getCurrent();
736 if (currentSdk != null) {
737 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
738 AndroidTargetData data = currentSdk.getTargetData(target);
740 LayoutBridge bridge = data.getLayoutBridge();
741 mConfigComposite.setClippingSupport(bridge.apiLevel >= 4);
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.
750 public void onConfigurationChange() {
751 mConfiguredFrameworkRes = mConfiguredProjectRes = null;
753 if (mEditedFile == null || mEditedConfig == null) {
757 // get the resources of the file's project.
758 ProjectResources resources = ResourceManager.getInstance().getProjectResources(
759 mEditedFile.getProject());
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());
770 if (match.getFile().equals(mEditedFile) == false) {
773 getSite().getWorkbenchWindow().getActivePage(),
774 match.getFile().getIFile());
778 } catch (PartInitException e) {
779 // FIXME: do something!
783 // at this point, we have not opened a new file.
785 // update the configuration icons with the new edited config.
786 setConfiguration(mEditedConfig, false /*force*/);
788 // enable the create button if the current and edited config are not equals
789 mConfigComposite.setEnabledCreate(
790 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false);
792 // Even though the layout doesn't change, the config changed, and referenced
793 // resources need to be updated.
796 // enable the Create button
797 mConfigComposite.setEnabledCreate(true);
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);
811 public void onThemeChange() {
815 public void OnClippingChange() {
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);
829 createAlternateLayout(config);
834 * Recomputes the layout with the help of layoutlib.
836 public void recomputeLayout() {
837 doXmlReload(false /* force */);
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
842 if (mEditedFile.exists() == false) {
843 String message = String.format("Resource '%1$s' does not exist.",
844 mEditedFile.getFullPath().toString());
846 showErrorInEditor(message);
851 IProject iProject = mEditedFile.getProject();
853 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
854 String message = String.format("%1$s is out of sync. Please refresh.",
855 mEditedFile.getName());
857 showErrorInEditor(message);
859 // also print it in the error console.
860 AdtPlugin.printErrorToConsole(iProject.getName(), message);
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.");
872 AndroidTargetData data = currentSdk.getTargetData(target);
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.",
888 // check there is actually a model (maybe the file is empty).
889 UiDocumentNode model = getModel();
891 if (model.getUiChildren().size() == 0) {
892 showErrorInEditor("No Xml content. Go to the Outline view and add nodes.");
896 LayoutBridge bridge = data.getLayoutBridge();
898 if (bridge.bridge != null) { // bridge can never be null.
899 ResourceManager resManager = ResourceManager.getInstance();
901 ProjectResources projectRes = resManager.getProjectResources(iProject);
902 if (projectRes == null) {
906 // get the resources of the file's project.
907 Map<String, Map<String, IResourceValue>> configuredProjectRes =
908 getConfiguredProjectResources();
910 // get the framework resources
911 Map<String, Map<String, IResourceValue>> frameworkResources =
912 getConfiguredFrameworkResources();
914 if (configuredProjectRes != null && frameworkResources != null) {
915 if (mProjectCallback == null) {
916 mProjectCallback = new ProjectCallback(
917 bridge.classLoader, projectRes, iProject);
920 if (mLogger == null) {
921 mLogger = new ILayoutLog() {
922 public void error(String message) {
923 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);
926 public void error(Throwable error) {
927 String message = error.getMessage();
928 if (message == null) {
929 message = error.getClass().getName();
932 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());
933 error.printStackTrace(ps);
936 public void warning(String message) {
937 AdtPlugin.printToConsole(mEditedFile.getName(), message);
942 // get the selected theme
943 String theme = mConfigComposite.getTheme();
946 // Compute the layout
947 UiElementPullParser parser = new UiElementPullParser(getModel());
948 Rectangle rect = getBounds();
949 boolean isProjectTheme = mConfigComposite.isProjectTheme();
951 // FIXME pass the density/dpi from somewhere (resource config or skin).
952 // For now, get it from the config
953 int density = Density.MEDIUM.getDpiValue();
954 PixelDensityQualifier qual =
955 mConfigComposite.getCurrentConfig().getPixelDensityQualifier();
957 int d = qual.getValue().getDpiValue();
963 ILayoutResult result = computeLayout(bridge, parser,
964 iProject /* projectKey */,
965 rect.width, rect.height, !mConfigComposite.getClipping(),
966 density, density, density,
967 theme, isProjectTheme,
968 configuredProjectRes, frameworkResources, mProjectCallback,
971 // update the UiElementNode with the layout info.
972 if (result.getSuccess() == ILayoutResult.SUCCESS) {
973 model.setEditData(result.getImage());
975 updateNodeWithBounds(result.getRootView());
977 String message = result.getErrorMessage();
979 // Reset the edit data for all the nodes.
980 resetNodeBounds(model);
982 if (message != null) {
983 // set the error in the top element.
984 model.setEditData(message);
992 // SDK is loaded but not the layout library!
993 String message = null;
994 // check whether the bridge managed to load, or not
995 if (bridge.status == LoadStatus.LOADING) {
996 message = String.format(
997 "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
998 mEditedFile.getName());
1000 message = String.format("Eclipse failed to load the framework information and the Layout library!");
1002 showErrorInEditor(message);
1005 String message = String.format(
1006 "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
1007 mEditedFile.getName());
1009 showErrorInEditor(message);
1012 // no matter the result, we are done doing the recompute based on the latest
1013 // resource/code change.
1014 mNeedsRecompute = false;
1018 private void showErrorInEditor(String message) {
1019 // get the model to display the error directly in the editor
1020 UiDocumentNode model = getModel();
1022 // Reset the edit data for all the nodes.
1023 resetNodeBounds(model);
1025 if (message != null) {
1026 // set the error in the top element.
1027 model.setEditData(message);
1033 private void resetNodeBounds(UiElementNode node) {
1034 node.setEditData(null);
1036 List<UiElementNode> children = node.getUiChildren();
1037 for (UiElementNode child : children) {
1038 resetNodeBounds(child);
1042 private void updateNodeWithBounds(ILayoutViewInfo r) {
1044 // update the node itself, as the viewKey is the XML node in this implementation.
1045 Object viewKey = r.getViewKey();
1046 if (viewKey instanceof UiElementNode) {
1047 Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(),
1048 r.getRight()-r.getLeft(), r.getBottom() - r.getTop());
1050 ((UiElementNode)viewKey).setEditData(bounds);
1053 // and then its children.
1054 ILayoutViewInfo[] children = r.getChildren();
1055 if (children != null) {
1056 for (ILayoutViewInfo child : children) {
1057 updateNodeWithBounds(child);
1065 * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean)
1067 * Called when the file changes triggered a redraw of the layout
1069 public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) {
1070 boolean recompute = rChange;
1075 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.
1077 // force a reparse in case a value XML file changed.
1078 mConfiguredProjectRes = null;
1080 // clear the cache in the bridge in case a bitmap/9-patch changed.
1081 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
1082 if (target != null) {
1084 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
1086 LayoutBridge bridge = data.getLayoutBridge();
1088 if (bridge.bridge != null) {
1089 bridge.bridge.clearCaches(mEditedFile.getProject());
1094 mParent.getDisplay().asyncExec(mUiUpdateFromResourcesRunnable);
1098 // only recompute if the custom view loader was used to load some code.
1099 if (mProjectCallback != null && mProjectCallback.isUsed()) {
1100 mProjectCallback = null;
1106 mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable);
1111 * Responds to a page change that made the Graphical editor page the activated page.
1113 public void activated() {
1114 if (mNeedsRecompute || mNeedsXmlReload) {
1120 * Responds to a page change that made the Graphical editor page the deactivated page
1122 public void deactivated() {
1123 // nothing to be done here for now.
1126 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {
1127 if (mConfiguredFrameworkRes == null) {
1128 ProjectResources frameworkRes = getFrameworkResources();
1130 if (frameworkRes == null) {
1131 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
1133 // get the framework resource values based on the current config
1134 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
1135 mConfigComposite.getCurrentConfig());
1139 return mConfiguredFrameworkRes;
1142 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {
1143 if (mConfiguredProjectRes == null) {
1144 ProjectResources project = getProjectResources();
1146 // make sure they are loaded
1149 // get the project resource values based on the current config
1150 mConfiguredProjectRes = project.getConfiguredResources(
1151 mConfigComposite.getCurrentConfig());
1154 return mConfiguredProjectRes;
1158 * Returns a {@link ProjectResources} for the framework resources.
1159 * @return the framework resources or null if not found.
1161 public ProjectResources getFrameworkResources() {
1162 if (mEditedFile != null) {
1163 Sdk currentSdk = Sdk.getCurrent();
1164 if (currentSdk != null) {
1165 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
1167 if (target != null) {
1168 AndroidTargetData data = currentSdk.getTargetData(target);
1171 return data.getFrameworkResources();
1180 public ProjectResources getProjectResources() {
1181 if (mEditedFile != null) {
1182 ResourceManager manager = ResourceManager.getInstance();
1183 return manager.getProjectResources(mEditedFile.getProject());
1190 * Creates a new layout file from the specified {@link FolderConfiguration}.
1192 private void createAlternateLayout(final FolderConfiguration config) {
1193 new Job("Create Alternate Resource") {
1195 protected IStatus run(IProgressMonitor monitor) {
1196 // get the folder name
1197 String folderName = config.getFolderName(ResourceFolderType.LAYOUT,
1198 Sdk.getCurrent().getTarget(mEditedFile.getProject()));
1201 // look to see if it exists.
1202 // get the res folder
1203 IFolder res = (IFolder)mEditedFile.getParent().getParent();
1204 String path = res.getLocation().toOSString();
1206 File newLayoutFolder = new File(path + File.separator + folderName);
1207 if (newLayoutFolder.isFile()) {
1208 // this should not happen since aapt would have complained
1209 // before, but if one disable the automatic build, this could
1211 String message = String.format("File 'res/%1$s' is in the way!",
1214 AdtPlugin.displayError("Layout Creation", message);
1216 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
1217 } else if (newLayoutFolder.exists() == false) {
1219 newLayoutFolder.mkdir();
1222 // now create the file
1223 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
1224 File.separator + mEditedFile.getName());
1226 newLayoutFile.createNewFile();
1228 InputStream input = mEditedFile.getContents();
1230 FileOutputStream fos = new FileOutputStream(newLayoutFile);
1232 byte[] data = new byte[512];
1234 while ((count = input.read(data)) != -1) {
1235 fos.write(data, 0, count);
1241 // refreshes the res folder to show up the new
1242 // layout folder (if needed) and the file.
1243 // We use a progress monitor to catch the end of the refresh
1244 // to trigger the edit of the new file.
1245 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
1246 public void done() {
1247 mParent.getDisplay().asyncExec(new Runnable() {
1249 onConfigurationChange();
1254 public void beginTask(String name, int totalWork) {
1258 public void internalWorked(double work) {
1262 public boolean isCanceled() {
1267 public void setCanceled(boolean value) {
1271 public void setTaskName(String name) {
1275 public void subTask(String name) {
1279 public void worked(int work) {
1283 } catch (IOException e2) {
1284 String message = String.format(
1285 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1286 folderName, mEditedFile.getName(), e2.getMessage());
1288 AdtPlugin.displayError("Layout Creation", message);
1290 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
1292 } catch (CoreException e2) {
1293 String message = String.format(
1294 "Failed to create File 'res/%1$s/%2$s' : %3$s",
1295 folderName, mEditedFile.getName(), e2.getMessage());
1297 AdtPlugin.displayError("Layout Creation", message);
1299 return e2.getStatus();
1302 return Status.OK_STATUS;
1309 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
1310 * the implementation API level.
1312 @SuppressWarnings("deprecation")
1313 private static ILayoutResult computeLayout(LayoutBridge bridge,
1314 IXmlPullParser layoutDescription, Object projectKey,
1315 int screenWidth, int screenHeight, boolean renderFullSize,
1316 int density, float xdpi, float ydpi,
1317 String themeName, boolean isProjectTheme,
1318 Map<String, Map<String, IResourceValue>> projectResources,
1319 Map<String, Map<String, IResourceValue>> frameworkResources,
1320 IProjectCallback projectCallback, ILayoutLog logger) {
1322 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {
1323 // newest API with support for "render full height"
1324 // TODO: link boolean to UI.
1325 return bridge.bridge.computeLayout(layoutDescription,
1326 projectKey, screenWidth, screenHeight, renderFullSize,
1327 density, xdpi, ydpi,
1328 themeName, isProjectTheme,
1329 projectResources, frameworkResources, projectCallback,
1331 } else if (bridge.apiLevel == 3) {
1332 // newer api with density support.
1333 return bridge.bridge.computeLayout(layoutDescription,
1334 projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
1335 themeName, isProjectTheme,
1336 projectResources, frameworkResources, projectCallback,
1338 } else if (bridge.apiLevel == 2) {
1339 // api with boolean for separation of project/framework theme
1340 return bridge.bridge.computeLayout(layoutDescription,
1341 projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
1342 projectResources, frameworkResources, projectCallback,
1345 // oldest api with no density/dpi, and project theme boolean mixed
1346 // into the theme name.
1348 // change the string if it's a custom theme to make sure we can
1349 // differentiate them
1350 if (isProjectTheme) {
1351 themeName = "*" + themeName; //$NON-NLS-1$
1354 return bridge.bridge.computeLayout(layoutDescription,
1355 projectKey, screenWidth, screenHeight, themeName,
1356 projectResources, frameworkResources, projectCallback,