-/*\r
- * Copyright (C) 2009 The Android Open Source Project\r
- *\r
- * Licensed under the Eclipse Public License, Version 1.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.eclipse.org/org/documents/epl-v10.php\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-package com.android.ide.eclipse.adt.internal.editors.layout.gle2;\r
-\r
-import com.android.ide.eclipse.adt.AdtPlugin;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;\r
-import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;\r
-import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;\r
-import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;\r
-import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;\r
-import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;\r
-import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;\r
-import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;\r
-import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;\r
-import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;\r
-import com.android.ide.eclipse.adt.internal.sdk.Sdk;\r
-import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;\r
-import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;\r
-import com.android.layoutlib.api.ILayoutBridge;\r
-import com.android.layoutlib.api.ILayoutLog;\r
-import com.android.layoutlib.api.ILayoutResult;\r
-import com.android.layoutlib.api.IProjectCallback;\r
-import com.android.layoutlib.api.IResourceValue;\r
-import com.android.layoutlib.api.IXmlPullParser;\r
-import com.android.sdklib.IAndroidTarget;\r
-\r
-import org.eclipse.core.resources.IFile;\r
-import org.eclipse.core.resources.IFolder;\r
-import org.eclipse.core.resources.IProject;\r
-import org.eclipse.core.resources.IResource;\r
-import org.eclipse.core.runtime.CoreException;\r
-import org.eclipse.core.runtime.IProgressMonitor;\r
-import org.eclipse.core.runtime.IStatus;\r
-import org.eclipse.core.runtime.Status;\r
-import org.eclipse.core.runtime.jobs.Job;\r
-import org.eclipse.draw2d.geometry.Rectangle;\r
-import org.eclipse.gef.ui.parts.SelectionSynchronizer;\r
-import org.eclipse.jface.action.Action;\r
-import org.eclipse.jface.dialogs.Dialog;\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.custom.SashForm;\r
-import org.eclipse.swt.custom.StyledText;\r
-import org.eclipse.swt.dnd.Clipboard;\r
-import org.eclipse.swt.layout.GridData;\r
-import org.eclipse.swt.layout.GridLayout;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Display;\r
-import org.eclipse.ui.IActionBars;\r
-import org.eclipse.ui.IEditorInput;\r
-import org.eclipse.ui.IEditorSite;\r
-import org.eclipse.ui.PartInitException;\r
-import org.eclipse.ui.actions.ActionFactory;\r
-import org.eclipse.ui.ide.IDE;\r
-import org.eclipse.ui.part.EditorPart;\r
-import org.eclipse.ui.part.FileEditorInput;\r
-\r
-import java.io.File;\r
-import java.io.FileOutputStream;\r
-import java.io.IOException;\r
-import java.io.InputStream;\r
-import java.io.PrintStream;\r
-import java.util.Map;\r
-\r
-/**\r
- * Graphical layout editor part, version 2.\r
- *\r
- * @since GLE2\r
- *\r
- * TODO List:\r
- * - display error icon\r
- * - finish palette (see palette's todo list)\r
- * - finish canvas (see canva's todo list)\r
- * - completly rethink the property panel\r
- * - link to the existing outline editor (prolly reuse/adapt for simplier model and will need\r
- * to adapt the selection synchronizer.)\r
- */\r
-public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutEditor {\r
-\r
- /*\r
- * Useful notes:\r
- * To understand Drag'n'drop:\r
- * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html\r
- */\r
-\r
- /** Reference to the layout editor */\r
- private final LayoutEditor mLayoutEditor;\r
-\r
- /** reference to the file being edited. Can also be used to access the {@link IProject}. */\r
- private IFile mEditedFile;\r
-\r
- /** The current clipboard. Must be disposed later. */\r
- private Clipboard mClipboard;\r
-\r
- /** The configuration composite at the top of the layout editor. */\r
- private ConfigurationComposite mConfigComposite;\r
-\r
- /** The sash that splits the palette from the canvas. */\r
- private SashForm mSashPalette;\r
- private SashForm mSashError;\r
-\r
- /** The palette displayed on the left of the sash. */\r
- private PaletteComposite mPalette;\r
-\r
- /** The layout canvas displayed to the right of the sash. */\r
- private LayoutCanvas mLayoutCanvas;\r
-\r
- /** The Groovy Rules Engine associated with this editor. It is project-specific. */\r
- private RulesEngine mRulesEngine;\r
-\r
- private StyledText mErrorLabel;\r
-\r
- private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;\r
- private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;\r
- private ProjectCallback mProjectCallback;\r
- private ILayoutLog mLogger;\r
-\r
- private boolean mNeedsXmlReload = false;\r
- private boolean mNeedsRecompute = false;\r
-\r
- private TargetListener mTargetListener;\r
-\r
- private ConfigListener mConfigListener;\r
-\r
- private ReloadListener mReloadListener;\r
-\r
- protected boolean mUseExplodeMode;\r
-\r
-\r
- public GraphicalEditorPart(LayoutEditor layoutEditor) {\r
- mLayoutEditor = layoutEditor;\r
- setPartName("Graphical Layout");\r
- }\r
-\r
- // ------------------------------------\r
- // Methods overridden from base classes\r
- //------------------------------------\r
-\r
- /**\r
- * Initializes the editor part with a site and input.\r
- * {@inheritDoc}\r
- */\r
- @Override\r
- public void init(IEditorSite site, IEditorInput input) throws PartInitException {\r
- setSite(site);\r
- useNewEditorInput(input);\r
-\r
- if (mTargetListener == null) {\r
- mTargetListener = new TargetListener();\r
- AdtPlugin.getDefault().addTargetListener(mTargetListener);\r
- }\r
- }\r
-\r
- private void useNewEditorInput(IEditorInput input) throws PartInitException {\r
- // The contract of init() mentions we need to fail if we can't understand the input.\r
- if (!(input instanceof FileEditorInput)) {\r
- throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$\r
- input == null ? "null" : input.toString()); //$NON-NLS-1$\r
- }\r
- }\r
-\r
- @Override\r
- public void createPartControl(Composite parent) {\r
-\r
- Display d = parent.getDisplay();\r
- mClipboard = new Clipboard(d);\r
-\r
- GridLayout gl = new GridLayout(1, false);\r
- parent.setLayout(gl);\r
- gl.marginHeight = gl.marginWidth = 0;\r
-\r
- // create the top part for the configuration control\r
-\r
- CustomToggle[] toggles = new CustomToggle[] {\r
- new CustomToggle(\r
- "Explode",\r
- null, //image\r
- "Displays extra margins in the layout."\r
- ) {\r
- @Override\r
- public void onSelected(boolean newState) {\r
- mUseExplodeMode = newState;\r
- recomputeLayout();\r
- }\r
- },\r
- new CustomToggle(\r
- "Outline",\r
- null, //image\r
- "Shows the of all views in the layout."\r
- ) {\r
- @Override\r
- public void onSelected(boolean newState) {\r
- mLayoutCanvas.setShowOutline(newState);\r
- }\r
- }\r
- };\r
-\r
- mConfigListener = new ConfigListener();\r
- mConfigComposite = new ConfigurationComposite(mConfigListener, toggles, parent, SWT.BORDER);\r
-\r
- mSashPalette = new SashForm(parent, SWT.HORIZONTAL);\r
- mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH));\r
-\r
- mPalette = new PaletteComposite(mSashPalette);\r
-\r
- mSashError = new SashForm(mSashPalette, SWT.VERTICAL | SWT.BORDER);\r
- mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));\r
-\r
- mLayoutCanvas = new LayoutCanvas(mRulesEngine, mSashError, SWT.NONE);\r
-\r
- mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY);\r
- mErrorLabel.setEditable(false);\r
- mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));\r
- mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));\r
-\r
- mSashPalette.setWeights(new int[] { 20, 80 });\r
- mSashError.setWeights(new int[] { 80, 20 });\r
- mSashError.setMaximizedControl(mLayoutCanvas);\r
-\r
- setupEditActions();\r
-\r
- // Initialize the state\r
- reloadPalette();\r
- }\r
-\r
- private void setupEditActions() {\r
-\r
- IActionBars actionBars = getEditorSite().getActionBars();\r
-\r
- actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action("Copy") {\r
- @Override\r
- public void run() {\r
- // TODO enable copy only when there's a selection\r
- mLayoutCanvas.onCopy(mClipboard);\r
- }\r
- });\r
-\r
- actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), new Action("Cut") {\r
- @Override\r
- public void run() {\r
- // TODO enable cut only when there's a selection\r
- mLayoutCanvas.onCut(mClipboard);\r
- }\r
- });\r
-\r
- actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), new Action("Paste") {\r
- @Override\r
- public void run() {\r
- mLayoutCanvas.onPaste(mClipboard);\r
- }\r
- });\r
-\r
- actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),\r
- new Action("Select All") {\r
- @Override\r
- public void run() {\r
- mLayoutCanvas.onSelectAll();\r
- }\r
- });\r
- }\r
-\r
- /**\r
- * Switches the stack to display the error label and hide the canvas.\r
- * @param errorFormat The new error to display if not null.\r
- * @param parameters String.format parameters for the error format.\r
- */\r
- private void displayError(String errorFormat, Object...parameters) {\r
- if (errorFormat != null) {\r
- mErrorLabel.setText(String.format(errorFormat, parameters));\r
- }\r
- mSashError.setMaximizedControl(null);\r
- }\r
-\r
- /** Displays the canvas and hides the error label. */\r
- private void hideError() {\r
- mSashError.setMaximizedControl(mLayoutCanvas);\r
- }\r
-\r
- @Override\r
- public void dispose() {\r
- if (mTargetListener != null) {\r
- AdtPlugin.getDefault().removeTargetListener(mTargetListener);\r
- mTargetListener = null;\r
- }\r
-\r
- if (mReloadListener != null) {\r
- LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);\r
- mReloadListener = null;\r
- }\r
-\r
- if (mClipboard != null) {\r
- mClipboard.dispose();\r
- mClipboard = null;\r
- }\r
-\r
- super.dispose();\r
- }\r
-\r
- /**\r
- * Listens to changes from the Configuration UI banner and triggers layout rendering when\r
- * changed. Also provide the Configuration UI with the list of resources/layout to display.\r
- */\r
- private class ConfigListener implements IConfigListener {\r
-\r
- /**\r
- * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.\r
- * <p/>If there is no match, notify the user.\r
- */\r
- public void onConfigurationChange() {\r
- mConfiguredFrameworkRes = mConfiguredProjectRes = null;\r
-\r
- if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) {\r
- return;\r
- }\r
-\r
- // Before doing the normal process, test for the following case.\r
- // - the editor is being opened (or reset for a new input)\r
- // - the file being opened is not the best match for any possible configuration\r
- // - another random compatible config was chosen in the config composite.\r
- // The result is that 'match' will not be the file being edited, but because this is not\r
- // due to a config change, we should not trigger opening the actual best match (also,\r
- // because the editor is still opening the MatchingStrategy woudln't answer true\r
- // and the best match file would open in a different editor).\r
- // So the solution is that if the editor is being created, we just call recomputeLayout\r
- // without looking for a better matching layout file.\r
- if (mLayoutEditor.isCreatingPages()) {\r
- recomputeLayout();\r
- } else {\r
- // get the resources of the file's project.\r
- ProjectResources resources = ResourceManager.getInstance().getProjectResources(\r
- mEditedFile.getProject());\r
-\r
- // from the resources, look for a matching file\r
- ResourceFile match = null;\r
- if (resources != null) {\r
- match = resources.getMatchingFile(mEditedFile.getName(),\r
- ResourceFolderType.LAYOUT,\r
- mConfigComposite.getCurrentConfig());\r
- }\r
-\r
- if (match != null) {\r
- if (match.getFile().equals(mEditedFile) == false) {\r
- try {\r
- // tell the editor that the next replacement file is due to a config\r
- // change.\r
- mLayoutEditor.setNewFileOnConfigChange(true);\r
-\r
- // ask the IDE to open the replacement file.\r
- IDE.openEditor(\r
- getSite().getWorkbenchWindow().getActivePage(),\r
- match.getFile().getIFile());\r
-\r
- // we're done!\r
- return;\r
- } catch (PartInitException e) {\r
- // FIXME: do something!\r
- }\r
- }\r
-\r
- // at this point, we have not opened a new file.\r
-\r
- // Even though the layout doesn't change, the config changed, and referenced\r
- // resources need to be updated.\r
- recomputeLayout();\r
- } else {\r
- // display the error.\r
- FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();\r
- displayError(\r
- "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.",\r
- currentConfig.toDisplayString(),\r
- currentConfig.getFolderName(ResourceFolderType.LAYOUT,\r
- Sdk.getCurrent().getTarget(mEditedFile.getProject())),\r
- mEditedFile.getName());\r
- }\r
- }\r
- }\r
-\r
- public void onThemeChange() {\r
- recomputeLayout();\r
- }\r
-\r
- public void onClippingChange() {\r
- recomputeLayout();\r
- }\r
-\r
- public void onCreate() {\r
- LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(),\r
- mEditedFile.getName(),\r
- Sdk.getCurrent().getTarget(mEditedFile.getProject()),\r
- mConfigComposite.getCurrentConfig());\r
- if (dialog.open() == Dialog.OK) {\r
- final FolderConfiguration config = new FolderConfiguration();\r
- dialog.getConfiguration(config);\r
-\r
- createAlternateLayout(config);\r
- }\r
- }\r
-\r
- public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {\r
- if (mConfiguredFrameworkRes == null && mConfigComposite != null) {\r
- ProjectResources frameworkRes = getFrameworkResources();\r
-\r
- if (frameworkRes == null) {\r
- AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");\r
- } else {\r
- // get the framework resource values based on the current config\r
- mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(\r
- mConfigComposite.getCurrentConfig());\r
- }\r
- }\r
-\r
- return mConfiguredFrameworkRes;\r
- }\r
-\r
- public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {\r
- if (mConfiguredProjectRes == null && mConfigComposite != null) {\r
- ProjectResources project = getProjectResources();\r
-\r
- // make sure they are loaded\r
- project.loadAll();\r
-\r
- // get the project resource values based on the current config\r
- mConfiguredProjectRes = project.getConfiguredResources(\r
- mConfigComposite.getCurrentConfig());\r
- }\r
-\r
- return mConfiguredProjectRes;\r
- }\r
-\r
- /**\r
- * Returns a {@link ProjectResources} for the framework resources.\r
- * @return the framework resources or null if not found.\r
- */\r
- public ProjectResources getFrameworkResources() {\r
- if (mEditedFile != null) {\r
- Sdk currentSdk = Sdk.getCurrent();\r
- if (currentSdk != null) {\r
- IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
-\r
- if (target != null) {\r
- AndroidTargetData data = currentSdk.getTargetData(target);\r
-\r
- if (data != null) {\r
- return data.getFrameworkResources();\r
- }\r
- }\r
- }\r
- }\r
-\r
- return null;\r
- }\r
-\r
- public ProjectResources getProjectResources() {\r
- if (mEditedFile != null) {\r
- ResourceManager manager = ResourceManager.getInstance();\r
- return manager.getProjectResources(mEditedFile.getProject());\r
- }\r
-\r
- return null;\r
- }\r
-\r
- /**\r
- * Creates a new layout file from the specified {@link FolderConfiguration}.\r
- */\r
- private void createAlternateLayout(final FolderConfiguration config) {\r
- new Job("Create Alternate Resource") {\r
- @Override\r
- protected IStatus run(IProgressMonitor monitor) {\r
- // get the folder name\r
- String folderName = config.getFolderName(ResourceFolderType.LAYOUT,\r
- Sdk.getCurrent().getTarget(mEditedFile.getProject()));\r
- try {\r
-\r
- // look to see if it exists.\r
- // get the res folder\r
- IFolder res = (IFolder)mEditedFile.getParent().getParent();\r
- String path = res.getLocation().toOSString();\r
-\r
- File newLayoutFolder = new File(path + File.separator + folderName);\r
- if (newLayoutFolder.isFile()) {\r
- // this should not happen since aapt would have complained\r
- // before, but if one disable the automatic build, this could\r
- // happen.\r
- String message = String.format("File 'res/%1$s' is in the way!",\r
- folderName);\r
-\r
- AdtPlugin.displayError("Layout Creation", message);\r
-\r
- return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);\r
- } else if (newLayoutFolder.exists() == false) {\r
- // create it.\r
- newLayoutFolder.mkdir();\r
- }\r
-\r
- // now create the file\r
- File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +\r
- File.separator + mEditedFile.getName());\r
-\r
- newLayoutFile.createNewFile();\r
-\r
- InputStream input = mEditedFile.getContents();\r
-\r
- FileOutputStream fos = new FileOutputStream(newLayoutFile);\r
-\r
- byte[] data = new byte[512];\r
- int count;\r
- while ((count = input.read(data)) != -1) {\r
- fos.write(data, 0, count);\r
- }\r
-\r
- input.close();\r
- fos.close();\r
-\r
- // refreshes the res folder to show up the new\r
- // layout folder (if needed) and the file.\r
- // We use a progress monitor to catch the end of the refresh\r
- // to trigger the edit of the new file.\r
- res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {\r
- public void done() {\r
- mConfigComposite.getDisplay().asyncExec(new Runnable() {\r
- public void run() {\r
- onConfigurationChange();\r
- }\r
- });\r
- }\r
-\r
- public void beginTask(String name, int totalWork) {\r
- // pass\r
- }\r
-\r
- public void internalWorked(double work) {\r
- // pass\r
- }\r
-\r
- public boolean isCanceled() {\r
- // pass\r
- return false;\r
- }\r
-\r
- public void setCanceled(boolean value) {\r
- // pass\r
- }\r
-\r
- public void setTaskName(String name) {\r
- // pass\r
- }\r
-\r
- public void subTask(String name) {\r
- // pass\r
- }\r
-\r
- public void worked(int work) {\r
- // pass\r
- }\r
- });\r
- } catch (IOException e2) {\r
- String message = String.format(\r
- "Failed to create File 'res/%1$s/%2$s' : %3$s",\r
- folderName, mEditedFile.getName(), e2.getMessage());\r
-\r
- AdtPlugin.displayError("Layout Creation", message);\r
-\r
- return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,\r
- message, e2);\r
- } catch (CoreException e2) {\r
- String message = String.format(\r
- "Failed to create File 'res/%1$s/%2$s' : %3$s",\r
- folderName, mEditedFile.getName(), e2.getMessage());\r
-\r
- AdtPlugin.displayError("Layout Creation", message);\r
-\r
- return e2.getStatus();\r
- }\r
-\r
- return Status.OK_STATUS;\r
-\r
- }\r
- }.schedule();\r
- }\r
- }\r
-\r
- /**\r
- * Listens to target changed in the current project, to trigger a new layout rendering.\r
- */\r
- private class TargetListener implements ITargetChangeListener {\r
-\r
- public void onProjectTargetChange(IProject changedProject) {\r
- if (changedProject != null && changedProject.equals(getProject())) {\r
- updateEditor();\r
- }\r
- }\r
-\r
- public void onTargetLoaded(IAndroidTarget target) {\r
- IProject project = getProject();\r
- if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {\r
- updateEditor();\r
- }\r
- }\r
-\r
- public void onSdkLoaded() {\r
- Sdk currentSdk = Sdk.getCurrent();\r
- if (currentSdk != null) {\r
- IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
- if (target != null) {\r
- mConfigComposite.onSdkLoaded(target);\r
- mConfigListener.onConfigurationChange();\r
- }\r
- }\r
- }\r
-\r
- private void updateEditor() {\r
- mLayoutEditor.commitPages(false /* onSave */);\r
-\r
- // because the target changed we must reset the configured resources.\r
- mConfiguredFrameworkRes = mConfiguredProjectRes = null;\r
-\r
- // make sure we remove the custom view loader, since its parent class loader is the\r
- // bridge class loader.\r
- mProjectCallback = null;\r
-\r
- // recreate the ui root node always, this will also call onTargetChange\r
- // on the config composite\r
- mLayoutEditor.initUiRootNode(true /*force*/);\r
- }\r
-\r
- private IProject getProject() {\r
- return getLayoutEditor().getProject();\r
- }\r
- }\r
-\r
- // ----------------\r
-\r
- /**\r
- * Save operation in the Graphical Editor Part.\r
- * <p/>\r
- * In our workflow, the model is owned by the Structured XML Editor.\r
- * The graphical layout editor just displays it -- thus we don't really\r
- * save anything here.\r
- * <p/>\r
- * This must NOT call the parent editor part. At the contrary, the parent editor\r
- * part will call this *after* having done the actual save operation.\r
- * <p/>\r
- * The only action this editor must do is mark the undo command stack as\r
- * being no longer dirty.\r
- */\r
- @Override\r
- public void doSave(IProgressMonitor monitor) {\r
- // TODO implement a command stack\r
-// getCommandStack().markSaveLocation();\r
-// firePropertyChange(PROP_DIRTY);\r
- }\r
-\r
- /**\r
- * Save operation in the Graphical Editor Part.\r
- * <p/>\r
- * In our workflow, the model is owned by the Structured XML Editor.\r
- * The graphical layout editor just displays it -- thus we don't really\r
- * save anything here.\r
- */\r
- @Override\r
- public void doSaveAs() {\r
- // pass\r
- }\r
-\r
- /**\r
- * In our workflow, the model is owned by the Structured XML Editor.\r
- * The graphical layout editor just displays it -- thus we don't really\r
- * save anything here.\r
- */\r
- @Override\r
- public boolean isDirty() {\r
- return false;\r
- }\r
-\r
- /**\r
- * In our workflow, the model is owned by the Structured XML Editor.\r
- * The graphical layout editor just displays it -- thus we don't really\r
- * save anything here.\r
- */\r
- @Override\r
- public boolean isSaveAsAllowed() {\r
- return false;\r
- }\r
-\r
- @Override\r
- public void setFocus() {\r
- // TODO Auto-generated method stub\r
-\r
- }\r
-\r
- /**\r
- * Responds to a page change that made the Graphical editor page the activated page.\r
- */\r
- public void activated() {\r
- if (mNeedsRecompute || mNeedsXmlReload) {\r
- recomputeLayout();\r
- }\r
- }\r
-\r
- /**\r
- * Responds to a page change that made the Graphical editor page the deactivated page\r
- */\r
- public void deactivated() {\r
- // nothing to be done here for now.\r
- }\r
-\r
- /**\r
- * Opens and initialize the editor with a new file.\r
- * @param file the file being edited.\r
- */\r
- public void openFile(IFile file) {\r
- mEditedFile = file;\r
- mConfigComposite.openFile(mEditedFile);\r
-\r
- if (mReloadListener == null) {\r
- mReloadListener = new ReloadListener();\r
- LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);\r
- }\r
-\r
- if (mRulesEngine == null) {\r
- mRulesEngine = new RulesEngine(mEditedFile.getProject());\r
- if (mLayoutCanvas != null) {\r
- mLayoutCanvas.setRulesEngine(mRulesEngine);\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Resets the editor with a replacement file.\r
- * @param file the replacement file.\r
- */\r
- public void replaceFile(IFile file) {\r
- mEditedFile = file;\r
- mConfigComposite.replaceFile(mEditedFile);\r
- }\r
-\r
- /**\r
- * Resets the editor with a replacement file coming from a config change in the config\r
- * selector.\r
- * @param file the replacement file.\r
- */\r
- public void changeFileOnNewConfig(IFile file) {\r
- mEditedFile = file;\r
- mConfigComposite.changeFileOnNewConfig(mEditedFile);\r
- }\r
-\r
- public void onTargetChange() {\r
- mConfigComposite.onTargetChange();\r
- mConfigListener.onConfigurationChange();\r
- }\r
-\r
- public void onSdkChange() {\r
- Sdk currentSdk = Sdk.getCurrent();\r
- if (currentSdk != null) {\r
- IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
- if (target != null) {\r
- mConfigComposite.onSdkLoaded(target);\r
- mConfigListener.onConfigurationChange();\r
- }\r
- }\r
- }\r
-\r
- public Clipboard getClipboard() {\r
- return mClipboard;\r
- }\r
-\r
- public LayoutEditor getLayoutEditor() {\r
- return mLayoutEditor;\r
- }\r
-\r
- public UiDocumentNode getModel() {\r
- return mLayoutEditor.getUiRootNode();\r
- }\r
-\r
- public SelectionSynchronizer getSelectionSynchronizer() {\r
- // TODO Auto-generated method stub\r
- return null;\r
- }\r
-\r
- /**\r
- * Callback for XML model changed. Only update/recompute the layout if the editor is visible\r
- */\r
- public void onXmlModelChanged() {\r
- if (mLayoutEditor.isGraphicalEditorActive()) {\r
- doXmlReload(true /* force */);\r
- recomputeLayout();\r
- } else {\r
- mNeedsXmlReload = true;\r
- }\r
- }\r
-\r
- /**\r
- * Actually performs the XML reload\r
- * @see #onXmlModelChanged()\r
- */\r
- private void doXmlReload(boolean force) {\r
- if (force || mNeedsXmlReload) {\r
-\r
- // TODO : update the mLayoutCanvas, preserving the current selection if possible.\r
-\r
-// GraphicalViewer viewer = getGraphicalViewer();\r
-//\r
-// // try to preserve the selection before changing the content\r
-// SelectionManager selMan = viewer.getSelectionManager();\r
-// ISelection selection = selMan.getSelection();\r
-//\r
-// try {\r
-// viewer.setContents(getModel());\r
-// } finally {\r
-// selMan.setSelection(selection);\r
-// }\r
-\r
- mNeedsXmlReload = false;\r
- }\r
- }\r
-\r
- public void recomputeLayout() {\r
- doXmlReload(false /* force */);\r
- try {\r
- // check that the resource exists. If the file is opened but the project is closed\r
- // or deleted for some reason (changed from outside of eclipse), then this will\r
- // return false;\r
- if (mEditedFile.exists() == false) {\r
- displayError("Resource '%1$s' does not exist.",\r
- mEditedFile.getFullPath().toString());\r
- return;\r
- }\r
-\r
- IProject iProject = mEditedFile.getProject();\r
-\r
- if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {\r
- String message = String.format("%1$s is out of sync. Please refresh.",\r
- mEditedFile.getName());\r
-\r
- displayError(message);\r
-\r
- // also print it in the error console.\r
- AdtPlugin.printErrorToConsole(iProject.getName(), message);\r
- return;\r
- }\r
-\r
- Sdk currentSdk = Sdk.getCurrent();\r
- if (currentSdk != null) {\r
- IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
- if (target == null) {\r
- displayError("The project target is not set.");\r
- return;\r
- }\r
-\r
- AndroidTargetData data = currentSdk.getTargetData(target);\r
- if (data == null) {\r
- // It can happen that the workspace refreshes while the SDK is loading its\r
- // data, which could trigger a redraw of the opened layout if some resources\r
- // changed while Eclipse is closed.\r
- // In this case data could be null, but this is not an error.\r
- // We can just silently return, as all the opened editors are automatically\r
- // refreshed once the SDK finishes loading.\r
- LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);\r
- switch (targetLoadStatus) {\r
- case LOADING:\r
- displayError("The project target (%1$s) is still loading.\n%2$s will refresh automatically once the process is finished.",\r
- target.getName(), mEditedFile.getName());\r
-\r
- break;\r
- case FAILED: // known failure\r
- case LOADED: // success but data isn't loaded?!?!\r
- displayError("The project target (%s) was not properly loaded.",\r
- target.getName());\r
- break;\r
- }\r
-\r
- return;\r
- }\r
-\r
- // check there is actually a model (maybe the file is empty).\r
- UiDocumentNode model = getModel();\r
-\r
- if (model.getUiChildren().size() == 0) {\r
- displayError("No Xml content. Go to the Outline view and add nodes.");\r
- return;\r
- }\r
-\r
- LayoutBridge bridge = data.getLayoutBridge();\r
-\r
- if (bridge.bridge != null) { // bridge can never be null.\r
- ResourceManager resManager = ResourceManager.getInstance();\r
-\r
- ProjectResources projectRes = resManager.getProjectResources(iProject);\r
- if (projectRes == null) {\r
- displayError("Missing project resources.");\r
- return;\r
- }\r
-\r
- // get the resources of the file's project.\r
- Map<String, Map<String, IResourceValue>> configuredProjectRes =\r
- mConfigListener.getConfiguredProjectResources();\r
-\r
- // get the framework resources\r
- Map<String, Map<String, IResourceValue>> frameworkResources =\r
- mConfigListener.getConfiguredFrameworkResources();\r
-\r
- if (configuredProjectRes != null && frameworkResources != null) {\r
- if (mProjectCallback == null) {\r
- mProjectCallback = new ProjectCallback(\r
- bridge.classLoader, projectRes, iProject);\r
- }\r
-\r
- if (mLogger == null) {\r
- mLogger = new ILayoutLog() {\r
- public void error(String message) {\r
- AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);\r
- }\r
-\r
- public void error(Throwable error) {\r
- String message = error.getMessage();\r
- if (message == null) {\r
- message = error.getClass().getName();\r
- }\r
-\r
- PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());\r
- error.printStackTrace(ps);\r
- }\r
-\r
- public void warning(String message) {\r
- AdtPlugin.printToConsole(mEditedFile.getName(), message);\r
- }\r
- };\r
- }\r
-\r
- // get the selected theme\r
- String theme = mConfigComposite.getTheme();\r
- if (theme != null) {\r
- // Compute the layout\r
- Rectangle rect = getBounds();\r
-\r
- int width = rect.width;\r
- int height = rect.height;\r
- if (mUseExplodeMode) {\r
- // compute how many padding in x and y will bump the screen size\r
- ExplodedRenderingHelper helper = new ExplodedRenderingHelper(\r
- getModel(), iProject);\r
-\r
- // there are 2 paddings for each view\r
- // left and right, or top and bottom.\r
- int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;\r
-\r
- width += helper.getWidthPadding() * paddingValue;\r
- height += helper.getHeightPadding() * paddingValue;\r
- }\r
-\r
- int density = mConfigComposite.getDensity().getDpiValue();\r
- float xdpi = mConfigComposite.getXDpi();\r
- float ydpi = mConfigComposite.getYDpi();\r
- boolean isProjectTheme = mConfigComposite.isProjectTheme();\r
-\r
- UiElementPullParser parser = new UiElementPullParser(getModel(),\r
- mUseExplodeMode, density, xdpi, iProject);\r
-\r
- ILayoutResult result = computeLayout(bridge, parser,\r
- iProject /* projectKey */,\r
- width, height, !mConfigComposite.getClipping(),\r
- density, xdpi, ydpi,\r
- theme, isProjectTheme,\r
- configuredProjectRes, frameworkResources, mProjectCallback,\r
- mLogger);\r
-\r
- mLayoutCanvas.setResult(result);\r
-\r
- // update the UiElementNode with the layout info.\r
- if (result.getSuccess() == ILayoutResult.SUCCESS) {\r
- hideError();\r
- } else {\r
- displayError(result.getErrorMessage());\r
- }\r
-\r
- model.refreshUi();\r
- }\r
- }\r
- } else {\r
- // SDK is loaded but not the layout library!\r
-\r
- // check whether the bridge managed to load, or not\r
- if (bridge.status == LoadStatus.LOADING) {\r
- displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",\r
- mEditedFile.getName());\r
- } else {\r
- displayError("Eclipse failed to load the framework information and the layout library!");\r
- }\r
- }\r
- } else {\r
- displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",\r
- mEditedFile.getName());\r
- }\r
- } finally {\r
- // no matter the result, we are done doing the recompute based on the latest\r
- // resource/code change.\r
- mNeedsRecompute = false;\r
- }\r
- }\r
-\r
- /**\r
- * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on\r
- * the implementation API level.\r
- *\r
- * Implementation detail: the bridge's computeLayout() method already returns a newly\r
- * allocated ILayoutResult.\r
- */\r
- @SuppressWarnings("deprecation")\r
- private static ILayoutResult computeLayout(LayoutBridge bridge,\r
- IXmlPullParser layoutDescription, Object projectKey,\r
- int screenWidth, int screenHeight, boolean renderFullSize,\r
- int density, float xdpi, float ydpi,\r
- String themeName, boolean isProjectTheme,\r
- Map<String, Map<String, IResourceValue>> projectResources,\r
- Map<String, Map<String, IResourceValue>> frameworkResources,\r
- IProjectCallback projectCallback, ILayoutLog logger) {\r
-\r
- if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {\r
- // newest API with support for "render full height"\r
- // TODO: link boolean to UI.\r
- return bridge.bridge.computeLayout(layoutDescription,\r
- projectKey, screenWidth, screenHeight, renderFullSize,\r
- density, xdpi, ydpi,\r
- themeName, isProjectTheme,\r
- projectResources, frameworkResources, projectCallback,\r
- logger);\r
- } else if (bridge.apiLevel == 3) {\r
- // newer api with density support.\r
- return bridge.bridge.computeLayout(layoutDescription,\r
- projectKey, screenWidth, screenHeight, density, xdpi, ydpi,\r
- themeName, isProjectTheme,\r
- projectResources, frameworkResources, projectCallback,\r
- logger);\r
- } else if (bridge.apiLevel == 2) {\r
- // api with boolean for separation of project/framework theme\r
- return bridge.bridge.computeLayout(layoutDescription,\r
- projectKey, screenWidth, screenHeight, themeName, isProjectTheme,\r
- projectResources, frameworkResources, projectCallback,\r
- logger);\r
- } else {\r
- // oldest api with no density/dpi, and project theme boolean mixed\r
- // into the theme name.\r
-\r
- // change the string if it's a custom theme to make sure we can\r
- // differentiate them\r
- if (isProjectTheme) {\r
- themeName = "*" + themeName; //$NON-NLS-1$\r
- }\r
-\r
- return bridge.bridge.computeLayout(layoutDescription,\r
- projectKey, screenWidth, screenHeight, themeName,\r
- projectResources, frameworkResources, projectCallback,\r
- logger);\r
- }\r
- }\r
-\r
- public Rectangle getBounds() {\r
- return mConfigComposite.getScreenBounds();\r
- }\r
-\r
- public void reloadPalette() {\r
- if (mPalette != null) {\r
- mPalette.reloadPalette(mLayoutEditor.getTargetData());\r
- }\r
- }\r
-\r
- /**\r
- * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node\r
- * created by {@link ElementCreateCommand#execute()}.\r
- *\r
- * @param uiNodeModel The {@link UiElementNode} to select.\r
- */\r
- public void selectModel(UiElementNode uiNodeModel) {\r
-\r
- // TODO this method was useful for GLE1. We may not need it anymore now.\r
-\r
-// GraphicalViewer viewer = getGraphicalViewer();\r
-//\r
-// // Give focus to the graphical viewer (in case the outline has it)\r
-// viewer.getControl().forceFocus();\r
-//\r
-// Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);\r
-//\r
-// if (editPart instanceof EditPart) {\r
-// viewer.select((EditPart)editPart);\r
-// }\r
- }\r
-\r
- private class ReloadListener implements ILayoutReloadListener {\r
- /*\r
- * Called when the file changes triggered a redraw of the layout\r
- */\r
- public void reloadLayout(ChangeFlags flags) {\r
- boolean recompute = false;\r
-\r
- if (flags.rClass) {\r
- recompute = true;\r
- if (mEditedFile != null) {\r
- ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(\r
- mEditedFile.getProject());\r
-\r
- if (projectRes != null) {\r
- projectRes.resetDynamicIds();\r
- }\r
- }\r
- }\r
-\r
- if (flags.localeList) {\r
- // the locale list *potentially* changed so we update the locale in the\r
- // config composite.\r
- // However there's no recompute, as it could not be needed\r
- // (for instance a new layout)\r
- // If a resource that's not a layout changed this will trigger a recompute anyway.\r
- mLayoutCanvas.getDisplay().asyncExec(new Runnable() {\r
- public void run() {\r
- mConfigComposite.updateLocales();\r
- }\r
- });\r
- }\r
-\r
- if (flags.resources) {\r
- recompute = true;\r
-\r
- // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.\r
-\r
- // force a reparse in case a value XML file changed.\r
- mConfiguredProjectRes = null;\r
-\r
- // clear the cache in the bridge in case a bitmap/9-patch changed.\r
- IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());\r
- if (target != null) {\r
-\r
- AndroidTargetData data = Sdk.getCurrent().getTargetData(target);\r
- if (data != null) {\r
- LayoutBridge bridge = data.getLayoutBridge();\r
-\r
- if (bridge.bridge != null) {\r
- bridge.bridge.clearCaches(mEditedFile.getProject());\r
- }\r
- }\r
- }\r
- }\r
-\r
- if (flags.code) {\r
- // only recompute if the custom view loader was used to load some code.\r
- if (mProjectCallback != null && mProjectCallback.isUsed()) {\r
- mProjectCallback = null;\r
- recompute = true;\r
- }\r
- }\r
-\r
- if (recompute) {\r
- mLayoutCanvas.getDisplay().asyncExec(new Runnable() {\r
- public void run() {\r
- if (mLayoutEditor.isGraphicalEditorActive()) {\r
- recomputeLayout();\r
- } else {\r
- mNeedsRecompute = true;\r
- }\r
- }\r
- });\r
- }\r
- }\r
- }\r
-}\r
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper;
+import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.layoutlib.api.ILayoutBridge;
+import com.android.layoutlib.api.ILayoutLog;
+import com.android.layoutlib.api.ILayoutResult;
+import com.android.layoutlib.api.IProjectCallback;
+import com.android.layoutlib.api.IResourceValue;
+import com.android.layoutlib.api.IXmlPullParser;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.gef.ui.parts.SelectionSynchronizer;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Map;
+
+/**
+ * Graphical layout editor part, version 2.
+ *
+ * @since GLE2
+ *
+ * TODO List:
+ * - display error icon
+ * - finish palette (see palette's todo list)
+ * - finish canvas (see canva's todo list)
+ * - completly rethink the property panel
+ * - link to the existing outline editor (prolly reuse/adapt for simplier model and will need
+ * to adapt the selection synchronizer.)
+ */
+public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutEditor {
+
+ /*
+ * Useful notes:
+ * To understand Drag'n'drop:
+ * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
+ */
+
+ /** Reference to the layout editor */
+ private final LayoutEditor mLayoutEditor;
+
+ /** reference to the file being edited. Can also be used to access the {@link IProject}. */
+ private IFile mEditedFile;
+
+ /** The current clipboard. Must be disposed later. */
+ private Clipboard mClipboard;
+
+ /** The configuration composite at the top of the layout editor. */
+ private ConfigurationComposite mConfigComposite;
+
+ /** The sash that splits the palette from the canvas. */
+ private SashForm mSashPalette;
+ private SashForm mSashError;
+
+ /** The palette displayed on the left of the sash. */
+ private PaletteComposite mPalette;
+
+ /** The layout canvas displayed to the right of the sash. */
+ private LayoutCanvas mLayoutCanvas;
+
+ /** The Groovy Rules Engine associated with this editor. It is project-specific. */
+ private RulesEngine mRulesEngine;
+
+ private StyledText mErrorLabel;
+
+ private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
+ private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
+ private ProjectCallback mProjectCallback;
+ private ILayoutLog mLogger;
+
+ private boolean mNeedsXmlReload = false;
+ private boolean mNeedsRecompute = false;
+
+ private TargetListener mTargetListener;
+
+ private ConfigListener mConfigListener;
+
+ private ReloadListener mReloadListener;
+
+ protected boolean mUseExplodeMode;
+
+
+ public GraphicalEditorPart(LayoutEditor layoutEditor) {
+ mLayoutEditor = layoutEditor;
+ setPartName("Graphical Layout");
+ }
+
+ // ------------------------------------
+ // Methods overridden from base classes
+ //------------------------------------
+
+ /**
+ * Initializes the editor part with a site and input.
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ setSite(site);
+ useNewEditorInput(input);
+
+ if (mTargetListener == null) {
+ mTargetListener = new TargetListener();
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+ }
+ }
+
+ private void useNewEditorInput(IEditorInput input) throws PartInitException {
+ // The contract of init() mentions we need to fail if we can't understand the input.
+ if (!(input instanceof FileEditorInput)) {
+ throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$
+ input == null ? "null" : input.toString()); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+
+ Display d = parent.getDisplay();
+ mClipboard = new Clipboard(d);
+
+ GridLayout gl = new GridLayout(1, false);
+ parent.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ // create the top part for the configuration control
+
+ CustomToggle[] toggles = new CustomToggle[] {
+ new CustomToggle(
+ "Explode",
+ null, //image
+ "Displays extra margins in the layout."
+ ) {
+ @Override
+ public void onSelected(boolean newState) {
+ mUseExplodeMode = newState;
+ recomputeLayout();
+ }
+ },
+ new CustomToggle(
+ "Outline",
+ null, //image
+ "Shows the of all views in the layout."
+ ) {
+ @Override
+ public void onSelected(boolean newState) {
+ mLayoutCanvas.setShowOutline(newState);
+ }
+ }
+ };
+
+ mConfigListener = new ConfigListener();
+ mConfigComposite = new ConfigurationComposite(mConfigListener, toggles, parent, SWT.BORDER);
+
+ mSashPalette = new SashForm(parent, SWT.HORIZONTAL);
+ mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mPalette = new PaletteComposite(mSashPalette);
+
+ mSashError = new SashForm(mSashPalette, SWT.VERTICAL | SWT.BORDER);
+ mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mLayoutCanvas = new LayoutCanvas(mRulesEngine, mSashError, SWT.NONE);
+
+ mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY);
+ mErrorLabel.setEditable(false);
+ mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+
+ mSashPalette.setWeights(new int[] { 20, 80 });
+ mSashError.setWeights(new int[] { 80, 20 });
+ mSashError.setMaximizedControl(mLayoutCanvas);
+
+ setupEditActions();
+
+ // Initialize the state
+ reloadPalette();
+ }
+
+ private void setupEditActions() {
+
+ IActionBars actionBars = getEditorSite().getActionBars();
+
+ actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action("Copy") {
+ @Override
+ public void run() {
+ // TODO enable copy only when there's a selection
+ mLayoutCanvas.onCopy(mClipboard);
+ }
+ });
+
+ actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), new Action("Cut") {
+ @Override
+ public void run() {
+ // TODO enable cut only when there's a selection
+ mLayoutCanvas.onCut(mClipboard);
+ }
+ });
+
+ actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), new Action("Paste") {
+ @Override
+ public void run() {
+ mLayoutCanvas.onPaste(mClipboard);
+ }
+ });
+
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ new Action("Select All") {
+ @Override
+ public void run() {
+ mLayoutCanvas.onSelectAll();
+ }
+ });
+ }
+
+ /**
+ * Switches the stack to display the error label and hide the canvas.
+ * @param errorFormat The new error to display if not null.
+ * @param parameters String.format parameters for the error format.
+ */
+ private void displayError(String errorFormat, Object...parameters) {
+ if (errorFormat != null) {
+ mErrorLabel.setText(String.format(errorFormat, parameters));
+ }
+ mSashError.setMaximizedControl(null);
+ }
+
+ /** Displays the canvas and hides the error label. */
+ private void hideError() {
+ mSashError.setMaximizedControl(mLayoutCanvas);
+ }
+
+ @Override
+ public void dispose() {
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ if (mReloadListener != null) {
+ LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);
+ mReloadListener = null;
+ }
+
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Listens to changes from the Configuration UI banner and triggers layout rendering when
+ * changed. Also provide the Configuration UI with the list of resources/layout to display.
+ */
+ private class ConfigListener implements IConfigListener {
+
+ /**
+ * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.
+ * <p/>If there is no match, notify the user.
+ */
+ public void onConfigurationChange() {
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+
+ if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) {
+ return;
+ }
+
+ // Before doing the normal process, test for the following case.
+ // - the editor is being opened (or reset for a new input)
+ // - the file being opened is not the best match for any possible configuration
+ // - another random compatible config was chosen in the config composite.
+ // The result is that 'match' will not be the file being edited, but because this is not
+ // due to a config change, we should not trigger opening the actual best match (also,
+ // because the editor is still opening the MatchingStrategy woudln't answer true
+ // and the best match file would open in a different editor).
+ // So the solution is that if the editor is being created, we just call recomputeLayout
+ // without looking for a better matching layout file.
+ if (mLayoutEditor.isCreatingPages()) {
+ recomputeLayout();
+ } else {
+ // get the resources of the file's project.
+ ProjectResources resources = ResourceManager.getInstance().getProjectResources(
+ mEditedFile.getProject());
+
+ // from the resources, look for a matching file
+ ResourceFile match = null;
+ if (resources != null) {
+ match = resources.getMatchingFile(mEditedFile.getName(),
+ ResourceFolderType.LAYOUT,
+ mConfigComposite.getCurrentConfig());
+ }
+
+ if (match != null) {
+ if (match.getFile().equals(mEditedFile) == false) {
+ try {
+ // tell the editor that the next replacement file is due to a config
+ // change.
+ mLayoutEditor.setNewFileOnConfigChange(true);
+
+ // ask the IDE to open the replacement file.
+ IDE.openEditor(
+ getSite().getWorkbenchWindow().getActivePage(),
+ match.getFile().getIFile());
+
+ // we're done!
+ return;
+ } catch (PartInitException e) {
+ // FIXME: do something!
+ }
+ }
+
+ // at this point, we have not opened a new file.
+
+ // Even though the layout doesn't change, the config changed, and referenced
+ // resources need to be updated.
+ recomputeLayout();
+ } else {
+ // display the error.
+ FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();
+ displayError(
+ "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.",
+ currentConfig.toDisplayString(),
+ currentConfig.getFolderName(ResourceFolderType.LAYOUT,
+ Sdk.getCurrent().getTarget(mEditedFile.getProject())),
+ mEditedFile.getName());
+ }
+ }
+ }
+
+ public void onThemeChange() {
+ recomputeLayout();
+ }
+
+ public void onClippingChange() {
+ recomputeLayout();
+ }
+
+ public void onCreate() {
+ LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(),
+ mEditedFile.getName(),
+ Sdk.getCurrent().getTarget(mEditedFile.getProject()),
+ mConfigComposite.getCurrentConfig());
+ if (dialog.open() == Dialog.OK) {
+ final FolderConfiguration config = new FolderConfiguration();
+ dialog.getConfiguration(config);
+
+ createAlternateLayout(config);
+ }
+ }
+
+ public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {
+ if (mConfiguredFrameworkRes == null && mConfigComposite != null) {
+ ProjectResources frameworkRes = getFrameworkResources();
+
+ if (frameworkRes == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
+ } else {
+ // get the framework resource values based on the current config
+ mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
+ mConfigComposite.getCurrentConfig());
+ }
+ }
+
+ return mConfiguredFrameworkRes;
+ }
+
+ public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {
+ if (mConfiguredProjectRes == null && mConfigComposite != null) {
+ ProjectResources project = getProjectResources();
+
+ // make sure they are loaded
+ project.loadAll();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = project.getConfiguredResources(
+ mConfigComposite.getCurrentConfig());
+ }
+
+ return mConfiguredProjectRes;
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources.
+ * @return the framework resources or null if not found.
+ */
+ public ProjectResources getFrameworkResources() {
+ if (mEditedFile != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+
+ if (data != null) {
+ return data.getFrameworkResources();
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public ProjectResources getProjectResources() {
+ if (mEditedFile != null) {
+ ResourceManager manager = ResourceManager.getInstance();
+ return manager.getProjectResources(mEditedFile.getProject());
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a new layout file from the specified {@link FolderConfiguration}.
+ */
+ private void createAlternateLayout(final FolderConfiguration config) {
+ new Job("Create Alternate Resource") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ // get the folder name
+ String folderName = config.getFolderName(ResourceFolderType.LAYOUT,
+ Sdk.getCurrent().getTarget(mEditedFile.getProject()));
+ try {
+
+ // look to see if it exists.
+ // get the res folder
+ IFolder res = (IFolder)mEditedFile.getParent().getParent();
+ String path = res.getLocation().toOSString();
+
+ File newLayoutFolder = new File(path + File.separator + folderName);
+ if (newLayoutFolder.isFile()) {
+ // this should not happen since aapt would have complained
+ // before, but if one disable the automatic build, this could
+ // happen.
+ String message = String.format("File 'res/%1$s' is in the way!",
+ folderName);
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
+ } else if (newLayoutFolder.exists() == false) {
+ // create it.
+ newLayoutFolder.mkdir();
+ }
+
+ // now create the file
+ File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
+ File.separator + mEditedFile.getName());
+
+ newLayoutFile.createNewFile();
+
+ InputStream input = mEditedFile.getContents();
+
+ FileOutputStream fos = new FileOutputStream(newLayoutFile);
+
+ byte[] data = new byte[512];
+ int count;
+ while ((count = input.read(data)) != -1) {
+ fos.write(data, 0, count);
+ }
+
+ input.close();
+ fos.close();
+
+ // refreshes the res folder to show up the new
+ // layout folder (if needed) and the file.
+ // We use a progress monitor to catch the end of the refresh
+ // to trigger the edit of the new file.
+ res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
+ public void done() {
+ mConfigComposite.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ onConfigurationChange();
+ }
+ });
+ }
+
+ public void beginTask(String name, int totalWork) {
+ // pass
+ }
+
+ public void internalWorked(double work) {
+ // pass
+ }
+
+ public boolean isCanceled() {
+ // pass
+ return false;
+ }
+
+ public void setCanceled(boolean value) {
+ // pass
+ }
+
+ public void setTaskName(String name) {
+ // pass
+ }
+
+ public void subTask(String name) {
+ // pass
+ }
+
+ public void worked(int work) {
+ // pass
+ }
+ });
+ } catch (IOException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mEditedFile.getName(), e2.getMessage());
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ message, e2);
+ } catch (CoreException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mEditedFile.getName(), e2.getMessage());
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+
+ }
+ }.schedule();
+ }
+ }
+
+ /**
+ * Listens to target changed in the current project, to trigger a new layout rendering.
+ */
+ private class TargetListener implements ITargetChangeListener {
+
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject != null && changedProject.equals(getProject())) {
+ updateEditor();
+ }
+ }
+
+ public void onTargetLoaded(IAndroidTarget target) {
+ IProject project = getProject();
+ if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
+ updateEditor();
+ }
+ }
+
+ public void onSdkLoaded() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target != null) {
+ mConfigComposite.onSdkLoaded(target);
+ mConfigListener.onConfigurationChange();
+ }
+ }
+ }
+
+ private void updateEditor() {
+ mLayoutEditor.commitPages(false /* onSave */);
+
+ // because the target changed we must reset the configured resources.
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+
+ // make sure we remove the custom view loader, since its parent class loader is the
+ // bridge class loader.
+ mProjectCallback = null;
+
+ // recreate the ui root node always, this will also call onTargetChange
+ // on the config composite
+ mLayoutEditor.initUiRootNode(true /*force*/);
+ }
+
+ private IProject getProject() {
+ return getLayoutEditor().getProject();
+ }
+ }
+
+ // ----------------
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ * <p/>
+ * This must NOT call the parent editor part. At the contrary, the parent editor
+ * part will call this *after* having done the actual save operation.
+ * <p/>
+ * The only action this editor must do is mark the undo command stack as
+ * being no longer dirty.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ // TODO implement a command stack
+// getCommandStack().markSaveLocation();
+// firePropertyChange(PROP_DIRTY);
+ }
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public void doSaveAs() {
+ // pass
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isDirty() {
+ return false;
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ @Override
+ public void setFocus() {
+ // TODO Auto-generated method stub
+
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the activated page.
+ */
+ public void activated() {
+ if (mNeedsRecompute || mNeedsXmlReload) {
+ recomputeLayout();
+ }
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the deactivated page
+ */
+ public void deactivated() {
+ // nothing to be done here for now.
+ }
+
+ /**
+ * Opens and initialize the editor with a new file.
+ * @param file the file being edited.
+ */
+ public void openFile(IFile file) {
+ mEditedFile = file;
+ mConfigComposite.openFile(mEditedFile);
+
+ if (mReloadListener == null) {
+ mReloadListener = new ReloadListener();
+ LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);
+ }
+
+ if (mRulesEngine == null) {
+ mRulesEngine = new RulesEngine(mEditedFile.getProject());
+ if (mLayoutCanvas != null) {
+ mLayoutCanvas.setRulesEngine(mRulesEngine);
+ }
+ }
+ }
+
+ /**
+ * Resets the editor with a replacement file.
+ * @param file the replacement file.
+ */
+ public void replaceFile(IFile file) {
+ mEditedFile = file;
+ mConfigComposite.replaceFile(mEditedFile);
+ }
+
+ /**
+ * Resets the editor with a replacement file coming from a config change in the config
+ * selector.
+ * @param file the replacement file.
+ */
+ public void changeFileOnNewConfig(IFile file) {
+ mEditedFile = file;
+ mConfigComposite.changeFileOnNewConfig(mEditedFile);
+ }
+
+ public void onTargetChange() {
+ mConfigComposite.onTargetChange();
+ mConfigListener.onConfigurationChange();
+ }
+
+ public void onSdkChange() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target != null) {
+ mConfigComposite.onSdkLoaded(target);
+ mConfigListener.onConfigurationChange();
+ }
+ }
+ }
+
+ public Clipboard getClipboard() {
+ return mClipboard;
+ }
+
+ public LayoutEditor getLayoutEditor() {
+ return mLayoutEditor;
+ }
+
+ public UiDocumentNode getModel() {
+ return mLayoutEditor.getUiRootNode();
+ }
+
+ public SelectionSynchronizer getSelectionSynchronizer() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * Callback for XML model changed. Only update/recompute the layout if the editor is visible
+ */
+ public void onXmlModelChanged() {
+ if (mLayoutEditor.isGraphicalEditorActive()) {
+ doXmlReload(true /* force */);
+ recomputeLayout();
+ } else {
+ mNeedsXmlReload = true;
+ }
+ }
+
+ /**
+ * Actually performs the XML reload
+ * @see #onXmlModelChanged()
+ */
+ private void doXmlReload(boolean force) {
+ if (force || mNeedsXmlReload) {
+
+ // TODO : update the mLayoutCanvas, preserving the current selection if possible.
+
+// GraphicalViewer viewer = getGraphicalViewer();
+//
+// // try to preserve the selection before changing the content
+// SelectionManager selMan = viewer.getSelectionManager();
+// ISelection selection = selMan.getSelection();
+//
+// try {
+// viewer.setContents(getModel());
+// } finally {
+// selMan.setSelection(selection);
+// }
+
+ mNeedsXmlReload = false;
+ }
+ }
+
+ public void recomputeLayout() {
+ doXmlReload(false /* force */);
+ try {
+ // check that the resource exists. If the file is opened but the project is closed
+ // or deleted for some reason (changed from outside of eclipse), then this will
+ // return false;
+ if (mEditedFile.exists() == false) {
+ displayError("Resource '%1$s' does not exist.",
+ mEditedFile.getFullPath().toString());
+ return;
+ }
+
+ IProject iProject = mEditedFile.getProject();
+
+ if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
+ String message = String.format("%1$s is out of sync. Please refresh.",
+ mEditedFile.getName());
+
+ displayError(message);
+
+ // also print it in the error console.
+ AdtPlugin.printErrorToConsole(iProject.getName(), message);
+ return;
+ }
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target == null) {
+ displayError("The project target is not set.");
+ return;
+ }
+
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data == null) {
+ // It can happen that the workspace refreshes while the SDK is loading its
+ // data, which could trigger a redraw of the opened layout if some resources
+ // changed while Eclipse is closed.
+ // In this case data could be null, but this is not an error.
+ // We can just silently return, as all the opened editors are automatically
+ // refreshed once the SDK finishes loading.
+ LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);
+ switch (targetLoadStatus) {
+ case LOADING:
+ displayError("The project target (%1$s) is still loading.\n%2$s will refresh automatically once the process is finished.",
+ target.getName(), mEditedFile.getName());
+
+ break;
+ case FAILED: // known failure
+ case LOADED: // success but data isn't loaded?!?!
+ displayError("The project target (%s) was not properly loaded.",
+ target.getName());
+ break;
+ }
+
+ return;
+ }
+
+ // check there is actually a model (maybe the file is empty).
+ UiDocumentNode model = getModel();
+
+ if (model.getUiChildren().size() == 0) {
+ displayError("No Xml content. Go to the Outline view and add nodes.");
+ return;
+ }
+
+ LayoutBridge bridge = data.getLayoutBridge();
+
+ if (bridge.bridge != null) { // bridge can never be null.
+ ResourceManager resManager = ResourceManager.getInstance();
+
+ ProjectResources projectRes = resManager.getProjectResources(iProject);
+ if (projectRes == null) {
+ displayError("Missing project resources.");
+ return;
+ }
+
+ // get the resources of the file's project.
+ Map<String, Map<String, IResourceValue>> configuredProjectRes =
+ mConfigListener.getConfiguredProjectResources();
+
+ // get the framework resources
+ Map<String, Map<String, IResourceValue>> frameworkResources =
+ mConfigListener.getConfiguredFrameworkResources();
+
+ if (configuredProjectRes != null && frameworkResources != null) {
+ if (mProjectCallback == null) {
+ mProjectCallback = new ProjectCallback(
+ bridge.classLoader, projectRes, iProject);
+ }
+
+ if (mLogger == null) {
+ mLogger = new ILayoutLog() {
+ public void error(String message) {
+ AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);
+ }
+
+ public void error(Throwable error) {
+ String message = error.getMessage();
+ if (message == null) {
+ message = error.getClass().getName();
+ }
+
+ PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());
+ error.printStackTrace(ps);
+ }
+
+ public void warning(String message) {
+ AdtPlugin.printToConsole(mEditedFile.getName(), message);
+ }
+ };
+ }
+
+ // get the selected theme
+ String theme = mConfigComposite.getTheme();
+ if (theme != null) {
+ // Compute the layout
+ Rectangle rect = getBounds();
+
+ int width = rect.width;
+ int height = rect.height;
+ if (mUseExplodeMode) {
+ // compute how many padding in x and y will bump the screen size
+ ExplodedRenderingHelper helper = new ExplodedRenderingHelper(
+ getModel(), iProject);
+
+ // there are 2 paddings for each view
+ // left and right, or top and bottom.
+ int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;
+
+ width += helper.getWidthPadding() * paddingValue;
+ height += helper.getHeightPadding() * paddingValue;
+ }
+
+ int density = mConfigComposite.getDensity().getDpiValue();
+ float xdpi = mConfigComposite.getXDpi();
+ float ydpi = mConfigComposite.getYDpi();
+ boolean isProjectTheme = mConfigComposite.isProjectTheme();
+
+ UiElementPullParser parser = new UiElementPullParser(getModel(),
+ mUseExplodeMode, density, xdpi, iProject);
+
+ ILayoutResult result = computeLayout(bridge, parser,
+ iProject /* projectKey */,
+ width, height, !mConfigComposite.getClipping(),
+ density, xdpi, ydpi,
+ theme, isProjectTheme,
+ configuredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+
+ mLayoutCanvas.setResult(result);
+
+ // update the UiElementNode with the layout info.
+ if (result.getSuccess() == ILayoutResult.SUCCESS) {
+ hideError();
+ } else {
+ displayError(result.getErrorMessage());
+ }
+
+ model.refreshUi();
+ }
+ }
+ } else {
+ // SDK is loaded but not the layout library!
+
+ // check whether the bridge managed to load, or not
+ if (bridge.status == LoadStatus.LOADING) {
+ displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ } else {
+ displayError("Eclipse failed to load the framework information and the layout library!");
+ }
+ }
+ } else {
+ displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ }
+ } finally {
+ // no matter the result, we are done doing the recompute based on the latest
+ // resource/code change.
+ mNeedsRecompute = false;
+ }
+ }
+
+ /**
+ * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
+ * the implementation API level.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayoutResult.
+ */
+ @SuppressWarnings("deprecation")
+ private static ILayoutResult computeLayout(LayoutBridge bridge,
+ IXmlPullParser layoutDescription, Object projectKey,
+ int screenWidth, int screenHeight, boolean renderFullSize,
+ int density, float xdpi, float ydpi,
+ String themeName, boolean isProjectTheme,
+ Map<String, Map<String, IResourceValue>> projectResources,
+ Map<String, Map<String, IResourceValue>> frameworkResources,
+ IProjectCallback projectCallback, ILayoutLog logger) {
+
+ if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {
+ // newest API with support for "render full height"
+ // TODO: link boolean to UI.
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, renderFullSize,
+ density, xdpi, ydpi,
+ themeName, isProjectTheme,
+ projectResources, frameworkResources, projectCallback,
+ logger);
+ } else if (bridge.apiLevel == 3) {
+ // newer api with density support.
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
+ themeName, isProjectTheme,
+ projectResources, frameworkResources, projectCallback,
+ logger);
+ } else if (bridge.apiLevel == 2) {
+ // api with boolean for separation of project/framework theme
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
+ projectResources, frameworkResources, projectCallback,
+ logger);
+ } else {
+ // oldest api with no density/dpi, and project theme boolean mixed
+ // into the theme name.
+
+ // change the string if it's a custom theme to make sure we can
+ // differentiate them
+ if (isProjectTheme) {
+ themeName = "*" + themeName; //$NON-NLS-1$
+ }
+
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, themeName,
+ projectResources, frameworkResources, projectCallback,
+ logger);
+ }
+ }
+
+ public Rectangle getBounds() {
+ return mConfigComposite.getScreenBounds();
+ }
+
+ public void reloadPalette() {
+ if (mPalette != null) {
+ mPalette.reloadPalette(mLayoutEditor.getTargetData());
+ }
+ }
+
+ /**
+ * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node
+ * created by {@link ElementCreateCommand#execute()}.
+ *
+ * @param uiNodeModel The {@link UiElementNode} to select.
+ */
+ public void selectModel(UiElementNode uiNodeModel) {
+
+ // TODO this method was useful for GLE1. We may not need it anymore now.
+
+// GraphicalViewer viewer = getGraphicalViewer();
+//
+// // Give focus to the graphical viewer (in case the outline has it)
+// viewer.getControl().forceFocus();
+//
+// Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);
+//
+// if (editPart instanceof EditPart) {
+// viewer.select((EditPart)editPart);
+// }
+ }
+
+ private class ReloadListener implements ILayoutReloadListener {
+ /*
+ * Called when the file changes triggered a redraw of the layout
+ */
+ public void reloadLayout(ChangeFlags flags) {
+ boolean recompute = false;
+
+ if (flags.rClass) {
+ recompute = true;
+ if (mEditedFile != null) {
+ ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(
+ mEditedFile.getProject());
+
+ if (projectRes != null) {
+ projectRes.resetDynamicIds();
+ }
+ }
+ }
+
+ if (flags.localeList) {
+ // the locale list *potentially* changed so we update the locale in the
+ // config composite.
+ // However there's no recompute, as it could not be needed
+ // (for instance a new layout)
+ // If a resource that's not a layout changed this will trigger a recompute anyway.
+ mLayoutCanvas.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ mConfigComposite.updateLocales();
+ }
+ });
+ }
+
+ if (flags.resources) {
+ recompute = true;
+
+ // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.
+
+ // force a reparse in case a value XML file changed.
+ mConfiguredProjectRes = null;
+
+ // clear the cache in the bridge in case a bitmap/9-patch changed.
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
+ if (target != null) {
+
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ if (data != null) {
+ LayoutBridge bridge = data.getLayoutBridge();
+
+ if (bridge.bridge != null) {
+ bridge.bridge.clearCaches(mEditedFile.getProject());
+ }
+ }
+ }
+ }
+
+ if (flags.code) {
+ // only recompute if the custom view loader was used to load some code.
+ if (mProjectCallback != null && mProjectCallback.isUsed()) {
+ mProjectCallback = null;
+ recompute = true;
+ }
+ }
+
+ if (recompute) {
+ mLayoutCanvas.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ if (mLayoutEditor.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ mNeedsRecompute = true;
+ }
+ }
+ });
+ }
+ }
+ }
+}
-/*\r
- * Copyright (C) 2009 The Android Open Source Project\r
- *\r
- * Licensed under the Eclipse Public License, Version 1.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.eclipse.org/org/documents/epl-v10.php\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-package com.android.ide.eclipse.adt.internal.editors.layout.gle2;\r
-\r
-import com.android.ide.eclipse.adt.gscripts.DropZone;\r
-import com.android.ide.eclipse.adt.gscripts.Rect;\r
-import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;\r
-import com.android.layoutlib.api.ILayoutResult;\r
-\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.SWTException;\r
-import org.eclipse.swt.dnd.Clipboard;\r
-import org.eclipse.swt.dnd.DND;\r
-import org.eclipse.swt.dnd.DropTarget;\r
-import org.eclipse.swt.dnd.Transfer;\r
-import org.eclipse.swt.events.MouseEvent;\r
-import org.eclipse.swt.events.MouseListener;\r
-import org.eclipse.swt.events.MouseMoveListener;\r
-import org.eclipse.swt.events.PaintEvent;\r
-import org.eclipse.swt.events.PaintListener;\r
-import org.eclipse.swt.graphics.Color;\r
-import org.eclipse.swt.graphics.Font;\r
-import org.eclipse.swt.graphics.FontMetrics;\r
-import org.eclipse.swt.graphics.GC;\r
-import org.eclipse.swt.graphics.Image;\r
-import org.eclipse.swt.graphics.ImageData;\r
-import org.eclipse.swt.graphics.PaletteData;\r
-import org.eclipse.swt.graphics.Rectangle;\r
-import org.eclipse.swt.widgets.Canvas;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Display;\r
-\r
-import java.awt.image.BufferedImage;\r
-import java.awt.image.DataBufferInt;\r
-import java.awt.image.Raster;\r
-import java.util.ArrayList;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import java.util.ListIterator;\r
-\r
-/**\r
- * Displays the image rendered by the {@link GraphicalEditorPart} and handles\r
- * the interaction with the widgets.\r
- * <p/>\r
- *\r
- * @since GLE2\r
- *\r
- * TODO list:\r
- * - gray on error, keep select but disable d'n'd.\r
- * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.)\r
- * - handle drop target (from palette).\r
- * - handle drag'n'drop (internal, for moving/duplicating).\r
- * - handle context menu (depending on selection).\r
- * - selection synchronization with the outline (both ways).\r
- */\r
-/* package */ class LayoutCanvas extends Canvas {\r
-\r
- /**\r
- * Margin around the rendered image. Should be enough space to display the layout\r
- * width and height pseudo widgets.\r
- */\r
- static final int IMAGE_MARGIN = 5;\r
-\r
-\r
- /** The Groovy Rules Engine, associated with the current project. */\r
- private RulesEngine mRulesEngine;\r
-\r
- /*\r
- * The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}.\r
- * This can be null.\r
- * When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too.\r
- */\r
- private ILayoutResult mLastValidResult;\r
-\r
- /**\r
- * The CanvasViewInfo root created for the last update of {@link #mLastValidResult}.\r
- * This is null when {@link #mLastValidResult} is null.\r
- * When non null, {@link #mLastValidResult} is guaranteed to be non-null too.\r
- */\r
- private CanvasViewInfo mLastValidViewInfoRoot;\r
-\r
- /**\r
- * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}\r
- * in which case it is also available in {@link #mLastValidResult}.\r
- * When false this means the canvas is displaying an out-dated result image & bounds and some\r
- * features should be disabled accordingly such a drag'n'drop.\r
- * <p/>\r
- * When this is false, {@link #mLastValidResult} can be non-null and points to an older\r
- * layout result.\r
- */\r
- private boolean mIsResultValid;\r
-\r
- /** Current background image. Null when there's no image. */\r
- private Image mImage;\r
-\r
- /** The current selection list. The list is never null, however it can be empty. */\r
- private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();\r
-\r
- /** CanvasSelection border color. Do not dispose, it's a system color. */\r
- private Color mSelectionFgColor;\r
-\r
- /** CanvasSelection name font. Do not dispose, it's a system font. */\r
- private Font mSelectionFont;\r
-\r
- /** Pixel height of the font displaying the selection name. Initially set to 0 and only\r
- * initialized in onPaint() when we have a GC. */\r
- private int mSelectionFontHeight;\r
-\r
- /** Current hover view info. Null when no mouse hover. */\r
- private CanvasViewInfo mHoverViewInfo;\r
-\r
- /** Current mouse hover border rectangle. Null when there's no mouse hover. */\r
- private Rectangle mHoverRect;\r
-\r
- /** Hover border color. Must be disposed, it's NOT a system color. */\r
- private Color mHoverFgColor;\r
-\r
- /** Outline color. Do not dispose, it's a system color. */\r
- private Color mOutlineColor;\r
-\r
- /**\r
- * The <em>current</em> alternate selection, if any, which changes when the Alt key is\r
- * used during a selection. Can be null.\r
- */\r
- private CanvasAlternateSelection mAltSelection;\r
-\r
- /** When true, always display the outline of all views. */\r
- private boolean mShowOutline;\r
-\r
- /** Drop target associated with this composite. */\r
- private DropTarget mDropTarget;\r
-\r
- /** Drop listener, with feedback from current drop */\r
- private CanvasDropListener mDropListener;\r
-\r
- /** Drop color. Do not dispose, it's a system color. */\r
- private Color mDropFgColor;\r
-\r
-\r
- public LayoutCanvas(RulesEngine rulesEngine, Composite parent, int style) {\r
- super(parent, style | SWT.DOUBLE_BUFFERED);\r
- mRulesEngine = rulesEngine;\r
-\r
- Display d = getDisplay();\r
- mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);\r
- mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange\r
- mOutlineColor = d.getSystemColor(SWT.COLOR_GREEN);\r
- mSelectionFont = d.getSystemFont();\r
- mDropFgColor = d.getSystemColor(SWT.COLOR_YELLOW);\r
-\r
- addPaintListener(new PaintListener() {\r
- public void paintControl(PaintEvent e) {\r
- onPaint(e);\r
- }\r
- });\r
-\r
- addMouseMoveListener(new MouseMoveListener() {\r
- public void mouseMove(MouseEvent e) {\r
- onMouseMove(e);\r
- }\r
- });\r
-\r
- addMouseListener(new MouseListener() {\r
- public void mouseUp(MouseEvent e) {\r
- onMouseUp(e);\r
- }\r
-\r
- public void mouseDown(MouseEvent e) {\r
- onMouseDown(e);\r
- }\r
-\r
- public void mouseDoubleClick(MouseEvent e) {\r
- onDoubleClick(e);\r
- }\r
- });\r
-\r
- mDropTarget = new DropTarget(this, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);\r
- mDropTarget.setTransfer(new Transfer[] { ElementDescTransfer.getInstance() });\r
- mDropListener = new CanvasDropListener(this);\r
- mDropTarget.addDropListener(mDropListener);\r
- }\r
-\r
- @Override\r
- public void dispose() {\r
- super.dispose();\r
-\r
- if (mHoverFgColor != null) {\r
- mHoverFgColor.dispose();\r
- mHoverFgColor = null;\r
- }\r
-\r
- if (mDropTarget != null) {\r
- mDropTarget.dispose();\r
- mDropTarget = null;\r
- }\r
-\r
- if (mRulesEngine != null) {\r
- mRulesEngine.dispose();\r
- mRulesEngine = null;\r
- }\r
- }\r
-\r
- /**\r
- * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid\r
- * {@link ILayoutResult} in which case it is also available in {@link #mLastValidResult}.\r
- * When false this means the canvas is displaying an out-dated result image & bounds and some\r
- * features should be disabled accordingly such a drag'n'drop.\r
- * <p/>\r
- * When this is false, {@link #mLastValidResult} can be non-null and points to an older\r
- * layout result.\r
- */\r
- /* package */ boolean isResultValid() {\r
- return mIsResultValid;\r
- }\r
-\r
- /** Returns the Groovy Rules Engine, associated with the current project. */\r
- /* package */ RulesEngine getRulesEngine() {\r
- return mRulesEngine;\r
- }\r
-\r
- /** Sets the Groovy Rules Engine, associated with the current project. */\r
- /* package */ void setRulesEngine(RulesEngine rulesEngine) {\r
- mRulesEngine = rulesEngine;\r
- }\r
-\r
- /**\r
- * Sets the result of the layout rendering. The result object indicates if the layout\r
- * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.\r
- *\r
- * Implementation detail: the bridge's computeLayout() method already returns a newly\r
- * allocated ILayourResult. That means we can keep this result and hold on to it\r
- * when it is valid.\r
- *\r
- * @param result The new rendering result, either valid or not.\r
- */\r
- public void setResult(ILayoutResult result) {\r
- // disable any hover\r
- mHoverRect = null;\r
-\r
- mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);\r
-\r
- if (mIsResultValid && result != null) {\r
- mLastValidResult = result;\r
- mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());\r
- setImage(result.getImage());\r
-\r
- // Check if the selection is still the same (based on the object keys)\r
- // and eventually recompute their bounds.\r
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {\r
- CanvasSelection s = it.next();\r
-\r
- // Check the if the selected object still exists\r
- Object key = s.getViewInfo().getUiViewKey();\r
- CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);\r
-\r
- // Remove the previous selection -- if the selected object still exists\r
- // we need to recompute its bounds in case it moved so we'll insert a new one\r
- // at the same place.\r
- it.remove();\r
- if (vi != null) {\r
- it.add(new CanvasSelection(vi, mRulesEngine));\r
- }\r
- }\r
-\r
- // remove the current alternate selection views\r
- mAltSelection = null;\r
- }\r
-\r
- redraw();\r
- }\r
-\r
- public void setShowOutline(boolean newState) {\r
- mShowOutline = newState;\r
- redraw();\r
- }\r
-\r
- /**\r
- * Called by the {@link GraphicalEditorPart} when the Copy action is requested.\r
- *\r
- * @param clipboard The shared clipboard. Must not be disposed.\r
- */\r
- public void onCopy(Clipboard clipboard) {\r
- // TODO implement copy to clipbard. Also will need to provide feedback to enable\r
- // copy only when there's a selection.\r
- }\r
-\r
- /**\r
- * Called by the {@link GraphicalEditorPart} when the Cut action is requested.\r
- *\r
- * @param clipboard The shared clipboard. Must not be disposed.\r
- */\r
- public void onCut(Clipboard clipboard) {\r
- // TODO implement copy to clipbard. Also will need to provide feedback to enable\r
- // cut only when there's a selection.\r
- }\r
-\r
- /**\r
- * Called by the {@link GraphicalEditorPart} when the Paste action is requested.\r
- *\r
- * @param clipboard The shared clipboard. Must not be disposed.\r
- */\r
- public void onPaste(Clipboard clipboard) {\r
-\r
- }\r
-\r
- /**\r
- * Called by the {@link GraphicalEditorPart} when the Select All action is requested.\r
- */\r
- public void onSelectAll() {\r
- // First clear the current selection, if any.\r
- mSelections.clear();\r
- mAltSelection = null;\r
-\r
- // Now select everything if there's a valid layout\r
- if (mIsResultValid && mLastValidResult != null) {\r
- selectAllViewInfos(mLastValidViewInfoRoot);\r
- redraw();\r
- }\r
- }\r
-\r
- /**\r
- * Delete action\r
- */\r
- public void onDelete() {\r
- // TODO not implemented yet, not even hooked in yet!\r
- }\r
-\r
- //---\r
-\r
- /**\r
- * Sets the image of the last *successful* rendering.\r
- * Converts the AWT image into an SWT image.\r
- */\r
- private void setImage(BufferedImage awtImage) {\r
- int width = awtImage.getWidth();\r
- int height = awtImage.getHeight();\r
-\r
- Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));\r
- int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();\r
-\r
- ImageData imageData = new ImageData(width, height, 32,\r
- new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));\r
-\r
- imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);\r
-\r
- mImage = new Image(getDisplay(), imageData);\r
- }\r
-\r
- /**\r
- * Sets the alpha for the given GC.\r
- * <p/>\r
- * Alpha may not work on all platforms and may fail with an exception.\r
- *\r
- * @param gc the GC to change\r
- * @param alpha the new alpha, 0 for transparent, 255 for opaque.\r
- * @return True if the operation worked, false if it failed with an exception.\r
- *\r
- * @see GC#setAlpha(int)\r
- */\r
- private boolean gc_setAlpha(GC gc, int alpha) {\r
- try {\r
- gc.setAlpha(alpha);\r
- return true;\r
- } catch (SWTException e) {\r
- return false;\r
- }\r
- }\r
-\r
- /**\r
- * Paints the canvas in response to paint events.\r
- */\r
- private void onPaint(PaintEvent e) {\r
- GC gc = e.gc;\r
-\r
- if (mImage != null) {\r
- if (!mIsResultValid) {\r
- gc_setAlpha(gc, 128); // half-transparent\r
- }\r
-\r
- gc.drawImage(mImage, IMAGE_MARGIN, IMAGE_MARGIN);\r
-\r
- if (!mIsResultValid) {\r
- gc_setAlpha(gc, 255); // opaque\r
- }\r
- }\r
-\r
- if (mShowOutline) {\r
- gc.setForeground(mOutlineColor);\r
- gc.setLineStyle(SWT.LINE_DOT);\r
- drawOutline(gc, mLastValidViewInfoRoot);\r
- }\r
-\r
- if (mHoverRect != null) {\r
- gc.setForeground(mHoverFgColor);\r
- gc.setLineStyle(SWT.LINE_DOT);\r
- gc.drawRectangle(mHoverRect);\r
- }\r
-\r
- // initialize the selection font height once. We need the GC to do that.\r
- if (mSelectionFontHeight == 0) {\r
- gc.setFont(mSelectionFont);\r
- FontMetrics fm = gc.getFontMetrics();\r
- mSelectionFontHeight = fm.getHeight();\r
- }\r
-\r
- for (CanvasSelection s : mSelections) {\r
- drawSelection(gc, s);\r
- }\r
-\r
- drawDropZones(gc);\r
- }\r
-\r
- private void drawOutline(GC gc, CanvasViewInfo info) {\r
-\r
- Rectangle r = info.getAbsRect();\r
- gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);\r
-\r
- for (CanvasViewInfo vi : info.getChildren()) {\r
- drawOutline(gc, vi);\r
- }\r
- }\r
-\r
- private void drawSelection(GC gc, CanvasSelection s) {\r
- Rectangle r = s.getRect();\r
-\r
- gc.setForeground(mSelectionFgColor);\r
- gc.setLineStyle(SWT.LINE_SOLID);\r
- gc.drawRectangle(s.getRect());\r
-\r
- String name = s.getName();\r
-\r
- if (name != null) {\r
- int xs = r.x + 2;\r
- int ys = r.y - mSelectionFontHeight;\r
- if (ys < 0) {\r
- ys = r.y + r.height;\r
- }\r
- gc.drawString(name, xs, ys, true /*transparent*/);\r
- }\r
- }\r
-\r
- private void drawDropZones(GC gc) {\r
- if (mDropListener == null) {\r
- return;\r
- }\r
-\r
- CanvasViewInfo vi = mDropListener.getTargetView();\r
- if (vi == null) {\r
- return;\r
- }\r
-\r
- gc.setForeground(mDropFgColor);\r
-\r
- ArrayList<DropZone> zones = mDropListener.getZones();\r
- if (zones != null) {\r
-\r
- gc.setLineStyle(SWT.LINE_SOLID);\r
- gc.setLineWidth(1);\r
-\r
- DropZone curr = mDropListener.getCurrentZone();\r
-\r
- for (DropZone zone : zones) {\r
- Rect r = zone.bounds;\r
- if (r != null && r.w > 0 && r.h > 0) {\r
- int x = r.x + IMAGE_MARGIN;\r
- int y = r.y + IMAGE_MARGIN;\r
-\r
- int alpha = 128; // half-transparent\r
- if (zone == curr) {\r
- alpha = 192;\r
- }\r
-\r
- if (gc_setAlpha(gc, alpha)) {\r
- gc.fillRectangle(x, y, r.w, r.h);\r
- gc_setAlpha(gc, 255); // opaque\r
- }\r
-\r
- gc.drawRectangle(x, y, r.w, r.h);\r
- }\r
- }\r
-\r
- }\r
-\r
- gc.setLineStyle(SWT.LINE_DOT);\r
- gc.setLineWidth(3);\r
- Rectangle r = vi.getAbsRect();\r
- gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);\r
- gc.setLineWidth(1);\r
- }\r
-\r
- /**\r
- * Hover on top of a known child.\r
- */\r
- private void onMouseMove(MouseEvent e) {\r
- if (mLastValidResult != null) {\r
- CanvasViewInfo root = mLastValidViewInfoRoot;\r
- CanvasViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN);\r
-\r
- // We don't hover on the root since it's not a widget per see and it is always there.\r
- if (vi == root) {\r
- vi = null;\r
- }\r
-\r
- boolean needsUpdate = vi != mHoverViewInfo;\r
- mHoverViewInfo = vi;\r
-\r
- if (vi == null) {\r
- mHoverRect = null;\r
- } else {\r
- Rectangle r = vi.getSelectionRect();\r
- mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN,\r
- r.width, r.height);\r
- }\r
-\r
- if (needsUpdate) {\r
- redraw();\r
- }\r
- }\r
- }\r
-\r
- private void onMouseDown(MouseEvent e) {\r
- // pass, not used yet.\r
- }\r
-\r
- /**\r
- * Performs selection on mouse up (not mouse down).\r
- * <p/>\r
- * Shift key is used to toggle in multi-selection.\r
- * Alt key is used to cycle selection through objects at the same level than the one\r
- * pointed at (i.e. click on an object then alt-click to cycle).\r
- */\r
- private void onMouseUp(MouseEvent e) {\r
- if (mLastValidResult != null) {\r
-\r
- boolean isShift = (e.stateMask & SWT.SHIFT) != 0;\r
- boolean isAlt = (e.stateMask & SWT.ALT) != 0;\r
-\r
- int x = e.x - IMAGE_MARGIN;\r
- int y = e.y - IMAGE_MARGIN;\r
- CanvasViewInfo vi = findViewInfoAt(x, y);\r
-\r
- if (isShift && !isAlt) {\r
- // Case where shift is pressed: pointed object is toggled.\r
-\r
- // reset alternate selection if any\r
- mAltSelection = null;\r
-\r
- // If nothing has been found at the cursor, assume it might be a user error\r
- // and avoid clearing the existing selection.\r
-\r
- if (vi != null) {\r
- // toggle this selection on-off: remove it if already selected\r
- if (deselect(vi)) {\r
- redraw();\r
- return;\r
- }\r
-\r
- // otherwise add it.\r
- mSelections.add(new CanvasSelection(vi, mRulesEngine));\r
- redraw();\r
- }\r
-\r
- } else if (isAlt) {\r
- // Case where alt is pressed: select or cycle the object pointed at.\r
-\r
- // Note: if shift and alt are pressed, shift is ignored. The alternate selection\r
- // mechanism does not reset the current multiple selection unless they intersect.\r
-\r
- // We need to remember the "origin" of the alternate selection, to be\r
- // able to continue cycling through it later. If there's no alternate selection,\r
- // create one. If there's one but not for the same origin object, create a new\r
- // one too.\r
- if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {\r
- mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt(\r
- x, y, mLastValidViewInfoRoot, null));\r
-\r
- // deselect them all, in case they were partially selected\r
- deselectAll(mAltSelection.getAltViews());\r
-\r
- // select the current one\r
- CanvasViewInfo vi2 = mAltSelection.getCurrent();\r
- if (vi2 != null) {\r
- mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));\r
- }\r
- } else {\r
- // We're trying to cycle through the current alternate selection.\r
- // First remove the current object.\r
- CanvasViewInfo vi2 = mAltSelection.getCurrent();\r
- deselect(vi2);\r
-\r
- // Now select the next one.\r
- vi2 = mAltSelection.getNext();\r
- if (vi2 != null) {\r
- mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));\r
- }\r
- }\r
- redraw();\r
-\r
- } else {\r
- // Case where no modifier is pressed: either select or reset the selection.\r
-\r
- // reset alternate selection if any\r
- mAltSelection = null;\r
-\r
- // reset (multi)selection if any\r
- if (mSelections.size() > 0) {\r
- if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {\r
- // CanvasSelection remains the same, don't touch it.\r
- return;\r
- }\r
- mSelections.clear();\r
- }\r
-\r
- if (vi != null) {\r
- mSelections.add(new CanvasSelection(vi, mRulesEngine));\r
- }\r
- redraw();\r
- }\r
- }\r
- }\r
-\r
- /** Deselects a view info. Returns true if the object was actually selected. */\r
- private boolean deselect(CanvasViewInfo canvasViewInfo) {\r
- if (canvasViewInfo == null) {\r
- return false;\r
- }\r
-\r
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {\r
- CanvasSelection s = it.next();\r
- if (canvasViewInfo == s.getViewInfo()) {\r
- it.remove();\r
- return true;\r
- }\r
- }\r
-\r
- return false;\r
- }\r
-\r
- /** Deselects multiple view infos, */\r
- private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {\r
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {\r
- CanvasSelection s = it.next();\r
- if (canvasViewInfos.contains(s.getViewInfo())) {\r
- it.remove();\r
- }\r
- }\r
- }\r
-\r
- private void onDoubleClick(MouseEvent e) {\r
- // pass, not used yet.\r
- }\r
-\r
- /**\r
- * Tries to find a child with the same view key in the view info sub-tree.\r
- * Returns null if not found.\r
- */\r
- private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {\r
- if (canvasViewInfo.getUiViewKey() == viewKey) {\r
- return canvasViewInfo;\r
- }\r
-\r
- // try to find a matching child\r
- for (CanvasViewInfo child : canvasViewInfo.getChildren()) {\r
- CanvasViewInfo v = findViewInfoKey(viewKey, child);\r
- if (v != null) {\r
- return v;\r
- }\r
- }\r
-\r
- return null;\r
- }\r
-\r
-\r
- /**\r
- * Tries to find the inner most child matching the given x,y coordinates in the view\r
- * info sub-tree, starting at the last know view info root.\r
- * This uses the potentially-expanded selection bounds.\r
- *\r
- * Returns null if not found or if there's view info root.\r
- */\r
- /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {\r
- if (mLastValidViewInfoRoot == null) {\r
- return null;\r
- } else {\r
- return findViewInfoAt(x, y, mLastValidViewInfoRoot);\r
- }\r
- }\r
-\r
- /**\r
- * Tries to find the inner most child matching the given x,y coordinates in the view\r
- * info sub-tree. This uses the potentially-expanded selection bounds.\r
- *\r
- * Returns null if not found.\r
- */\r
- private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) {\r
- Rectangle r = canvasViewInfo.getSelectionRect();\r
- if (r.contains(x, y)) {\r
-\r
- // try to find a matching child first\r
- for (CanvasViewInfo child : canvasViewInfo.getChildren()) {\r
- CanvasViewInfo v = findViewInfoAt(x, y, child);\r
- if (v != null) {\r
- return v;\r
- }\r
- }\r
-\r
- // if no children matched, this is the view that we're looking for\r
- return canvasViewInfo;\r
- }\r
-\r
- return null;\r
- }\r
-\r
- private ArrayList<CanvasViewInfo> findAltViewInfoAt(\r
- int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) {\r
- Rectangle r;\r
-\r
- if (outList == null) {\r
- outList = new ArrayList<CanvasViewInfo>();\r
-\r
- // add the parent root only once\r
- r = parent.getSelectionRect();\r
- if (r.contains(x, y)) {\r
- outList.add(parent);\r
- }\r
- }\r
-\r
- if (parent.getChildren().size() > 0) {\r
- // then add all children that match the position\r
- for (CanvasViewInfo child : parent.getChildren()) {\r
- r = child.getSelectionRect();\r
- if (r.contains(x, y)) {\r
- outList.add(child);\r
- }\r
- }\r
-\r
- // finally recurse in the children\r
- for (CanvasViewInfo child : parent.getChildren()) {\r
- r = child.getSelectionRect();\r
- if (r.contains(x, y)) {\r
- findAltViewInfoAt(x, y, child, outList);\r
- }\r
- }\r
- }\r
-\r
- return outList;\r
- }\r
-\r
- /**\r
- * Used by {@link #onSelectAll()} to add all current view infos to the selection list.\r
- *\r
- * @param canvasViewInfo The root to add. This info and all its children will be added to the\r
- * selection list.\r
- */\r
- private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {\r
- mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine));\r
- for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {\r
- selectAllViewInfos(vi);\r
- }\r
- }\r
-}\r
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone;
+import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.layoutlib.api.ILayoutResult;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Displays the image rendered by the {@link GraphicalEditorPart} and handles
+ * the interaction with the widgets.
+ * <p/>
+ *
+ * @since GLE2
+ *
+ * TODO list:
+ * - gray on error, keep select but disable d'n'd.
+ * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.)
+ * - handle drop target (from palette).
+ * - handle drag'n'drop (internal, for moving/duplicating).
+ * - handle context menu (depending on selection).
+ * - selection synchronization with the outline (both ways).
+ */
+/* package */ class LayoutCanvas extends Canvas {
+
+ /**
+ * Margin around the rendered image. Should be enough space to display the layout
+ * width and height pseudo widgets.
+ */
+ static final int IMAGE_MARGIN = 5;
+
+
+ /** The Groovy Rules Engine, associated with the current project. */
+ private RulesEngine mRulesEngine;
+
+ /*
+ * The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}.
+ * This can be null.
+ * When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too.
+ */
+ private ILayoutResult mLastValidResult;
+
+ /**
+ * The CanvasViewInfo root created for the last update of {@link #mLastValidResult}.
+ * This is null when {@link #mLastValidResult} is null.
+ * When non null, {@link #mLastValidResult} is guaranteed to be non-null too.
+ */
+ private CanvasViewInfo mLastValidViewInfoRoot;
+
+ /**
+ * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}
+ * in which case it is also available in {@link #mLastValidResult}.
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * When this is false, {@link #mLastValidResult} can be non-null and points to an older
+ * layout result.
+ */
+ private boolean mIsResultValid;
+
+ /** Current background image. Null when there's no image. */
+ private Image mImage;
+
+ /** The current selection list. The list is never null, however it can be empty. */
+ private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
+
+ /** CanvasSelection border color. Do not dispose, it's a system color. */
+ private Color mSelectionFgColor;
+
+ /** CanvasSelection name font. Do not dispose, it's a system font. */
+ private Font mSelectionFont;
+
+ /** Pixel height of the font displaying the selection name. Initially set to 0 and only
+ * initialized in onPaint() when we have a GC. */
+ private int mSelectionFontHeight;
+
+ /** Current hover view info. Null when no mouse hover. */
+ private CanvasViewInfo mHoverViewInfo;
+
+ /** Current mouse hover border rectangle. Null when there's no mouse hover. */
+ private Rectangle mHoverRect;
+
+ /** Hover border color. Must be disposed, it's NOT a system color. */
+ private Color mHoverFgColor;
+
+ /** Outline color. Do not dispose, it's a system color. */
+ private Color mOutlineColor;
+
+ /**
+ * The <em>current</em> alternate selection, if any, which changes when the Alt key is
+ * used during a selection. Can be null.
+ */
+ private CanvasAlternateSelection mAltSelection;
+
+ /** When true, always display the outline of all views. */
+ private boolean mShowOutline;
+
+ /** Drop target associated with this composite. */
+ private DropTarget mDropTarget;
+
+ /** Drop listener, with feedback from current drop */
+ private CanvasDropListener mDropListener;
+
+ /** Drop color. Do not dispose, it's a system color. */
+ private Color mDropFgColor;
+
+
+ public LayoutCanvas(RulesEngine rulesEngine, Composite parent, int style) {
+ super(parent, style | SWT.DOUBLE_BUFFERED);
+ mRulesEngine = rulesEngine;
+
+ Display d = getDisplay();
+ mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);
+ mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange
+ mOutlineColor = d.getSystemColor(SWT.COLOR_GREEN);
+ mSelectionFont = d.getSystemFont();
+ mDropFgColor = d.getSystemColor(SWT.COLOR_YELLOW);
+
+ addPaintListener(new PaintListener() {
+ public void paintControl(PaintEvent e) {
+ onPaint(e);
+ }
+ });
+
+ addMouseMoveListener(new MouseMoveListener() {
+ public void mouseMove(MouseEvent e) {
+ onMouseMove(e);
+ }
+ });
+
+ addMouseListener(new MouseListener() {
+ public void mouseUp(MouseEvent e) {
+ onMouseUp(e);
+ }
+
+ public void mouseDown(MouseEvent e) {
+ onMouseDown(e);
+ }
+
+ public void mouseDoubleClick(MouseEvent e) {
+ onDoubleClick(e);
+ }
+ });
+
+ mDropTarget = new DropTarget(this, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
+ mDropTarget.setTransfer(new Transfer[] { ElementDescTransfer.getInstance() });
+ mDropListener = new CanvasDropListener(this);
+ mDropTarget.addDropListener(mDropListener);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mHoverFgColor != null) {
+ mHoverFgColor.dispose();
+ mHoverFgColor = null;
+ }
+
+ if (mDropTarget != null) {
+ mDropTarget.dispose();
+ mDropTarget = null;
+ }
+
+ if (mRulesEngine != null) {
+ mRulesEngine.dispose();
+ mRulesEngine = null;
+ }
+ }
+
+ /**
+ * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
+ * {@link ILayoutResult} in which case it is also available in {@link #mLastValidResult}.
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * When this is false, {@link #mLastValidResult} can be non-null and points to an older
+ * layout result.
+ */
+ /* package */ boolean isResultValid() {
+ return mIsResultValid;
+ }
+
+ /** Returns the Groovy Rules Engine, associated with the current project. */
+ /* package */ RulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ /** Sets the Groovy Rules Engine, associated with the current project. */
+ /* package */ void setRulesEngine(RulesEngine rulesEngine) {
+ mRulesEngine = rulesEngine;
+ }
+
+ /**
+ * Sets the result of the layout rendering. The result object indicates if the layout
+ * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayourResult. That means we can keep this result and hold on to it
+ * when it is valid.
+ *
+ * @param result The new rendering result, either valid or not.
+ */
+ public void setResult(ILayoutResult result) {
+ // disable any hover
+ mHoverRect = null;
+
+ mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
+
+ if (mIsResultValid && result != null) {
+ mLastValidResult = result;
+ mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
+ setImage(result.getImage());
+
+ // Check if the selection is still the same (based on the object keys)
+ // and eventually recompute their bounds.
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+
+ // Check the if the selected object still exists
+ Object key = s.getViewInfo().getUiViewKey();
+ CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);
+
+ // Remove the previous selection -- if the selected object still exists
+ // we need to recompute its bounds in case it moved so we'll insert a new one
+ // at the same place.
+ it.remove();
+ if (vi != null) {
+ it.add(new CanvasSelection(vi, mRulesEngine));
+ }
+ }
+
+ // remove the current alternate selection views
+ mAltSelection = null;
+ }
+
+ redraw();
+ }
+
+ public void setShowOutline(boolean newState) {
+ mShowOutline = newState;
+ redraw();
+ }
+
+ /**
+ * Called by the {@link GraphicalEditorPart} when the Copy action is requested.
+ *
+ * @param clipboard The shared clipboard. Must not be disposed.
+ */
+ public void onCopy(Clipboard clipboard) {
+ // TODO implement copy to clipbard. Also will need to provide feedback to enable
+ // copy only when there's a selection.
+ }
+
+ /**
+ * Called by the {@link GraphicalEditorPart} when the Cut action is requested.
+ *
+ * @param clipboard The shared clipboard. Must not be disposed.
+ */
+ public void onCut(Clipboard clipboard) {
+ // TODO implement copy to clipbard. Also will need to provide feedback to enable
+ // cut only when there's a selection.
+ }
+
+ /**
+ * Called by the {@link GraphicalEditorPart} when the Paste action is requested.
+ *
+ * @param clipboard The shared clipboard. Must not be disposed.
+ */
+ public void onPaste(Clipboard clipboard) {
+
+ }
+
+ /**
+ * Called by the {@link GraphicalEditorPart} when the Select All action is requested.
+ */
+ public void onSelectAll() {
+ // First clear the current selection, if any.
+ mSelections.clear();
+ mAltSelection = null;
+
+ // Now select everything if there's a valid layout
+ if (mIsResultValid && mLastValidResult != null) {
+ selectAllViewInfos(mLastValidViewInfoRoot);
+ redraw();
+ }
+ }
+
+ /**
+ * Delete action
+ */
+ public void onDelete() {
+ // TODO not implemented yet, not even hooked in yet!
+ }
+
+ //---
+
+ /**
+ * Sets the image of the last *successful* rendering.
+ * Converts the AWT image into an SWT image.
+ */
+ private void setImage(BufferedImage awtImage) {
+ int width = awtImage.getWidth();
+ int height = awtImage.getHeight();
+
+ Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
+ int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
+
+ ImageData imageData = new ImageData(width, height, 32,
+ new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
+
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+ mImage = new Image(getDisplay(), imageData);
+ }
+
+ /**
+ * Sets the alpha for the given GC.
+ * <p/>
+ * Alpha may not work on all platforms and may fail with an exception.
+ *
+ * @param gc the GC to change
+ * @param alpha the new alpha, 0 for transparent, 255 for opaque.
+ * @return True if the operation worked, false if it failed with an exception.
+ *
+ * @see GC#setAlpha(int)
+ */
+ private boolean gc_setAlpha(GC gc, int alpha) {
+ try {
+ gc.setAlpha(alpha);
+ return true;
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Paints the canvas in response to paint events.
+ */
+ private void onPaint(PaintEvent e) {
+ GC gc = e.gc;
+
+ if (mImage != null) {
+ if (!mIsResultValid) {
+ gc_setAlpha(gc, 128); // half-transparent
+ }
+
+ gc.drawImage(mImage, IMAGE_MARGIN, IMAGE_MARGIN);
+
+ if (!mIsResultValid) {
+ gc_setAlpha(gc, 255); // opaque
+ }
+ }
+
+ if (mShowOutline) {
+ gc.setForeground(mOutlineColor);
+ gc.setLineStyle(SWT.LINE_DOT);
+ drawOutline(gc, mLastValidViewInfoRoot);
+ }
+
+ if (mHoverRect != null) {
+ gc.setForeground(mHoverFgColor);
+ gc.setLineStyle(SWT.LINE_DOT);
+ gc.drawRectangle(mHoverRect);
+ }
+
+ // initialize the selection font height once. We need the GC to do that.
+ if (mSelectionFontHeight == 0) {
+ gc.setFont(mSelectionFont);
+ FontMetrics fm = gc.getFontMetrics();
+ mSelectionFontHeight = fm.getHeight();
+ }
+
+ for (CanvasSelection s : mSelections) {
+ drawSelection(gc, s);
+ }
+
+ drawDropZones(gc);
+ }
+
+ private void drawOutline(GC gc, CanvasViewInfo info) {
+
+ Rectangle r = info.getAbsRect();
+ gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);
+
+ for (CanvasViewInfo vi : info.getChildren()) {
+ drawOutline(gc, vi);
+ }
+ }
+
+ private void drawSelection(GC gc, CanvasSelection s) {
+ Rectangle r = s.getRect();
+
+ gc.setForeground(mSelectionFgColor);
+ gc.setLineStyle(SWT.LINE_SOLID);
+ gc.drawRectangle(s.getRect());
+
+ String name = s.getName();
+
+ if (name != null) {
+ int xs = r.x + 2;
+ int ys = r.y - mSelectionFontHeight;
+ if (ys < 0) {
+ ys = r.y + r.height;
+ }
+ gc.drawString(name, xs, ys, true /*transparent*/);
+ }
+ }
+
+ private void drawDropZones(GC gc) {
+ if (mDropListener == null) {
+ return;
+ }
+
+ CanvasViewInfo vi = mDropListener.getTargetView();
+ if (vi == null) {
+ return;
+ }
+
+ gc.setForeground(mDropFgColor);
+
+ ArrayList<DropZone> zones = mDropListener.getZones();
+ if (zones != null) {
+
+ gc.setLineStyle(SWT.LINE_SOLID);
+ gc.setLineWidth(1);
+
+ DropZone curr = mDropListener.getCurrentZone();
+
+ for (DropZone zone : zones) {
+ Rect r = zone.bounds;
+ if (r != null && r.w > 0 && r.h > 0) {
+ int x = r.x + IMAGE_MARGIN;
+ int y = r.y + IMAGE_MARGIN;
+
+ int alpha = 128; // half-transparent
+ if (zone == curr) {
+ alpha = 192;
+ }
+
+ if (gc_setAlpha(gc, alpha)) {
+ gc.fillRectangle(x, y, r.w, r.h);
+ gc_setAlpha(gc, 255); // opaque
+ }
+
+ gc.drawRectangle(x, y, r.w, r.h);
+ }
+ }
+
+ }
+
+ gc.setLineStyle(SWT.LINE_DOT);
+ gc.setLineWidth(3);
+ Rectangle r = vi.getAbsRect();
+ gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);
+ gc.setLineWidth(1);
+ }
+
+ /**
+ * Hover on top of a known child.
+ */
+ private void onMouseMove(MouseEvent e) {
+ if (mLastValidResult != null) {
+ CanvasViewInfo root = mLastValidViewInfoRoot;
+ CanvasViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN);
+
+ // We don't hover on the root since it's not a widget per see and it is always there.
+ if (vi == root) {
+ vi = null;
+ }
+
+ boolean needsUpdate = vi != mHoverViewInfo;
+ mHoverViewInfo = vi;
+
+ if (vi == null) {
+ mHoverRect = null;
+ } else {
+ Rectangle r = vi.getSelectionRect();
+ mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN,
+ r.width, r.height);
+ }
+
+ if (needsUpdate) {
+ redraw();
+ }
+ }
+ }
+
+ private void onMouseDown(MouseEvent e) {
+ // pass, not used yet.
+ }
+
+ /**
+ * Performs selection on mouse up (not mouse down).
+ * <p/>
+ * Shift key is used to toggle in multi-selection.
+ * Alt key is used to cycle selection through objects at the same level than the one
+ * pointed at (i.e. click on an object then alt-click to cycle).
+ */
+ private void onMouseUp(MouseEvent e) {
+ if (mLastValidResult != null) {
+
+ boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
+ boolean isAlt = (e.stateMask & SWT.ALT) != 0;
+
+ int x = e.x - IMAGE_MARGIN;
+ int y = e.y - IMAGE_MARGIN;
+ CanvasViewInfo vi = findViewInfoAt(x, y);
+
+ if (isShift && !isAlt) {
+ // Case where shift is pressed: pointed object is toggled.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // If nothing has been found at the cursor, assume it might be a user error
+ // and avoid clearing the existing selection.
+
+ if (vi != null) {
+ // toggle this selection on-off: remove it if already selected
+ if (deselect(vi)) {
+ redraw();
+ return;
+ }
+
+ // otherwise add it.
+ mSelections.add(new CanvasSelection(vi, mRulesEngine));
+ redraw();
+ }
+
+ } else if (isAlt) {
+ // Case where alt is pressed: select or cycle the object pointed at.
+
+ // Note: if shift and alt are pressed, shift is ignored. The alternate selection
+ // mechanism does not reset the current multiple selection unless they intersect.
+
+ // We need to remember the "origin" of the alternate selection, to be
+ // able to continue cycling through it later. If there's no alternate selection,
+ // create one. If there's one but not for the same origin object, create a new
+ // one too.
+ if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
+ mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt(
+ x, y, mLastValidViewInfoRoot, null));
+
+ // deselect them all, in case they were partially selected
+ deselectAll(mAltSelection.getAltViews());
+
+ // select the current one
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ if (vi2 != null) {
+ mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));
+ }
+ } else {
+ // We're trying to cycle through the current alternate selection.
+ // First remove the current object.
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ deselect(vi2);
+
+ // Now select the next one.
+ vi2 = mAltSelection.getNext();
+ if (vi2 != null) {
+ mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));
+ }
+ }
+ redraw();
+
+ } else {
+ // Case where no modifier is pressed: either select or reset the selection.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // reset (multi)selection if any
+ if (mSelections.size() > 0) {
+ if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
+ // CanvasSelection remains the same, don't touch it.
+ return;
+ }
+ mSelections.clear();
+ }
+
+ if (vi != null) {
+ mSelections.add(new CanvasSelection(vi, mRulesEngine));
+ }
+ redraw();
+ }
+ }
+ }
+
+ /** Deselects a view info. Returns true if the object was actually selected. */
+ private boolean deselect(CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return false;
+ }
+
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+ if (canvasViewInfo == s.getViewInfo()) {
+ it.remove();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Deselects multiple view infos, */
+ private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+ if (canvasViewInfos.contains(s.getViewInfo())) {
+ it.remove();
+ }
+ }
+ }
+
+ private void onDoubleClick(MouseEvent e) {
+ // pass, not used yet.
+ }
+
+ /**
+ * Tries to find a child with the same view key in the view info sub-tree.
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo.getUiViewKey() == viewKey) {
+ return canvasViewInfo;
+ }
+
+ // try to find a matching child
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ CanvasViewInfo v = findViewInfoKey(viewKey, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree, starting at the last know view info root.
+ * This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found or if there's view info root.
+ */
+ /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
+ if (mLastValidViewInfoRoot == null) {
+ return null;
+ } else {
+ return findViewInfoAt(x, y, mLastValidViewInfoRoot);
+ }
+ }
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) {
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (r.contains(x, y)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ CanvasViewInfo v = findViewInfoAt(x, y, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ // if no children matched, this is the view that we're looking for
+ return canvasViewInfo;
+ }
+
+ return null;
+ }
+
+ private ArrayList<CanvasViewInfo> findAltViewInfoAt(
+ int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) {
+ Rectangle r;
+
+ if (outList == null) {
+ outList = new ArrayList<CanvasViewInfo>();
+
+ // add the parent root only once
+ r = parent.getSelectionRect();
+ if (r.contains(x, y)) {
+ outList.add(parent);
+ }
+ }
+
+ if (parent.getChildren().size() > 0) {
+ // then add all children that match the position
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(x, y)) {
+ outList.add(child);
+ }
+ }
+
+ // finally recurse in the children
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(x, y)) {
+ findAltViewInfoAt(x, y, child, outList);
+ }
+ }
+ }
+
+ return outList;
+ }
+
+ /**
+ * Used by {@link #onSelectAll()} to add all current view infos to the selection list.
+ *
+ * @param canvasViewInfo The root to add. This info and all its children will be added to the
+ * selection list.
+ */
+ private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
+ mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine));
+ for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
+ selectAllViewInfos(vi);
+ }
+ }
+}
-/*\r
- * Copyright (C) 2009 The Android Open Source Project\r
- *\r
- * Licensed under the Eclipse Public License, Version 1.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.eclipse.org/org/documents/epl-v10.php\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-package com.android.ide.eclipse.adt.internal.editors.layout.gle2;\r
-\r
-import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;\r
-import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;\r
-\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.custom.CLabel;\r
-import org.eclipse.swt.dnd.DND;\r
-import org.eclipse.swt.dnd.DragSource;\r
-import org.eclipse.swt.dnd.DragSourceEvent;\r
-import org.eclipse.swt.dnd.DragSourceListener;\r
-import org.eclipse.swt.dnd.Transfer;\r
-import org.eclipse.swt.events.MouseEvent;\r
-import org.eclipse.swt.events.MouseListener;\r
-import org.eclipse.swt.events.MouseTrackListener;\r
-import org.eclipse.swt.graphics.Point;\r
-import org.eclipse.swt.layout.GridData;\r
-import org.eclipse.swt.layout.GridLayout;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Control;\r
-import org.eclipse.swt.widgets.Event;\r
-import org.eclipse.swt.widgets.Listener;\r
-import org.eclipse.swt.widgets.ScrollBar;\r
-\r
-import java.util.ArrayList;\r
-import java.util.List;\r
-\r
-/**\r
- * A palette composite for the {@link GraphicalEditorPart}.\r
- * <p/>\r
- * The palette contains several groups, each with a UI name (e.g. layouts and views) and each\r
- * with a list of element descriptors.\r
- * <p/>\r
- *\r
- * @since GLE2\r
- *\r
- * TODO list:\r
- * - The available items should depend on the actual GLE2 Canvas selection. Selected android\r
- * views should force filtering on what they accept can be dropped on them (e.g. TabHost,\r
- * TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.\r
- * - Optional: a text filter\r
- * - Optional: have icons that depict the element and/or automatically rendered icons\r
- * based on a rendering of the widget.\r
- * - Optional: have context-sensitive tools items, e.g. selection arrow tool,\r
- * group selection tool, alignment, etc.\r
- * - Different view strategies: big icon, small icons, text vs no text, compact grid.\r
- * - This would only be useful with meaningful icons. Out current 1-letter icons are not enough\r
- * to get rid of text labels.\r
- */\r
-public class PaletteComposite extends Composite {\r
-\r
-\r
- /** The parent grid layout that contains all the {@link Toggle} and {@link Item} widgets. */\r
- private Composite mRoot;\r
-\r
- /**\r
- * Create the composite.\r
- * @param parent The parent composite.\r
- */\r
- public PaletteComposite(Composite parent) {\r
- super(parent, SWT.BORDER | SWT.V_SCROLL);\r
- }\r
-\r
- @Override\r
- protected void checkSubclass() {\r
- // Disable the check that prevents subclassing of SWT components\r
- }\r
-\r
- /**\r
- * Loads or reloads the palette elements by using the layout and view descriptors from the\r
- * given target data.\r
- *\r
- * @param targetData The target data that contains the descriptors. If null or empty,\r
- * no groups will be created.\r
- */\r
- public void reloadPalette(AndroidTargetData targetData) {\r
-\r
- for (Control c : getChildren()) {\r
- c.dispose();\r
- }\r
-\r
- setGridLayout(this, 2);\r
-\r
- mRoot = new Composite(this, SWT.NONE);\r
- setGridLayout(mRoot, 0);\r
-\r
- if (targetData != null) {\r
- addGroup(mRoot, "Views", targetData.getLayoutDescriptors().getViewDescriptors());\r
- addGroup(mRoot, "Layouts", targetData.getLayoutDescriptors().getLayoutDescriptors());\r
- }\r
-\r
- layout(true);\r
-\r
- final ScrollBar vbar = getVerticalBar();\r
-\r
- vbar.setMaximum(getSize().y);\r
-\r
- for (Listener listener : vbar.getListeners(SWT.Selection)) {\r
- vbar.removeListener(SWT.Selection, listener);\r
- }\r
-\r
- vbar.addListener(SWT.Selection, new Listener() {\r
- public void handleEvent(Event event) {\r
- Point p = mRoot.getLocation();\r
- p.y = - vbar.getSelection();\r
- mRoot.setLocation(p);\r
- }\r
- });\r
- }\r
-\r
- private void setGridLayout(Composite parent, int spacing) {\r
- GridLayout gl = new GridLayout(1, false);\r
- gl.horizontalSpacing = 0;\r
- gl.verticalSpacing = 0;\r
- gl.marginHeight = spacing;\r
- gl.marginBottom = spacing;\r
- gl.marginLeft = spacing;\r
- gl.marginRight = spacing;\r
- gl.marginTop = spacing;\r
- gl.marginBottom = spacing;\r
- parent.setLayout(gl);\r
- }\r
-\r
- private void addGroup(Composite parent,\r
- String uiName,\r
- List<ElementDescriptor> descriptors) {\r
-\r
- Composite group = new Composite(parent, SWT.NONE);\r
- setGridLayout(group, 0);\r
-\r
- Toggle toggle = new Toggle(group, uiName);\r
-\r
- for (ElementDescriptor desc : descriptors) {\r
- Item item = new Item(group, desc);\r
- toggle.addItem(item);\r
- GridData gd = new GridData();\r
- item.setLayoutData(gd);\r
- }\r
- }\r
-\r
- /**\r
- * A Toggle widget is a row that is the head of a group.\r
- * <p/>\r
- * When clicked, the toggle will show/hide all the {@link Item} widgets that have been\r
- * added to it using {@link #addItem(Item)}.\r
- */\r
- private static class Toggle extends CLabel implements MouseTrackListener, MouseListener {\r
- private boolean mMouseIn;\r
- private DragSource mSource;\r
- private ArrayList<Item> mItems = new ArrayList<Item>();\r
-\r
- public Toggle(Composite parent, String groupName) {\r
- super(parent, SWT.NONE);\r
- mMouseIn = false;\r
-\r
- setData(null);\r
-\r
- String s = String.format("-= %s =-", groupName);\r
- setText(s);\r
- setToolTipText(s);\r
- //TODO use triangle icon and swap it -- setImage(desc.getIcon());\r
- addMouseTrackListener(this);\r
- addMouseListener(this);\r
- }\r
-\r
- public void addItem(Item item) {\r
- mItems.add(item);\r
- }\r
-\r
- @Override\r
- public void dispose() {\r
- if (mSource != null) {\r
- mSource.dispose();\r
- mSource = null;\r
- }\r
- super.dispose();\r
- }\r
-\r
- @Override\r
- public int getStyle() {\r
- int style = super.getStyle();\r
- if (mMouseIn) {\r
- style |= SWT.SHADOW_IN;\r
- }\r
- return style;\r
- }\r
-\r
- // -- MouseTrackListener callbacks\r
-\r
- public void mouseEnter(MouseEvent e) {\r
- if (!mMouseIn) {\r
- mMouseIn = true;\r
- redraw();\r
- }\r
- }\r
-\r
- public void mouseExit(MouseEvent e) {\r
- if (mMouseIn) {\r
- mMouseIn = false;\r
- redraw();\r
- }\r
- }\r
-\r
- public void mouseHover(MouseEvent e) {\r
- // pass\r
- }\r
-\r
- // -- MouseListener callbacks\r
-\r
- public void mouseDoubleClick(MouseEvent arg0) {\r
- // pass\r
- }\r
-\r
- public void mouseDown(MouseEvent arg0) {\r
- // pass\r
- }\r
-\r
- public void mouseUp(MouseEvent arg0) {\r
- for (Item i : mItems) {\r
- if (i.isVisible()) {\r
- Object ld = i.getLayoutData();\r
- if (ld instanceof GridData) {\r
- GridData gd = (GridData) ld;\r
-\r
- i.setData(gd.heightHint != SWT.DEFAULT ?\r
- Integer.valueOf(gd.heightHint) :\r
- null);\r
- gd.heightHint = 0;\r
- }\r
- } else {\r
- Object ld = i.getLayoutData();\r
- if (ld instanceof GridData) {\r
- GridData gd = (GridData) ld;\r
-\r
- Object d = i.getData();\r
- if (d instanceof Integer) {\r
- gd.heightHint = ((Integer) d).intValue();\r
- } else {\r
- gd.heightHint = SWT.DEFAULT;\r
- }\r
- }\r
- }\r
- i.setVisible(!i.isVisible());\r
- }\r
-\r
- getParent().getParent().layout(true /*changed*/);\r
- }\r
- }\r
-\r
- /**\r
- * An Item widget represents one {@link ElementDescriptor} that can be dropped on the\r
- * GLE2 canvas using drag'n'drop.\r
- */\r
- private static class Item extends CLabel implements MouseTrackListener {\r
-\r
- private boolean mMouseIn;\r
- private DragSource mSource;\r
-\r
- public Item(Composite parent, ElementDescriptor desc) {\r
- super(parent, SWT.NONE);\r
- mMouseIn = false;\r
-\r
- setText(desc.getUiName());\r
- setImage(desc.getIcon());\r
- setToolTipText(desc.getTooltip());\r
- addMouseTrackListener(this);\r
-\r
- // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html\r
- mSource = new DragSource(this, DND.DROP_COPY);\r
- mSource.setTransfer(new Transfer[] { ElementDescTransfer.getInstance() });\r
- mSource.addDragListener(new DescDragSourceListener(desc));\r
- }\r
-\r
- @Override\r
- public void dispose() {\r
- if (mSource != null) {\r
- mSource.dispose();\r
- mSource = null;\r
- }\r
- super.dispose();\r
- }\r
-\r
- @Override\r
- public int getStyle() {\r
- int style = super.getStyle();\r
- if (mMouseIn) {\r
- style |= SWT.SHADOW_IN;\r
- }\r
- return style;\r
- }\r
-\r
- public void mouseEnter(MouseEvent e) {\r
- if (!mMouseIn) {\r
- mMouseIn = true;\r
- redraw();\r
- }\r
- }\r
-\r
- public void mouseExit(MouseEvent e) {\r
- if (mMouseIn) {\r
- mMouseIn = false;\r
- redraw();\r
- }\r
- }\r
-\r
- public void mouseHover(MouseEvent e) {\r
- // pass\r
- }\r
- }\r
-\r
- /**\r
- * A {@link DragSourceListener} that deals with drag'n'drop of\r
- * {@link ElementDescriptor}s.\r
- */\r
- private static class DescDragSourceListener implements DragSourceListener {\r
-\r
- private final ElementDescriptor mDesc;\r
-\r
- public DescDragSourceListener(ElementDescriptor desc) {\r
- mDesc = desc;\r
- }\r
-\r
- public void dragStart(DragSourceEvent e) {\r
- if (mDesc == null) {\r
- e.doit = false;\r
- }\r
- }\r
-\r
-\r
- public void dragSetData(DragSourceEvent e) {\r
- // Provide the data for the drop when requested by the other side.\r
- if (ElementDescTransfer.getInstance().isSupportedType(e.dataType)) {\r
- e.data = mDesc;\r
- }\r
- }\r
-\r
- public void dragFinished(DragSourceEvent e) {\r
- // Nothing to do here.\r
- }\r
- }\r
-\r
-}\r
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ScrollBar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A palette composite for the {@link GraphicalEditorPart}.
+ * <p/>
+ * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
+ * with a list of element descriptors.
+ * <p/>
+ *
+ * @since GLE2
+ *
+ * TODO list:
+ * - The available items should depend on the actual GLE2 Canvas selection. Selected android
+ * views should force filtering on what they accept can be dropped on them (e.g. TabHost,
+ * TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
+ * - Optional: a text filter
+ * - Optional: have icons that depict the element and/or automatically rendered icons
+ * based on a rendering of the widget.
+ * - Optional: have context-sensitive tools items, e.g. selection arrow tool,
+ * group selection tool, alignment, etc.
+ * - Different view strategies: big icon, small icons, text vs no text, compact grid.
+ * - This would only be useful with meaningful icons. Out current 1-letter icons are not enough
+ * to get rid of text labels.
+ */
+public class PaletteComposite extends Composite {
+
+
+ /** The parent grid layout that contains all the {@link Toggle} and {@link Item} widgets. */
+ private Composite mRoot;
+
+ /**
+ * Create the composite.
+ * @param parent The parent composite.
+ */
+ public PaletteComposite(Composite parent) {
+ super(parent, SWT.BORDER | SWT.V_SCROLL);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ /**
+ * Loads or reloads the palette elements by using the layout and view descriptors from the
+ * given target data.
+ *
+ * @param targetData The target data that contains the descriptors. If null or empty,
+ * no groups will be created.
+ */
+ public void reloadPalette(AndroidTargetData targetData) {
+
+ for (Control c : getChildren()) {
+ c.dispose();
+ }
+
+ setGridLayout(this, 2);
+
+ mRoot = new Composite(this, SWT.NONE);
+ setGridLayout(mRoot, 0);
+
+ if (targetData != null) {
+ addGroup(mRoot, "Views", targetData.getLayoutDescriptors().getViewDescriptors());
+ addGroup(mRoot, "Layouts", targetData.getLayoutDescriptors().getLayoutDescriptors());
+ }
+
+ layout(true);
+
+ final ScrollBar vbar = getVerticalBar();
+
+ vbar.setMaximum(getSize().y);
+
+ for (Listener listener : vbar.getListeners(SWT.Selection)) {
+ vbar.removeListener(SWT.Selection, listener);
+ }
+
+ vbar.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ Point p = mRoot.getLocation();
+ p.y = - vbar.getSelection();
+ mRoot.setLocation(p);
+ }
+ });
+ }
+
+ private void setGridLayout(Composite parent, int spacing) {
+ GridLayout gl = new GridLayout(1, false);
+ gl.horizontalSpacing = 0;
+ gl.verticalSpacing = 0;
+ gl.marginHeight = spacing;
+ gl.marginBottom = spacing;
+ gl.marginLeft = spacing;
+ gl.marginRight = spacing;
+ gl.marginTop = spacing;
+ gl.marginBottom = spacing;
+ parent.setLayout(gl);
+ }
+
+ private void addGroup(Composite parent,
+ String uiName,
+ List<ElementDescriptor> descriptors) {
+
+ Composite group = new Composite(parent, SWT.NONE);
+ setGridLayout(group, 0);
+
+ Toggle toggle = new Toggle(group, uiName);
+
+ for (ElementDescriptor desc : descriptors) {
+ Item item = new Item(group, desc);
+ toggle.addItem(item);
+ GridData gd = new GridData();
+ item.setLayoutData(gd);
+ }
+ }
+
+ /**
+ * A Toggle widget is a row that is the head of a group.
+ * <p/>
+ * When clicked, the toggle will show/hide all the {@link Item} widgets that have been
+ * added to it using {@link #addItem(Item)}.
+ */
+ private static class Toggle extends CLabel implements MouseTrackListener, MouseListener {
+ private boolean mMouseIn;
+ private DragSource mSource;
+ private ArrayList<Item> mItems = new ArrayList<Item>();
+
+ public Toggle(Composite parent, String groupName) {
+ super(parent, SWT.NONE);
+ mMouseIn = false;
+
+ setData(null);
+
+ String s = String.format("-= %s =-", groupName);
+ setText(s);
+ setToolTipText(s);
+ //TODO use triangle icon and swap it -- setImage(desc.getIcon());
+ addMouseTrackListener(this);
+ addMouseListener(this);
+ }
+
+ public void addItem(Item item) {
+ mItems.add(item);
+ }
+
+ @Override
+ public void dispose() {
+ if (mSource != null) {
+ mSource.dispose();
+ mSource = null;
+ }
+ super.dispose();
+ }
+
+ @Override
+ public int getStyle() {
+ int style = super.getStyle();
+ if (mMouseIn) {
+ style |= SWT.SHADOW_IN;
+ }
+ return style;
+ }
+
+ // -- MouseTrackListener callbacks
+
+ public void mouseEnter(MouseEvent e) {
+ if (!mMouseIn) {
+ mMouseIn = true;
+ redraw();
+ }
+ }
+
+ public void mouseExit(MouseEvent e) {
+ if (mMouseIn) {
+ mMouseIn = false;
+ redraw();
+ }
+ }
+
+ public void mouseHover(MouseEvent e) {
+ // pass
+ }
+
+ // -- MouseListener callbacks
+
+ public void mouseDoubleClick(MouseEvent arg0) {
+ // pass
+ }
+
+ public void mouseDown(MouseEvent arg0) {
+ // pass
+ }
+
+ public void mouseUp(MouseEvent arg0) {
+ for (Item i : mItems) {
+ if (i.isVisible()) {
+ Object ld = i.getLayoutData();
+ if (ld instanceof GridData) {
+ GridData gd = (GridData) ld;
+
+ i.setData(gd.heightHint != SWT.DEFAULT ?
+ Integer.valueOf(gd.heightHint) :
+ null);
+ gd.heightHint = 0;
+ }
+ } else {
+ Object ld = i.getLayoutData();
+ if (ld instanceof GridData) {
+ GridData gd = (GridData) ld;
+
+ Object d = i.getData();
+ if (d instanceof Integer) {
+ gd.heightHint = ((Integer) d).intValue();
+ } else {
+ gd.heightHint = SWT.DEFAULT;
+ }
+ }
+ }
+ i.setVisible(!i.isVisible());
+ }
+
+ getParent().getParent().layout(true /*changed*/);
+ }
+ }
+
+ /**
+ * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
+ * GLE2 canvas using drag'n'drop.
+ */
+ private static class Item extends CLabel implements MouseTrackListener {
+
+ private boolean mMouseIn;
+ private DragSource mSource;
+
+ public Item(Composite parent, ElementDescriptor desc) {
+ super(parent, SWT.NONE);
+ mMouseIn = false;
+
+ setText(desc.getUiName());
+ setImage(desc.getIcon());
+ setToolTipText(desc.getTooltip());
+ addMouseTrackListener(this);
+
+ // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+ mSource = new DragSource(this, DND.DROP_COPY);
+ mSource.setTransfer(new Transfer[] { ElementDescTransfer.getInstance() });
+ mSource.addDragListener(new DescDragSourceListener(desc));
+ }
+
+ @Override
+ public void dispose() {
+ if (mSource != null) {
+ mSource.dispose();
+ mSource = null;
+ }
+ super.dispose();
+ }
+
+ @Override
+ public int getStyle() {
+ int style = super.getStyle();
+ if (mMouseIn) {
+ style |= SWT.SHADOW_IN;
+ }
+ return style;
+ }
+
+ public void mouseEnter(MouseEvent e) {
+ if (!mMouseIn) {
+ mMouseIn = true;
+ redraw();
+ }
+ }
+
+ public void mouseExit(MouseEvent e) {
+ if (mMouseIn) {
+ mMouseIn = false;
+ redraw();
+ }
+ }
+
+ public void mouseHover(MouseEvent e) {
+ // pass
+ }
+ }
+
+ /**
+ * A {@link DragSourceListener} that deals with drag'n'drop of
+ * {@link ElementDescriptor}s.
+ */
+ private static class DescDragSourceListener implements DragSourceListener {
+
+ private final ElementDescriptor mDesc;
+
+ public DescDragSourceListener(ElementDescriptor desc) {
+ mDesc = desc;
+ }
+
+ public void dragStart(DragSourceEvent e) {
+ if (mDesc == null) {
+ e.doit = false;
+ }
+ }
+
+
+ public void dragSetData(DragSourceEvent e) {
+ // Provide the data for the drop when requested by the other side.
+ if (ElementDescTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDesc;
+ }
+ }
+
+ public void dragFinished(DragSourceEvent e) {
+ // Nothing to do here.
+ }
+ }
+
+}