OSDN Git Service

ADT/GLE: Improve the config selector.
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / editors / layout / gle2 / GraphicalEditorPart.java
1 /*\r
2  * Copyright (C) 2009 The Android Open Source Project\r
3  *\r
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *      http://www.eclipse.org/org/documents/epl-v10.php\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;\r
18 \r
19 import com.android.ide.eclipse.adt.AdtPlugin;\r
20 import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper;\r
21 import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor;\r
22 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;\r
23 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;\r
24 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;\r
25 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;\r
26 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;\r
27 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;\r
28 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;\r
29 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle;\r
30 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;\r
31 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;\r
32 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand;\r
33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;\r
34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;\r
35 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;\r
36 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;\r
37 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;\r
38 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;\r
39 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;\r
40 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;\r
41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;\r
42 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;\r
43 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;\r
44 import com.android.ide.eclipse.adt.internal.sdk.Sdk;\r
45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;\r
46 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;\r
47 import com.android.layoutlib.api.ILayoutBridge;\r
48 import com.android.layoutlib.api.ILayoutLog;\r
49 import com.android.layoutlib.api.ILayoutResult;\r
50 import com.android.layoutlib.api.IProjectCallback;\r
51 import com.android.layoutlib.api.IResourceValue;\r
52 import com.android.layoutlib.api.IXmlPullParser;\r
53 import com.android.sdklib.IAndroidTarget;\r
54 \r
55 import org.eclipse.core.resources.IFile;\r
56 import org.eclipse.core.resources.IFolder;\r
57 import org.eclipse.core.resources.IProject;\r
58 import org.eclipse.core.resources.IResource;\r
59 import org.eclipse.core.runtime.CoreException;\r
60 import org.eclipse.core.runtime.IProgressMonitor;\r
61 import org.eclipse.core.runtime.IStatus;\r
62 import org.eclipse.core.runtime.Status;\r
63 import org.eclipse.core.runtime.jobs.Job;\r
64 import org.eclipse.draw2d.geometry.Rectangle;\r
65 import org.eclipse.gef.ui.parts.SelectionSynchronizer;\r
66 import org.eclipse.jface.action.Action;\r
67 import org.eclipse.jface.dialogs.Dialog;\r
68 import org.eclipse.swt.SWT;\r
69 import org.eclipse.swt.custom.SashForm;\r
70 import org.eclipse.swt.custom.StyledText;\r
71 import org.eclipse.swt.dnd.Clipboard;\r
72 import org.eclipse.swt.layout.GridData;\r
73 import org.eclipse.swt.layout.GridLayout;\r
74 import org.eclipse.swt.widgets.Composite;\r
75 import org.eclipse.swt.widgets.Display;\r
76 import org.eclipse.ui.IActionBars;\r
77 import org.eclipse.ui.IEditorInput;\r
78 import org.eclipse.ui.IEditorSite;\r
79 import org.eclipse.ui.PartInitException;\r
80 import org.eclipse.ui.actions.ActionFactory;\r
81 import org.eclipse.ui.ide.IDE;\r
82 import org.eclipse.ui.part.EditorPart;\r
83 import org.eclipse.ui.part.FileEditorInput;\r
84 \r
85 import java.io.File;\r
86 import java.io.FileOutputStream;\r
87 import java.io.IOException;\r
88 import java.io.InputStream;\r
89 import java.io.PrintStream;\r
90 import java.util.Map;\r
91 \r
92 /**\r
93  * Graphical layout editor part, version 2.\r
94  *\r
95  * @since GLE2\r
96  *\r
97  * TODO List:\r
98  * - display error icon\r
99  * - finish palette (see palette's todo list)\r
100  * - finish canvas (see canva's todo list)\r
101  * - completly rethink the property panel\r
102  * - link to the existing outline editor (prolly reuse/adapt for simplier model and will need\r
103  *   to adapt the selection synchronizer.)\r
104  */\r
105 public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutEditor {\r
106 \r
107     /*\r
108      * Useful notes:\r
109      * To understand Drag'n'drop:\r
110      *   http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html\r
111      */\r
112 \r
113     /** Reference to the layout editor */\r
114     private final LayoutEditor mLayoutEditor;\r
115 \r
116     /** reference to the file being edited. Can also be used to access the {@link IProject}. */\r
117     private IFile mEditedFile;\r
118 \r
119     /** The current clipboard. Must be disposed later. */\r
120     private Clipboard mClipboard;\r
121 \r
122     /** The configuration composite at the top of the layout editor. */\r
123     private ConfigurationComposite mConfigComposite;\r
124 \r
125     /** The sash that splits the palette from the canvas. */\r
126     private SashForm mSashPalette;\r
127     private SashForm mSashError;\r
128 \r
129     /** The palette displayed on the left of the sash. */\r
130     private PaletteComposite mPalette;\r
131 \r
132     /** The layout canvas displayed to the right of the sash. */\r
133     private LayoutCanvas mLayoutCanvas;\r
134 \r
135     /** The Groovy Rules Engine associated with this editor. It is project-specific. */\r
136     private RulesEngine mRulesEngine;\r
137 \r
138     private StyledText mErrorLabel;\r
139 \r
140     /** The {@link FolderConfiguration} being edited. */\r
141     private FolderConfiguration mEditedConfig;\r
142 \r
143     private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;\r
144     private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;\r
145     private ProjectCallback mProjectCallback;\r
146     private ILayoutLog mLogger;\r
147 \r
148     private boolean mNeedsXmlReload = false;\r
149     private boolean mNeedsRecompute = false;\r
150 \r
151     private TargetListener mTargetListener;\r
152 \r
153     private ConfigListener mConfigListener;\r
154 \r
155     private ReloadListener mReloadListener;\r
156 \r
157     protected boolean mUseExplodeMode;\r
158 \r
159 \r
160     public GraphicalEditorPart(LayoutEditor layoutEditor) {\r
161         mLayoutEditor = layoutEditor;\r
162         setPartName("Graphical Layout");\r
163     }\r
164 \r
165     // ------------------------------------\r
166     // Methods overridden from base classes\r
167     //------------------------------------\r
168 \r
169     /**\r
170      * Initializes the editor part with a site and input.\r
171      * {@inheritDoc}\r
172      */\r
173     @Override\r
174     public void init(IEditorSite site, IEditorInput input) throws PartInitException {\r
175         setSite(site);\r
176         useNewEditorInput(input);\r
177 \r
178         if (mTargetListener == null) {\r
179             mTargetListener = new TargetListener();\r
180             AdtPlugin.getDefault().addTargetListener(mTargetListener);\r
181         }\r
182     }\r
183 \r
184     /**\r
185      * Reloads this editor, by getting the new model from the {@link LayoutEditor}.\r
186      */\r
187     public void reloadEditor() {\r
188         // nothing to be done here. the edited file is now set by initWithFile(IFile)\r
189     }\r
190 \r
191     private void useNewEditorInput(IEditorInput input) throws PartInitException {\r
192         // The contract of init() mentions we need to fail if we can't understand the input.\r
193         if (!(input instanceof FileEditorInput)) {\r
194             throw new PartInitException("Input is not of type FileEditorInput: " +  //$NON-NLS-1$\r
195                     input == null ? "null" : input.toString());                     //$NON-NLS-1$\r
196         }\r
197     }\r
198 \r
199     @Override\r
200     public void createPartControl(Composite parent) {\r
201 \r
202         Display d = parent.getDisplay();\r
203         mClipboard = new Clipboard(d);\r
204 \r
205         GridLayout gl = new GridLayout(1, false);\r
206         parent.setLayout(gl);\r
207         gl.marginHeight = gl.marginWidth = 0;\r
208 \r
209         // create the top part for the configuration control\r
210 \r
211         CustomToggle[] toggles = new CustomToggle[] {\r
212                 new CustomToggle(\r
213                         "Explode",\r
214                         null, //image\r
215                         "Displays extra margins in the layout."\r
216                         ) {\r
217                     @Override\r
218                     public void onSelected(boolean newState) {\r
219                         mUseExplodeMode = newState;\r
220                         recomputeLayout();\r
221                     }\r
222                 },\r
223                 new CustomToggle(\r
224                         "Outline",\r
225                         null, //image\r
226                         "Shows the of all views in the layout."\r
227                         ) {\r
228                     @Override\r
229                     public void onSelected(boolean newState) {\r
230                         mLayoutCanvas.setShowOutline(newState);\r
231                     }\r
232                 }\r
233         };\r
234 \r
235         mConfigListener = new ConfigListener();\r
236         mConfigComposite = new ConfigurationComposite(mConfigListener, toggles, parent, SWT.BORDER);\r
237         mConfigComposite.updateThemesAndLocales();\r
238 \r
239         mSashPalette = new SashForm(parent, SWT.HORIZONTAL);\r
240         mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH));\r
241 \r
242         mPalette = new PaletteComposite(mSashPalette);\r
243 \r
244         mSashError = new SashForm(mSashPalette, SWT.VERTICAL | SWT.BORDER);\r
245         mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));\r
246 \r
247         mLayoutCanvas = new LayoutCanvas(mSashError, SWT.NONE);\r
248         mLayoutCanvas.setRulesEngine(mRulesEngine);\r
249 \r
250         mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY);\r
251         mErrorLabel.setEditable(false);\r
252         mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));\r
253         mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));\r
254 \r
255         mSashPalette.setWeights(new int[] { 20, 80 });\r
256         mSashError.setWeights(new int[] { 80, 20 });\r
257         mSashError.setMaximizedControl(mLayoutCanvas);\r
258 \r
259         setupEditActions();\r
260 \r
261         // Initialize the state\r
262         reloadPalette();\r
263     }\r
264 \r
265     private void setupEditActions() {\r
266 \r
267         IActionBars actionBars = getEditorSite().getActionBars();\r
268 \r
269         actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action("Copy") {\r
270             @Override\r
271             public void run() {\r
272                 // TODO enable copy only when there's a selection\r
273                 mLayoutCanvas.onCopy(mClipboard);\r
274             }\r
275         });\r
276 \r
277         actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), new Action("Cut") {\r
278             @Override\r
279             public void run() {\r
280                 // TODO enable cut only when there's a selection\r
281                 mLayoutCanvas.onCut(mClipboard);\r
282             }\r
283         });\r
284 \r
285         actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), new Action("Paste") {\r
286             @Override\r
287             public void run() {\r
288                 mLayoutCanvas.onPaste(mClipboard);\r
289             }\r
290         });\r
291 \r
292         actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),\r
293                 new Action("Select All") {\r
294             @Override\r
295             public void run() {\r
296                 mLayoutCanvas.onSelectAll();\r
297             }\r
298         });\r
299     }\r
300 \r
301     /**\r
302      * Switches the stack to display the error label and hide the canvas.\r
303      * @param errorFormat The new error to display if not null.\r
304      * @param parameters String.format parameters for the error format.\r
305      */\r
306     private void displayError(String errorFormat, Object...parameters) {\r
307         if (errorFormat != null) {\r
308             mErrorLabel.setText(String.format(errorFormat, parameters));\r
309         }\r
310         mSashError.setMaximizedControl(null);\r
311     }\r
312 \r
313     /** Displays the canvas and hides the error label. */\r
314     private void hideError() {\r
315         mSashError.setMaximizedControl(mLayoutCanvas);\r
316     }\r
317 \r
318     @Override\r
319     public void dispose() {\r
320         if (mTargetListener != null) {\r
321             AdtPlugin.getDefault().removeTargetListener(mTargetListener);\r
322             mTargetListener = null;\r
323         }\r
324 \r
325         if (mReloadListener != null) {\r
326             LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);\r
327             mReloadListener = null;\r
328         }\r
329 \r
330         if (mClipboard != null) {\r
331             mClipboard.dispose();\r
332             mClipboard = null;\r
333         }\r
334 \r
335         super.dispose();\r
336     }\r
337 \r
338     /**\r
339      * Listens to changes from the Configuration UI banner and triggers layout rendering when\r
340      * changed. Also provide the Configuration UI with the list of resources/layout to display.\r
341      */\r
342     private class ConfigListener implements IConfigListener {\r
343 \r
344         /**\r
345          * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.\r
346          * <p/>If there is no match, notify the user.\r
347          */\r
348         public void onConfigurationChange() {\r
349             mConfiguredFrameworkRes = mConfiguredProjectRes = null;\r
350 \r
351             if (mEditedFile == null || mEditedConfig == null) {\r
352                 return;\r
353             }\r
354 \r
355             // get the resources of the file's project.\r
356             ProjectResources resources = ResourceManager.getInstance().getProjectResources(\r
357                     mEditedFile.getProject());\r
358 \r
359             // from the resources, look for a matching file\r
360             ResourceFile match = null;\r
361             if (resources != null) {\r
362                 match = resources.getMatchingFile(mEditedFile.getName(),\r
363                                                   ResourceFolderType.LAYOUT,\r
364                                                   mConfigComposite.getCurrentConfig());\r
365             }\r
366 \r
367             if (match != null) {\r
368                 if (match.getFile().equals(mEditedFile) == false) {\r
369                     try {\r
370                         IDE.openEditor(\r
371                                 getSite().getWorkbenchWindow().getActivePage(),\r
372                                 match.getFile().getIFile());\r
373 \r
374                         // we're done!\r
375                         return;\r
376                     } catch (PartInitException e) {\r
377                         // FIXME: do something!\r
378                     }\r
379                 }\r
380 \r
381                 // at this point, we have not opened a new file.\r
382 \r
383                 // Even though the layout doesn't change, the config changed, and referenced\r
384                 // resources need to be updated.\r
385                 recomputeLayout();\r
386             } else {\r
387                 // display the error.\r
388                 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();\r
389                 displayError(\r
390                         "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
391                         currentConfig.toDisplayString(),\r
392                         currentConfig.getFolderName(ResourceFolderType.LAYOUT,\r
393                                 Sdk.getCurrent().getTarget(mEditedFile.getProject())),\r
394                         mEditedFile.getName());\r
395             }\r
396         }\r
397 \r
398         public void onThemeChange() {\r
399             recomputeLayout();\r
400         }\r
401 \r
402         public void onClippingChange() {\r
403             recomputeLayout();\r
404         }\r
405 \r
406         public void onCreate() {\r
407             LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(),\r
408                     mEditedFile.getName(),\r
409                     Sdk.getCurrent().getTarget(mEditedFile.getProject()),\r
410                     mConfigComposite.getCurrentConfig());\r
411             if (dialog.open() == Dialog.OK) {\r
412                 final FolderConfiguration config = new FolderConfiguration();\r
413                 dialog.getConfiguration(config);\r
414 \r
415                 createAlternateLayout(config);\r
416             }\r
417         }\r
418 \r
419         public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {\r
420             if (mConfiguredFrameworkRes == null && mConfigComposite != null) {\r
421                 ProjectResources frameworkRes = getFrameworkResources();\r
422 \r
423                 if (frameworkRes == null) {\r
424                     AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");\r
425                 } else {\r
426                     // get the framework resource values based on the current config\r
427                     mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(\r
428                             mConfigComposite.getCurrentConfig());\r
429                 }\r
430             }\r
431 \r
432             return mConfiguredFrameworkRes;\r
433         }\r
434 \r
435         public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() {\r
436             if (mConfiguredProjectRes == null && mConfigComposite != null) {\r
437                 ProjectResources project = getProjectResources();\r
438 \r
439                 // make sure they are loaded\r
440                 project.loadAll();\r
441 \r
442                 // get the project resource values based on the current config\r
443                 mConfiguredProjectRes = project.getConfiguredResources(\r
444                         mConfigComposite.getCurrentConfig());\r
445             }\r
446 \r
447             return mConfiguredProjectRes;\r
448         }\r
449 \r
450         /**\r
451          * Returns a {@link ProjectResources} for the framework resources.\r
452          * @return the framework resources or null if not found.\r
453          */\r
454         public ProjectResources getFrameworkResources() {\r
455             if (mEditedFile != null) {\r
456                 Sdk currentSdk = Sdk.getCurrent();\r
457                 if (currentSdk != null) {\r
458                     IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
459 \r
460                     if (target != null) {\r
461                         AndroidTargetData data = currentSdk.getTargetData(target);\r
462 \r
463                         if (data != null) {\r
464                             return data.getFrameworkResources();\r
465                         }\r
466                     }\r
467                 }\r
468             }\r
469 \r
470             return null;\r
471         }\r
472 \r
473         public ProjectResources getProjectResources() {\r
474             if (mEditedFile != null) {\r
475                 ResourceManager manager = ResourceManager.getInstance();\r
476                 return manager.getProjectResources(mEditedFile.getProject());\r
477             }\r
478 \r
479             return null;\r
480         }\r
481 \r
482         /**\r
483          * Creates a new layout file from the specified {@link FolderConfiguration}.\r
484          */\r
485         private void createAlternateLayout(final FolderConfiguration config) {\r
486             new Job("Create Alternate Resource") {\r
487                 @Override\r
488                 protected IStatus run(IProgressMonitor monitor) {\r
489                     // get the folder name\r
490                     String folderName = config.getFolderName(ResourceFolderType.LAYOUT,\r
491                             Sdk.getCurrent().getTarget(mEditedFile.getProject()));\r
492                     try {\r
493 \r
494                         // look to see if it exists.\r
495                         // get the res folder\r
496                         IFolder res = (IFolder)mEditedFile.getParent().getParent();\r
497                         String path = res.getLocation().toOSString();\r
498 \r
499                         File newLayoutFolder = new File(path + File.separator + folderName);\r
500                         if (newLayoutFolder.isFile()) {\r
501                             // this should not happen since aapt would have complained\r
502                             // before, but if one disable the automatic build, this could\r
503                             // happen.\r
504                             String message = String.format("File 'res/%1$s' is in the way!",\r
505                                     folderName);\r
506 \r
507                             AdtPlugin.displayError("Layout Creation", message);\r
508 \r
509                             return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);\r
510                         } else if (newLayoutFolder.exists() == false) {\r
511                             // create it.\r
512                             newLayoutFolder.mkdir();\r
513                         }\r
514 \r
515                         // now create the file\r
516                         File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +\r
517                                     File.separator + mEditedFile.getName());\r
518 \r
519                         newLayoutFile.createNewFile();\r
520 \r
521                         InputStream input = mEditedFile.getContents();\r
522 \r
523                         FileOutputStream fos = new FileOutputStream(newLayoutFile);\r
524 \r
525                         byte[] data = new byte[512];\r
526                         int count;\r
527                         while ((count = input.read(data)) != -1) {\r
528                             fos.write(data, 0, count);\r
529                         }\r
530 \r
531                         input.close();\r
532                         fos.close();\r
533 \r
534                         // refreshes the res folder to show up the new\r
535                         // layout folder (if needed) and the file.\r
536                         // We use a progress monitor to catch the end of the refresh\r
537                         // to trigger the edit of the new file.\r
538                         res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {\r
539                             public void done() {\r
540                                 mConfigComposite.getDisplay().asyncExec(new Runnable() {\r
541                                     public void run() {\r
542                                         onConfigurationChange();\r
543                                     }\r
544                                 });\r
545                             }\r
546 \r
547                             public void beginTask(String name, int totalWork) {\r
548                                 // pass\r
549                             }\r
550 \r
551                             public void internalWorked(double work) {\r
552                                 // pass\r
553                             }\r
554 \r
555                             public boolean isCanceled() {\r
556                                 // pass\r
557                                 return false;\r
558                             }\r
559 \r
560                             public void setCanceled(boolean value) {\r
561                                 // pass\r
562                             }\r
563 \r
564                             public void setTaskName(String name) {\r
565                                 // pass\r
566                             }\r
567 \r
568                             public void subTask(String name) {\r
569                                 // pass\r
570                             }\r
571 \r
572                             public void worked(int work) {\r
573                                 // pass\r
574                             }\r
575                         });\r
576                     } catch (IOException e2) {\r
577                         String message = String.format(\r
578                                 "Failed to create File 'res/%1$s/%2$s' : %3$s",\r
579                                 folderName, mEditedFile.getName(), e2.getMessage());\r
580 \r
581                         AdtPlugin.displayError("Layout Creation", message);\r
582 \r
583                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,\r
584                                 message, e2);\r
585                     } catch (CoreException e2) {\r
586                         String message = String.format(\r
587                                 "Failed to create File 'res/%1$s/%2$s' : %3$s",\r
588                                 folderName, mEditedFile.getName(), e2.getMessage());\r
589 \r
590                         AdtPlugin.displayError("Layout Creation", message);\r
591 \r
592                         return e2.getStatus();\r
593                     }\r
594 \r
595                     return Status.OK_STATUS;\r
596 \r
597                 }\r
598             }.schedule();\r
599         }\r
600     }\r
601 \r
602     /**\r
603      * Listens to target changed in the current project, to trigger a new layout rendering.\r
604      */\r
605     private class TargetListener extends TargetChangeListener {\r
606         boolean mSdkLoaded = false; // indicates whether we got a sdk loaded event.\r
607 \r
608         @Override\r
609         public IProject getProject() {\r
610             return getLayoutEditor().getProject();\r
611         }\r
612 \r
613         @Override\r
614         public void reload() {\r
615             // because the target changed we must reset the configured resources.\r
616             mConfiguredFrameworkRes = mConfiguredProjectRes = null;\r
617 \r
618             // make sure we remove the custom view loader, since its parent class loader is the\r
619             // bridge class loader.\r
620             mProjectCallback = null;\r
621 \r
622             // update the themes and locales since the target change could have changed the\r
623             // theme list.\r
624             mConfigComposite.updateThemesAndLocales();\r
625 \r
626             // reset the config selector UI to match the new target (and possibly sdk).\r
627             if (mEditedConfig != null) {\r
628                 if (mSdkLoaded) {\r
629                     onSdkChange();\r
630                 } else {\r
631                     onTargetChange();\r
632                 }\r
633             }\r
634 \r
635             // SDK change has been handled, reset the flag.\r
636             mSdkLoaded = false;\r
637         }\r
638 \r
639         @Override\r
640         public void onSdkLoaded() {\r
641             mSdkLoaded = true; // this will be reset when we get the target loaded event\r
642                                // which always comes after.\r
643         }\r
644     }\r
645 \r
646     // ----------------\r
647 \r
648     /**\r
649      * Save operation in the Graphical Editor Part.\r
650      * <p/>\r
651      * In our workflow, the model is owned by the Structured XML Editor.\r
652      * The graphical layout editor just displays it -- thus we don't really\r
653      * save anything here.\r
654      * <p/>\r
655      * This must NOT call the parent editor part. At the contrary, the parent editor\r
656      * part will call this *after* having done the actual save operation.\r
657      * <p/>\r
658      * The only action this editor must do is mark the undo command stack as\r
659      * being no longer dirty.\r
660      */\r
661     @Override\r
662     public void doSave(IProgressMonitor monitor) {\r
663         // TODO implement a command stack\r
664 //        getCommandStack().markSaveLocation();\r
665 //        firePropertyChange(PROP_DIRTY);\r
666     }\r
667 \r
668     /**\r
669      * Save operation in the Graphical Editor Part.\r
670      * <p/>\r
671      * In our workflow, the model is owned by the Structured XML Editor.\r
672      * The graphical layout editor just displays it -- thus we don't really\r
673      * save anything here.\r
674      */\r
675     @Override\r
676     public void doSaveAs() {\r
677         // pass\r
678     }\r
679 \r
680     /**\r
681      * In our workflow, the model is owned by the Structured XML Editor.\r
682      * The graphical layout editor just displays it -- thus we don't really\r
683      * save anything here.\r
684      */\r
685     @Override\r
686     public boolean isDirty() {\r
687         return false;\r
688     }\r
689 \r
690     /**\r
691      * In our workflow, the model is owned by the Structured XML Editor.\r
692      * The graphical layout editor just displays it -- thus we don't really\r
693      * save anything here.\r
694      */\r
695     @Override\r
696     public boolean isSaveAsAllowed() {\r
697         return false;\r
698     }\r
699 \r
700     @Override\r
701     public void setFocus() {\r
702         // TODO Auto-generated method stub\r
703 \r
704     }\r
705 \r
706     /**\r
707      * Responds to a page change that made the Graphical editor page the activated page.\r
708      */\r
709     public void activated() {\r
710         if (mNeedsRecompute || mNeedsXmlReload) {\r
711             recomputeLayout();\r
712         }\r
713     }\r
714 \r
715     /**\r
716      * Responds to a page change that made the Graphical editor page the deactivated page\r
717      */\r
718     public void deactivated() {\r
719         // nothing to be done here for now.\r
720     }\r
721 \r
722     /**\r
723      * Sets the UI for the edition of a new file.\r
724      * @param iFile the file being edited.\r
725      */\r
726     public void initWithFile(IFile file) {\r
727         mEditedFile = file;\r
728 \r
729         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);\r
730         mEditedConfig = resFolder.getConfiguration();\r
731 \r
732         mConfigComposite.updateThemesAndLocales();\r
733 \r
734         Sdk currentSdk = Sdk.getCurrent();\r
735         if (currentSdk != null) {\r
736             IAndroidTarget target = currentSdk.getTarget(file.getProject());\r
737             if (target != null) {\r
738                 mConfigComposite.initWith(mEditedConfig, target);\r
739             }\r
740         }\r
741 \r
742         if (mReloadListener == null) {\r
743             mReloadListener = new ReloadListener();\r
744             LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);\r
745         }\r
746 \r
747         if (mRulesEngine == null) {\r
748             mRulesEngine = new RulesEngine(mEditedFile.getProject());\r
749         }\r
750     }\r
751 \r
752     public void onTargetChange() {\r
753         onTargetOrSdkChange(false /* reloadDevices */);\r
754     }\r
755 \r
756     public void onSdkChange() {\r
757         onTargetOrSdkChange(true /* reloadDevices */);\r
758     }\r
759 \r
760     /**\r
761      * Reloads the configuration selector.\r
762      * @param reloadDevices whether the {@link LayoutDevice} objects should be reloaded.\r
763      */\r
764     private void onTargetOrSdkChange(boolean reloadDevices) {\r
765         Sdk currentSdk = Sdk.getCurrent();\r
766         if (currentSdk != null) {\r
767             IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
768             if (target != null) {\r
769                 mConfigComposite.resetUi(mEditedConfig, target, reloadDevices);\r
770                 mConfigListener.onConfigurationChange();\r
771             }\r
772         }\r
773     }\r
774 \r
775     public Clipboard getClipboard() {\r
776         return mClipboard;\r
777     }\r
778 \r
779     public LayoutEditor getLayoutEditor() {\r
780         return mLayoutEditor;\r
781     }\r
782 \r
783     public UiDocumentNode getModel() {\r
784         return mLayoutEditor.getUiRootNode();\r
785     }\r
786 \r
787     public SelectionSynchronizer getSelectionSynchronizer() {\r
788         // TODO Auto-generated method stub\r
789         return null;\r
790     }\r
791 \r
792     /**\r
793      * Callback for XML model changed. Only update/recompute the layout if the editor is visible\r
794      */\r
795     public void onXmlModelChanged() {\r
796         if (mLayoutEditor.isGraphicalEditorActive()) {\r
797             doXmlReload(true /* force */);\r
798             recomputeLayout();\r
799         } else {\r
800             mNeedsXmlReload = true;\r
801         }\r
802     }\r
803 \r
804     /**\r
805      * Actually performs the XML reload\r
806      * @see #onXmlModelChanged()\r
807      */\r
808     private void doXmlReload(boolean force) {\r
809         if (force || mNeedsXmlReload) {\r
810 \r
811             // TODO : update the mLayoutCanvas, preserving the current selection if possible.\r
812 \r
813 //            GraphicalViewer viewer = getGraphicalViewer();\r
814 //\r
815 //            // try to preserve the selection before changing the content\r
816 //            SelectionManager selMan = viewer.getSelectionManager();\r
817 //            ISelection selection = selMan.getSelection();\r
818 //\r
819 //            try {\r
820 //                viewer.setContents(getModel());\r
821 //            } finally {\r
822 //                selMan.setSelection(selection);\r
823 //            }\r
824 \r
825             mNeedsXmlReload = false;\r
826         }\r
827     }\r
828 \r
829     public void recomputeLayout() {\r
830         doXmlReload(false /* force */);\r
831         try {\r
832             // check that the resource exists. If the file is opened but the project is closed\r
833             // or deleted for some reason (changed from outside of eclipse), then this will\r
834             // return false;\r
835             if (mEditedFile.exists() == false) {\r
836                 displayError("Resource '%1$s' does not exist.",\r
837                              mEditedFile.getFullPath().toString());\r
838                 return;\r
839             }\r
840 \r
841             IProject iProject = mEditedFile.getProject();\r
842 \r
843             if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {\r
844                 String message = String.format("%1$s is out of sync. Please refresh.",\r
845                         mEditedFile.getName());\r
846 \r
847                 displayError(message);\r
848 \r
849                 // also print it in the error console.\r
850                 AdtPlugin.printErrorToConsole(iProject.getName(), message);\r
851                 return;\r
852             }\r
853 \r
854             Sdk currentSdk = Sdk.getCurrent();\r
855             if (currentSdk != null) {\r
856                 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());\r
857                 if (target == null) {\r
858                     displayError("The project target is not set.");\r
859                     return;\r
860                 }\r
861 \r
862                 AndroidTargetData data = currentSdk.getTargetData(target);\r
863                 if (data == null) {\r
864                     // It can happen that the workspace refreshes while the SDK is loading its\r
865                     // data, which could trigger a redraw of the opened layout if some resources\r
866                     // changed while Eclipse is closed.\r
867                     // In this case data could be null, but this is not an error.\r
868                     // We can just silently return, as all the opened editors are automatically\r
869                     // refreshed once the SDK finishes loading.\r
870                     LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);\r
871                     switch (targetLoadStatus) {\r
872                         case LOADING:\r
873                             displayError("The project target (%1$s) is still loading.\n%2$s will refresh automatically once the process is finished.",\r
874                                     target.getName(), mEditedFile.getName());\r
875 \r
876                             break;\r
877                         case FAILED: // known failure\r
878                         case LOADED: // success but data isn't loaded?!?!\r
879                             displayError("The project target (%s) was not properly loaded.",\r
880                                     target.getName());\r
881                             break;\r
882                     }\r
883 \r
884                     return;\r
885                 }\r
886 \r
887                 // check there is actually a model (maybe the file is empty).\r
888                 UiDocumentNode model = getModel();\r
889 \r
890                 if (model.getUiChildren().size() == 0) {\r
891                     displayError("No Xml content. Go to the Outline view and add nodes.");\r
892                     return;\r
893                 }\r
894 \r
895                 LayoutBridge bridge = data.getLayoutBridge();\r
896 \r
897                 if (bridge.bridge != null) { // bridge can never be null.\r
898                     ResourceManager resManager = ResourceManager.getInstance();\r
899 \r
900                     ProjectResources projectRes = resManager.getProjectResources(iProject);\r
901                     if (projectRes == null) {\r
902                         displayError("Missing project resources.");\r
903                         return;\r
904                     }\r
905 \r
906                     // get the resources of the file's project.\r
907                     Map<String, Map<String, IResourceValue>> configuredProjectRes =\r
908                         mConfigListener.getConfiguredProjectResources();\r
909 \r
910                     // get the framework resources\r
911                     Map<String, Map<String, IResourceValue>> frameworkResources =\r
912                         mConfigListener.getConfiguredFrameworkResources();\r
913 \r
914                     if (configuredProjectRes != null && frameworkResources != null) {\r
915                         if (mProjectCallback == null) {\r
916                             mProjectCallback = new ProjectCallback(\r
917                                     bridge.classLoader, projectRes, iProject);\r
918                         }\r
919 \r
920                         if (mLogger == null) {\r
921                             mLogger = new ILayoutLog() {\r
922                                 public void error(String message) {\r
923                                     AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);\r
924                                 }\r
925 \r
926                                 public void error(Throwable error) {\r
927                                     String message = error.getMessage();\r
928                                     if (message == null) {\r
929                                         message = error.getClass().getName();\r
930                                     }\r
931 \r
932                                     PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());\r
933                                     error.printStackTrace(ps);\r
934                                 }\r
935 \r
936                                 public void warning(String message) {\r
937                                     AdtPlugin.printToConsole(mEditedFile.getName(), message);\r
938                                 }\r
939                             };\r
940                         }\r
941 \r
942                         // get the selected theme\r
943                         String theme = mConfigComposite.getTheme();\r
944                         if (theme != null) {\r
945                             // Compute the layout\r
946                             Rectangle rect = getBounds();\r
947 \r
948                             int width = rect.width;\r
949                             int height = rect.height;\r
950                             if (mUseExplodeMode) {\r
951                                 // compute how many padding in x and y will bump the screen size\r
952                                 ExplodedRenderingHelper helper = new ExplodedRenderingHelper(\r
953                                         getModel(), iProject);\r
954 \r
955                                 // there are 2 paddings for each view\r
956                                 // left and right, or top and bottom.\r
957                                 int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;\r
958 \r
959                                 width += helper.getWidthPadding() * paddingValue;\r
960                                 height += helper.getHeightPadding() * paddingValue;\r
961                             }\r
962 \r
963                             int density = mConfigComposite.getDensity().getDpiValue();\r
964                             float xdpi = mConfigComposite.getXDpi();\r
965                             float ydpi = mConfigComposite.getYDpi();\r
966                             boolean isProjectTheme = mConfigComposite.isProjectTheme();\r
967 \r
968                             UiElementPullParser parser = new UiElementPullParser(getModel(),\r
969                                     mUseExplodeMode, density, xdpi, iProject);\r
970 \r
971                             ILayoutResult result = computeLayout(bridge, parser,\r
972                                     iProject /* projectKey */,\r
973                                     width, height, !mConfigComposite.getClipping(),\r
974                                     density, xdpi, ydpi,\r
975                                     theme, isProjectTheme,\r
976                                     configuredProjectRes, frameworkResources, mProjectCallback,\r
977                                     mLogger);\r
978 \r
979                             mLayoutCanvas.setResult(result);\r
980 \r
981                             // update the UiElementNode with the layout info.\r
982                             if (result.getSuccess() == ILayoutResult.SUCCESS) {\r
983                                 hideError();\r
984                             } else {\r
985                                 displayError(result.getErrorMessage());\r
986                             }\r
987 \r
988                             model.refreshUi();\r
989                         }\r
990                     }\r
991                 } else {\r
992                     // SDK is loaded but not the layout library!\r
993 \r
994                     // check whether the bridge managed to load, or not\r
995                     if (bridge.status == LoadStatus.LOADING) {\r
996                         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
997                                      mEditedFile.getName());\r
998                     } else {\r
999                         displayError("Eclipse failed to load the framework information and the layout library!");\r
1000                     }\r
1001                 }\r
1002             } else {\r
1003                 displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",\r
1004                              mEditedFile.getName());\r
1005             }\r
1006         } finally {\r
1007             // no matter the result, we are done doing the recompute based on the latest\r
1008             // resource/code change.\r
1009             mNeedsRecompute = false;\r
1010         }\r
1011     }\r
1012 \r
1013     /**\r
1014      * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on\r
1015      * the implementation API level.\r
1016      *\r
1017      * Implementation detail: the bridge's computeLayout() method already returns a newly\r
1018      * allocated ILayoutResult.\r
1019      */\r
1020     @SuppressWarnings("deprecation")\r
1021     private static ILayoutResult computeLayout(LayoutBridge bridge,\r
1022             IXmlPullParser layoutDescription, Object projectKey,\r
1023             int screenWidth, int screenHeight, boolean renderFullSize,\r
1024             int density, float xdpi, float ydpi,\r
1025             String themeName, boolean isProjectTheme,\r
1026             Map<String, Map<String, IResourceValue>> projectResources,\r
1027             Map<String, Map<String, IResourceValue>> frameworkResources,\r
1028             IProjectCallback projectCallback, ILayoutLog logger) {\r
1029 \r
1030         if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) {\r
1031             // newest API with support for "render full height"\r
1032             // TODO: link boolean to UI.\r
1033             return bridge.bridge.computeLayout(layoutDescription,\r
1034                     projectKey, screenWidth, screenHeight, renderFullSize,\r
1035                     density, xdpi, ydpi,\r
1036                     themeName, isProjectTheme,\r
1037                     projectResources, frameworkResources, projectCallback,\r
1038                     logger);\r
1039         } else if (bridge.apiLevel == 3) {\r
1040             // newer api with density support.\r
1041             return bridge.bridge.computeLayout(layoutDescription,\r
1042                     projectKey, screenWidth, screenHeight, density, xdpi, ydpi,\r
1043                     themeName, isProjectTheme,\r
1044                     projectResources, frameworkResources, projectCallback,\r
1045                     logger);\r
1046         } else if (bridge.apiLevel == 2) {\r
1047             // api with boolean for separation of project/framework theme\r
1048             return bridge.bridge.computeLayout(layoutDescription,\r
1049                     projectKey, screenWidth, screenHeight, themeName, isProjectTheme,\r
1050                     projectResources, frameworkResources, projectCallback,\r
1051                     logger);\r
1052         } else {\r
1053             // oldest api with no density/dpi, and project theme boolean mixed\r
1054             // into the theme name.\r
1055 \r
1056             // change the string if it's a custom theme to make sure we can\r
1057             // differentiate them\r
1058             if (isProjectTheme) {\r
1059                 themeName = "*" + themeName; //$NON-NLS-1$\r
1060             }\r
1061 \r
1062             return bridge.bridge.computeLayout(layoutDescription,\r
1063                     projectKey, screenWidth, screenHeight, themeName,\r
1064                     projectResources, frameworkResources, projectCallback,\r
1065                     logger);\r
1066         }\r
1067     }\r
1068 \r
1069     public Rectangle getBounds() {\r
1070         return mConfigComposite.getScreenBounds();\r
1071     }\r
1072 \r
1073     public void reloadPalette() {\r
1074         if (mPalette != null) {\r
1075             mPalette.reloadPalette(mLayoutEditor.getTargetData());\r
1076         }\r
1077     }\r
1078 \r
1079     /**\r
1080      * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node\r
1081      * created by {@link ElementCreateCommand#execute()}.\r
1082      *\r
1083      * @param uiNodeModel The {@link UiElementNode} to select.\r
1084      */\r
1085     public void selectModel(UiElementNode uiNodeModel) {\r
1086 \r
1087         // TODO this method was useful for GLE1. We may not need it anymore now.\r
1088 \r
1089 //        GraphicalViewer viewer = getGraphicalViewer();\r
1090 //\r
1091 //        // Give focus to the graphical viewer (in case the outline has it)\r
1092 //        viewer.getControl().forceFocus();\r
1093 //\r
1094 //        Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);\r
1095 //\r
1096 //        if (editPart instanceof EditPart) {\r
1097 //            viewer.select((EditPart)editPart);\r
1098 //        }\r
1099     }\r
1100 \r
1101     private class ReloadListener implements ILayoutReloadListener {\r
1102         /*\r
1103          * (non-Javadoc)\r
1104          * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean)\r
1105          *\r
1106          * Called when the file changes triggered a redraw of the layout\r
1107          */\r
1108         public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) {\r
1109             boolean recompute = rChange;\r
1110 \r
1111             if (resChange) {\r
1112                 recompute = true;\r
1113 \r
1114                 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.\r
1115 \r
1116                 // force a reparse in case a value XML file changed.\r
1117                 mConfiguredProjectRes = null;\r
1118 \r
1119                 // clear the cache in the bridge in case a bitmap/9-patch changed.\r
1120                 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());\r
1121                 if (target != null) {\r
1122 \r
1123                     AndroidTargetData data = Sdk.getCurrent().getTargetData(target);\r
1124                     if (data != null) {\r
1125                         LayoutBridge bridge = data.getLayoutBridge();\r
1126 \r
1127                         if (bridge.bridge != null) {\r
1128                             bridge.bridge.clearCaches(mEditedFile.getProject());\r
1129                         }\r
1130                     }\r
1131                 }\r
1132 \r
1133                 mLayoutCanvas.getDisplay().asyncExec(new Runnable() {\r
1134                     public void run() {\r
1135                         mConfigComposite.updateThemesAndLocales();\r
1136                     }\r
1137                 });\r
1138             }\r
1139 \r
1140             if (codeChange) {\r
1141                 // only recompute if the custom view loader was used to load some code.\r
1142                 if (mProjectCallback != null && mProjectCallback.isUsed()) {\r
1143                     mProjectCallback = null;\r
1144                     recompute = true;\r
1145                 }\r
1146             }\r
1147 \r
1148             if (recompute) {\r
1149                 mLayoutCanvas.getDisplay().asyncExec(new Runnable() {\r
1150                     public void run() {\r
1151                         if (mLayoutEditor.isGraphicalEditorActive()) {\r
1152                             recomputeLayout();\r
1153                         } else {\r
1154                             mNeedsRecompute = true;\r
1155                         }\r
1156                     }\r
1157                 });\r
1158             }\r
1159         }\r
1160     }\r
1161 }\r