OSDN Git Service

f941cebff1b58fecc5dca8f34fc0e778acc46703
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / editors / layout / configuration / ConfigurationComposite.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
18
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME_PREFIX;
20 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_STYLE;
21
22 import com.android.ide.common.rendering.api.ResourceValue;
23 import com.android.ide.common.rendering.api.StyleResourceValue;
24 import com.android.ide.common.resources.ResourceFile;
25 import com.android.ide.common.resources.ResourceFolder;
26 import com.android.ide.common.resources.ResourceRepository;
27 import com.android.ide.common.resources.configuration.DockModeQualifier;
28 import com.android.ide.common.resources.configuration.FolderConfiguration;
29 import com.android.ide.common.resources.configuration.LanguageQualifier;
30 import com.android.ide.common.resources.configuration.NightModeQualifier;
31 import com.android.ide.common.resources.configuration.PixelDensityQualifier;
32 import com.android.ide.common.resources.configuration.RegionQualifier;
33 import com.android.ide.common.resources.configuration.ResourceQualifier;
34 import com.android.ide.common.resources.configuration.ScreenDimensionQualifier;
35 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
36 import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
37 import com.android.ide.common.resources.configuration.VersionQualifier;
38 import com.android.ide.common.sdk.LoadStatus;
39 import com.android.ide.eclipse.adt.AdtPlugin;
40 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
41 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
42 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
43 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
44 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
45 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
46 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
47 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
48 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
49 import com.android.resources.Density;
50 import com.android.resources.DockMode;
51 import com.android.resources.NightMode;
52 import com.android.resources.ResourceFolderType;
53 import com.android.resources.ResourceType;
54 import com.android.resources.ScreenOrientation;
55 import com.android.resources.ScreenSize;
56 import com.android.sdklib.AndroidVersion;
57 import com.android.sdklib.IAndroidTarget;
58 import com.android.util.Pair;
59
60 import org.eclipse.core.resources.IFile;
61 import org.eclipse.core.resources.IFolder;
62 import org.eclipse.core.resources.IProject;
63 import org.eclipse.core.runtime.CoreException;
64 import org.eclipse.core.runtime.IStatus;
65 import org.eclipse.core.runtime.QualifiedName;
66 import org.eclipse.draw2d.geometry.Rectangle;
67 import org.eclipse.swt.SWT;
68 import org.eclipse.swt.events.SelectionAdapter;
69 import org.eclipse.swt.events.SelectionEvent;
70 import org.eclipse.swt.layout.GridData;
71 import org.eclipse.swt.layout.GridLayout;
72 import org.eclipse.swt.widgets.Button;
73 import org.eclipse.swt.widgets.Combo;
74 import org.eclipse.swt.widgets.Composite;
75 import org.eclipse.swt.widgets.Label;
76
77 import java.util.ArrayList;
78 import java.util.Collections;
79 import java.util.Comparator;
80 import java.util.HashSet;
81 import java.util.List;
82 import java.util.Locale;
83 import java.util.Map;
84 import java.util.Set;
85 import java.util.SortedSet;
86
87 /**
88  * A composite that displays the current configuration displayed in a Graphical Layout Editor.
89  * <p/>
90  * The composite has several entry points:<br>
91  * - {@link #setFile(IFile)}<br>
92  *   Called after the constructor to set the file being edited. Nothing else is performed.<br>
93  *<br>
94  * - {@link #onXmlModelLoaded()}<br>
95  *   Called when the XML model is loaded, either the first time or when the Target/SDK changes.
96  *   This initializes the UI, either with the first compatible configuration found, or attempts
97  *   to restore a configuration if one is found to have been saved in the file persistent storage.
98  *   (see {@link #storeState()})<br>
99  *<br>
100  * - {@link #replaceFile(IFile)}<br>
101  *   Called when a file, representing the same resource but with a different config is opened<br>
102  *   by the user.<br>
103  *<br>
104  * - {@link #changeFileOnNewConfig(IFile)}<br>
105  *   Called when config change triggers the editing of a file with a different config.
106  *<p/>
107  * Additionally, the composite can handle the following events.<br>
108  * - SDK reload. This is when the main SDK is finished loading.<br>
109  * - Target reload. This is when the target used by the project is the edited file has finished<br>
110  *   loading.<br>
111  */
112 public class ConfigurationComposite extends Composite {
113     private final static String SEP = ":"; //$NON-NLS-1$
114     private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
115
116     /**
117      * Setting name for project-wide setting controlling rendering target and locale which
118      * is shared for all files
119      */
120     public final static QualifiedName NAME_RENDER_STATE =
121         new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$
122
123     /**
124      * Settings name for file-specific configuration preferences, such as which theme or
125      * device to render the current layout with
126      */
127     public final static QualifiedName NAME_CONFIG_STATE =
128         new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
129
130     private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
131
132     private final static int LOCALE_LANG = 0;
133     private final static int LOCALE_REGION = 1;
134
135     private Label mCurrentLayoutLabel;
136     private Button mCreateButton;
137
138     private Combo mDeviceCombo;
139     private Combo mDeviceConfigCombo;
140     private Combo mLocaleCombo;
141     private Combo mDockCombo;
142     private Combo mNightCombo;
143     private Combo mThemeCombo;
144     private Combo mTargetCombo;
145
146     /**
147      * List of booleans, matching item for item the theme names in the mThemeCombo
148      * combobox, where each boolean represents whether the corresponding theme is a
149      * project theme
150      */
151     private List<Boolean> mIsProjectTheme = new ArrayList<Boolean>(40);
152
153     /** updates are disabled if > 0 */
154     private int mDisableUpdates = 0;
155
156     private List<LayoutDevice> mDeviceList;
157     private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
158
159     private final ArrayList<ResourceQualifier[] > mLocaleList =
160         new ArrayList<ResourceQualifier[]>();
161
162     private final ConfigState mState = new ConfigState();
163
164     private boolean mSdkChanged = false;
165     private boolean mFirstXmlModelChange = true;
166
167     /** The config listener given to the constructor. Never null. */
168     private final IConfigListener mListener;
169
170     /** The {@link FolderConfiguration} representing the state of the UI controls */
171     private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
172
173     /** The file being edited */
174     private IFile mEditedFile;
175     /** The {@link ProjectResources} for the edited file's project */
176     private ProjectResources mResources;
177     /** The target of the project of the file being edited. */
178     private IAndroidTarget mProjectTarget;
179     /** The target of the project of the file being edited. */
180     private IAndroidTarget mRenderingTarget;
181     /** The {@link FolderConfiguration} being edited. */
182     private FolderConfiguration mEditedConfig;
183     /** Serialized state to use when initializing the configuration after the SDK is loaded */
184     private String mInitialState;
185
186     /**
187      * Interface implemented by the part which owns a {@link ConfigurationComposite}.
188      * This notifies the owners when the configuration change.
189      * The owner must also provide methods to provide the configuration that will
190      * be displayed.
191      */
192     public interface IConfigListener {
193         /**
194          * Called when the {@link FolderConfiguration} change. The new config can be queried
195          * with {@link ConfigurationComposite#getCurrentConfig()}.
196          */
197         void onConfigurationChange();
198
199         /**
200          * Called after a device has changed (in addition to {@link #onConfigurationChange}
201          * getting called)
202          */
203         void onDevicePostChange();
204
205         /**
206          * Called when the current theme changes. The theme can be queried with
207          * {@link ConfigurationComposite#getTheme()}.
208          */
209         void onThemeChange();
210
211         /**
212          * Called when the "Create" button is clicked.
213          */
214         void onCreate();
215
216         /**
217          * Called before the rendering target changes.
218          * @param oldTarget the old rendering target
219          */
220         void onRenderingTargetPreChange(IAndroidTarget oldTarget);
221
222         /**
223          * Called after the rendering target changes.
224          *
225          * @param target the new rendering target
226          */
227         void onRenderingTargetPostChange(IAndroidTarget target);
228
229         ResourceRepository getProjectResources();
230         ResourceRepository getFrameworkResources();
231         ResourceRepository getFrameworkResources(IAndroidTarget target);
232         Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
233         Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
234         String getIncludedWithin();
235     }
236
237     /**
238      * State of the current config. This is used during UI reset to attempt to return the
239      * rendering to its original configuration.
240      */
241     private class ConfigState {
242         LayoutDevice device;
243         String configName;
244         ResourceQualifier[] locale;
245         String theme;
246         /** dock mode. Guaranteed to be non null */
247         DockMode dock = DockMode.NONE;
248         /** night mode. Guaranteed to be non null */
249         NightMode night = NightMode.NOTNIGHT;
250         /** the version being targeted for rendering */
251         IAndroidTarget target;
252
253         String getData() {
254             StringBuilder sb = new StringBuilder();
255             if (device != null) {
256                 sb.append(device.getName());
257                 sb.append(SEP);
258                 sb.append(configName);
259                 sb.append(SEP);
260                 if (isLocaleSpecificLayout() && locale != null) {
261                     if (locale[0] != null && locale[1] != null) {
262                         // locale[0]/[1] can be null sometimes when starting Eclipse
263                         sb.append(((LanguageQualifier) locale[0]).getValue());
264                         sb.append(SEP_LOCALE);
265                         sb.append(((RegionQualifier) locale[1]).getValue());
266                     }
267                 }
268                 sb.append(SEP);
269                 sb.append(theme);
270                 sb.append(SEP);
271                 sb.append(dock.getResourceValue());
272                 sb.append(SEP);
273                 sb.append(night.getResourceValue());
274                 sb.append(SEP);
275
276                 // We used to store the render target here in R9. Leave a marker
277                 // to ensure that we don't reuse this slot; add new extra fields after it.
278                 sb.append(SEP);
279             }
280
281             return sb.toString();
282         }
283
284         boolean setData(String data) {
285             String[] values = data.split(SEP);
286             if (values.length == 6 || values.length == 7) {
287                 for (LayoutDevice d : mDeviceList) {
288                     if (d.getName().equals(values[0])) {
289                         device = d;
290                         FolderConfiguration config = device.getFolderConfigByName(values[1]);
291                         if (config != null) {
292                             configName = values[1];
293
294                             // Load locale. Note that this can get overwritten by the
295                             // project-wide settings read below.
296                             locale = new ResourceQualifier[2];
297                             String locales[] = values[2].split(SEP_LOCALE);
298                             if (locales.length >= 2) {
299                                 if (locales[0].length() > 0) {
300                                     locale[0] = new LanguageQualifier(locales[0]);
301                                 }
302                                 if (locales[1].length() > 0) {
303                                     locale[1] = new RegionQualifier(locales[1]);
304                                 }
305                             }
306
307                             theme = values[3];
308                             dock = DockMode.getEnum(values[4]);
309                             if (dock == null) {
310                                 dock = DockMode.NONE;
311                             }
312                             night = NightMode.getEnum(values[5]);
313                             if (night == null) {
314                                 night = NightMode.NOTNIGHT;
315                             }
316
317                             // element 7/values[6]: used to store render target in R9.
318                             // No longer stored here. If adding more data, make
319                             // sure you leave 7 alone.
320
321                             Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
322
323                             // We only use the "global" setting
324                             if (!isLocaleSpecificLayout()) {
325                                 locale = pair.getFirst();
326                             }
327                             target = pair.getSecond();
328
329                             return true;
330                         }
331                     }
332                 }
333             }
334
335             return false;
336         }
337
338         @Override
339         public String toString() {
340             return getData();
341         }
342     }
343
344     /**
345      * Returns a String id to represent an {@link IAndroidTarget} which can be translated
346      * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id
347      * will never contain the {@link #SEP} character.
348      *
349      * @param target the target to return an id for
350      * @return an id for the given target; never null
351      */
352     private String targetToString(IAndroidTarget target) {
353         return target.getFullName().replace(SEP, "");  //$NON-NLS-1$
354     }
355
356     /**
357      * Returns an {@link IAndroidTarget} that corresponds to the given id that was
358      * originally returned by {@link #targetToString}. May be null, if the platform is no
359      * longer available, or if the platform list has not yet been initialized.
360      *
361      * @param id the id that corresponds to the desired platform
362      * @return an {@link IAndroidTarget} that matches the given id, or null
363      */
364     private IAndroidTarget stringToTarget(String id) {
365         if (mTargetList != null && mTargetList.size() > 0) {
366             for (IAndroidTarget target : mTargetList) {
367                 if (id.equals(targetToString(target))) {
368                     return target;
369                 }
370             }
371         }
372
373         return null;
374     }
375
376     /**
377      * Creates a new {@link ConfigurationComposite} and adds it to the parent.
378      *
379      * The method also receives custom buttons to set into the configuration composite. The list
380      * is organized as an array of arrays. Each array represents a group of buttons thematically
381      * grouped together.
382      *
383      * @param listener An {@link IConfigListener} that gets and sets configuration properties.
384      *          Mandatory, cannot be null.
385      * @param parent The parent composite.
386      * @param style The style of this composite.
387      * @param initialState The initial state (serialized form) to use for the configuration
388      */
389     public ConfigurationComposite(IConfigListener listener,
390             Composite parent, int style, String initialState) {
391         super(parent, style);
392         mListener = listener;
393         mInitialState = initialState;
394
395         GridLayout gl;
396         GridData gd;
397         int cols = 7;  // device+config+dock+day+separator*2+theme
398
399         // ---- First line: editing config display, locale, theme, create-button
400         Composite labelParent = new Composite(this, SWT.NONE);
401         labelParent.setLayout(gl = new GridLayout(5, false));
402         gl.marginWidth = gl.marginHeight = 0;
403         gl.marginTop = 3;
404         labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
405         gd.horizontalSpan = cols;
406
407         new Label(labelParent, SWT.NONE).setText("Editing config:");
408         mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
409         mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
410         gd.widthHint = 50;
411
412         mLocaleCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
413         mLocaleCombo.addSelectionListener(new SelectionAdapter() {
414             @Override
415             public void widgetSelected(SelectionEvent e) {
416                 onLocaleChange();
417             }
418         });
419
420         // Layout bug workaround. Without this, in -some- scenarios the Locale combo box was
421         // coming up tiny. Setting a minimumWidth hint does not work either. We need to have
422         // 2 or more items in the locale combo box when the layout is first run. These items
423         // are removed as part of the locale initialization when the SDK is loaded.
424         mLocaleCombo.add("Locale"); //$NON-NLS-1$  // Dummy place holders
425         mLocaleCombo.add("Locale"); //$NON-NLS-1$
426
427         mTargetCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
428         mTargetCombo.add("Android AOSP"); //$NON-NLS-1$  // Dummy place holders
429         mTargetCombo.add("Android AOSP"); //$NON-NLS-1$
430         mTargetCombo.addSelectionListener(new SelectionAdapter() {
431             @Override
432             public void widgetSelected(SelectionEvent e) {
433                 onRenderingTargetChange();
434             }
435         });
436
437         mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT);
438         mCreateButton.setText("Create...");
439         mCreateButton.setEnabled(false);
440         mCreateButton.addSelectionListener(new SelectionAdapter() {
441             @Override
442             public void widgetSelected(SelectionEvent e) {
443                 if (mListener != null) {
444                     mListener.onCreate();
445                 }
446             }
447         });
448
449         // ---- 2nd line: device/config/locale/theme Combos, create button.
450
451         setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
452         setLayout(gl = new GridLayout(cols, false));
453         gl.marginHeight = 0;
454         gl.horizontalSpacing = 0;
455
456         mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
457         mDeviceCombo.setLayoutData(new GridData(
458                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
459         mDeviceCombo.addSelectionListener(new SelectionAdapter() {
460             @Override
461             public void widgetSelected(SelectionEvent e) {
462                 onDeviceChange(true /* recomputeLayout*/);
463             }
464         });
465
466         mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
467         mDeviceConfigCombo.setLayoutData(new GridData(
468                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
469         mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
470             @Override
471              public void widgetSelected(SelectionEvent e) {
472                 onDeviceConfigChange();
473             }
474         });
475
476         // first separator
477         Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
478         separator.setLayoutData(gd = new GridData(
479                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
480         gd.heightHint = 0;
481
482         mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
483         mDockCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
484                 | GridData.GRAB_HORIZONTAL));
485         for (DockMode mode : DockMode.values()) {
486             mDockCombo.add(mode.getLongDisplayValue());
487         }
488         mDockCombo.addSelectionListener(new SelectionAdapter() {
489             @Override
490             public void widgetSelected(SelectionEvent e) {
491                 onDockChange();
492             }
493         });
494
495         mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
496         mNightCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
497                 | GridData.GRAB_HORIZONTAL));
498         for (NightMode mode : NightMode.values()) {
499             mNightCombo.add(mode.getLongDisplayValue());
500         }
501         mNightCombo.addSelectionListener(new SelectionAdapter() {
502             @Override
503             public void widgetSelected(SelectionEvent e) {
504                 onDayChange();
505             }
506         });
507
508         mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
509         mThemeCombo.setLayoutData(new GridData(
510                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
511         mThemeCombo.setEnabled(false);
512
513         mThemeCombo.addSelectionListener(new SelectionAdapter() {
514             @Override
515             public void widgetSelected(SelectionEvent e) {
516                 onThemeChange();
517             }
518         });
519     }
520
521     // ---- Init and reset/reload methods ----
522
523     /**
524      * Sets the reference to the file being edited.
525      * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
526      * loaded (or reloaded as the SDK/target changes).
527      *
528      * @param file the file being opened
529      *
530      * @see #onXmlModelLoaded()
531      * @see #replaceFile(IFile)
532      * @see #changeFileOnNewConfig(IFile)
533      */
534     public void setFile(IFile file) {
535         mEditedFile = file;
536     }
537
538     /**
539      * Replaces the UI with a given file configuration. This is meant to answer the user
540      * explicitly opening a different version of the same layout from the Package Explorer.
541      * <p/>This attempts to keep the current config, but may change it if it's not compatible or
542      * not the best match
543      * <p/>This will NOT trigger a redraw event (will not call
544      * {@link IConfigListener#onConfigurationChange()}.)
545      * @param file the file being opened.
546      */
547     public void replaceFile(IFile file) {
548         // if there is no previous selection, revert to default mode.
549         if (mState.device == null) {
550             setFile(file); // onTargetChanged will be called later.
551             return;
552         }
553
554         mEditedFile = file;
555         IProject iProject = mEditedFile.getProject();
556         mResources = ResourceManager.getInstance().getProjectResources(iProject);
557
558         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
559         mEditedConfig = resFolder.getConfiguration();
560
561         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
562                            // new values in the widgets.
563
564         try {
565             // only attempt to do anything if the SDK and targets are loaded.
566             LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
567             if (sdkStatus == LoadStatus.LOADED) {
568                 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
569                         null /*project*/);
570
571                 if (targetStatus == LoadStatus.LOADED) {
572
573                     // update the current config selection to make sure it's
574                     // compatible with the new file
575                     adaptConfigSelection(true /*needBestMatch*/);
576
577                     // compute the final current config
578                     computeCurrentConfig();
579
580                     // update the string showing the config value
581                     updateConfigDisplay(mEditedConfig);
582                 }
583             }
584         } finally {
585             mDisableUpdates--;
586         }
587     }
588
589     /**
590      * Updates the UI with a new file that was opened in response to a config change.
591      * @param file the file being opened.
592      *
593      * @see #replaceFile(IFile)
594      */
595     public void changeFileOnNewConfig(IFile file) {
596         mEditedFile = file;
597         IProject iProject = mEditedFile.getProject();
598         mResources = ResourceManager.getInstance().getProjectResources(iProject);
599
600         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
601         mEditedConfig = resFolder.getConfiguration();
602
603         // All that's needed is to update the string showing the config value
604         // (since the config combo were chosen by the user).
605         updateConfigDisplay(mEditedConfig);
606     }
607
608     /**
609      * Responds to the event that the basic SDK information finished loading.
610      * @param target the possibly new target object associated with the file being edited (in case
611      * the SDK path was changed).
612      */
613     public void onSdkLoaded(IAndroidTarget target) {
614         // a change to the SDK means that we need to check for new/removed devices.
615         mSdkChanged = true;
616
617         // store the new target.
618         mProjectTarget = target;
619
620         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
621                            // new values in the widgets.
622         try {
623             // this is going to be followed by a call to onTargetLoaded.
624             // So we can only care about the layout devices in this case.
625             initDevices();
626             initTargets();
627         } finally {
628             mDisableUpdates--;
629         }
630     }
631
632     /**
633      * Answers to the XML model being loaded, either the first time or when the Target/SDK changes.
634      * <p>This initializes the UI, either with the first compatible configuration found,
635      * or attempts to restore a configuration if one is found to have been saved in the file
636      * persistent storage.
637      * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called
638      * back when those are loaded).
639      * <p>The method automatically handles being called the first time after editor creation, or
640      * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
641      * is properly called).
642      *
643      * @see #storeState()
644      * @see #onSdkLoaded(IAndroidTarget)
645      */
646     public AndroidTargetData onXmlModelLoaded() {
647         AndroidTargetData targetData = null;
648
649         // only attempt to do anything if the SDK and targets are loaded.
650         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
651         if (sdkStatus == LoadStatus.LOADED) {
652             mDisableUpdates++; // we do not want to trigger onXXXChange when setting
653
654             try {
655                 // init the devices if needed (new SDK or first time going through here)
656                 if (mSdkChanged || mFirstXmlModelChange) {
657                     initDevices();
658                     initTargets();
659                 }
660
661                 IProject iProject = mEditedFile.getProject();
662
663                 Sdk currentSdk = Sdk.getCurrent();
664                 if (currentSdk != null) {
665                     mProjectTarget = currentSdk.getTarget(iProject);
666                 }
667
668                 LoadStatus targetStatus = LoadStatus.FAILED;
669                 if (mProjectTarget != null) {
670                     targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
671                     initTargets();
672                 }
673
674                 if (targetStatus == LoadStatus.LOADED) {
675                     if (mResources == null) {
676                         mResources = ResourceManager.getInstance().getProjectResources(iProject);
677                     }
678                     if (mEditedConfig == null) {
679                         ResourceFolder resFolder = mResources.getResourceFolder(
680                                 (IFolder) mEditedFile.getParent());
681                         mEditedConfig = resFolder.getConfiguration();
682                     }
683
684                     targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
685
686                     // get the file stored state
687                     boolean loadedConfigData = false;
688                     String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE);
689                     if (mInitialState != null) {
690                         data = mInitialState;
691                         mInitialState = null;
692                     }
693                     if (data != null) {
694                         loadedConfigData = mState.setData(data);
695                     }
696
697                     updateLocales();
698
699                     // If the current state was loaded from the persistent storage, we update the
700                     // UI with it and then try to adapt it (which will handle incompatible
701                     // configuration).
702                     // Otherwise, just look for the first compatible configuration.
703                     if (loadedConfigData) {
704                         // first make sure we have the config to adapt
705                         selectDevice(mState.device);
706                         fillConfigCombo(mState.configName);
707
708                         adaptConfigSelection(false /*needBestMatch*/);
709
710                         mDockCombo.select(DockMode.getIndex(mState.dock));
711                         mNightCombo.select(NightMode.getIndex(mState.night));
712                         mTargetCombo.select(mTargetList.indexOf(mState.target));
713
714                         targetData = Sdk.getCurrent().getTargetData(mState.target);
715                     } else {
716                         findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
717
718                         // Default to modern layout lib
719                         IAndroidTarget target = findDefaultRenderTarget();
720                         if (target != null) {
721                             targetData = Sdk.getCurrent().getTargetData(target);
722                             mTargetCombo.select(mTargetList.indexOf(target));
723                         }
724                     }
725
726                     // Update themes. This is done after updating the devices above,
727                     // since we want to look at the chosen device size to decide
728                     // what the default theme (for example, with Honeycomb we choose
729                     // Holo as the default theme but only if the screen size is XLARGE
730                     // (and of course only if the manifest does not specify another
731                     // default theme).
732                     updateThemes();
733
734                     // update the string showing the config value
735                     updateConfigDisplay(mEditedConfig);
736
737                     // compute the final current config
738                     computeCurrentConfig();
739                 }
740             } finally {
741                 mDisableUpdates--;
742                 mFirstXmlModelChange = false;
743             }
744         }
745
746         return targetData;
747     }
748
749     /** Return the default render target to use, or null if no strong preference */
750     private IAndroidTarget findDefaultRenderTarget() {
751         // Default to layoutlib version 5
752         Sdk current = Sdk.getCurrent();
753         if (current != null) {
754             for (IAndroidTarget target : current.getTargets()) {
755                 // Only Honeycomb has layoutlib version 5; as soon as we backport
756                 // adjust this algorithm to find the lowest version that contains
757                 // layoutlib 5
758                 AndroidVersion version = target.getVersion();
759                 int apiLevel = version.getApiLevel();
760                 if (apiLevel >= 11) { // Layoutlib so far has been backported to 11
761                     return target;
762                 }
763             }
764         }
765
766         return null;
767     }
768
769     private static class ConfigBundle {
770         FolderConfiguration config;
771         int localeIndex;
772         int dockModeIndex;
773         int nightModeIndex;
774
775         ConfigBundle() {
776             config = new FolderConfiguration();
777             localeIndex = 0;
778             dockModeIndex = 0;
779             nightModeIndex = 0;
780         }
781
782         ConfigBundle(ConfigBundle bundle) {
783             config = new FolderConfiguration();
784             config.set(bundle.config);
785             localeIndex = bundle.localeIndex;
786             dockModeIndex = bundle.dockModeIndex;
787             nightModeIndex = bundle.nightModeIndex;
788         }
789     }
790
791     private static class ConfigMatch {
792         final FolderConfiguration testConfig;
793         final LayoutDevice device;
794         final String name;
795         final ConfigBundle bundle;
796
797         public ConfigMatch(FolderConfiguration testConfig,
798                 LayoutDevice device, String name, ConfigBundle bundle) {
799             this.testConfig = testConfig;
800             this.device = device;
801             this.name = name;
802             this.bundle = bundle;
803         }
804
805         @Override
806         public String toString() {
807             return device.getName() + " - " + name;
808         }
809     }
810
811     /**
812      * Finds a device/config that can display {@link #mEditedConfig}.
813      * <p/>Once found the device and config combos are set to the config.
814      * <p/>If there is no compatible configuration, a custom one is created.
815      * @param favorCurrentConfig if true, and no best match is found, don't change
816      * the current config. This must only be true if the current config is compatible.
817      */
818     private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
819         // list of compatible device/config/locale
820         List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
821
822         // list of actual best match (ie the file is a best match for the device/config)
823         List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
824
825         // get a locale that match the host locale roughly (may not be exact match on the region.)
826         int localeHostMatch = getLocaleMatch();
827
828         // build a list of combinations of non standard qualifiers to add to each device's
829         // qualifier set when testing for a match.
830         // These qualifiers are: locale, night-mode, car dock.
831         List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
832
833         // If the edited file has locales, then we have to select a matching locale from
834         // the list.
835         // However, if it doesn't, we don't randomly take the first locale, we take one
836         // matching the current host locale (making sure it actually exist in the project)
837         int start, max;
838         if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) {
839             // add all the locales
840             start = 0;
841             max = mLocaleList.size();
842         } else {
843             // only add the locale host match
844             start = localeHostMatch;
845             max = localeHostMatch + 1; // test is <
846         }
847
848         for (int i = start ; i < max ; i++) {
849             ResourceQualifier[] l = mLocaleList.get(i);
850
851             ConfigBundle bundle = new ConfigBundle();
852             bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]);
853             bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]);
854
855             bundle.localeIndex = i;
856             configBundles.add(bundle);
857         }
858
859         // add the dock mode to the bundle combinations.
860         addDockModeToBundles(configBundles);
861
862         // add the night mode to the bundle combinations.
863         addNightModeToBundles(configBundles);
864
865         for (LayoutDevice device : mDeviceList) {
866             for (DeviceConfig config : device.getConfigs()) {
867
868                 // loop on the list of config bundles to create full configurations.
869                 for (ConfigBundle bundle : configBundles) {
870                     // create a new config with device config
871                     FolderConfiguration testConfig = new FolderConfiguration();
872                     testConfig.set(config.getConfig());
873
874                     // add on top of it, the extra qualifiers from the bundle
875                     testConfig.add(bundle.config);
876
877                     if (mEditedConfig.isMatchFor(testConfig)) {
878                         // this is a basic match. record it in case we don't find a match
879                         // where the edited file is a best config.
880                         anyMatches.add(new ConfigMatch(testConfig, device, config.getName(),
881                                 bundle));
882
883                         if (isCurrentFileBestMatchFor(testConfig)) {
884                             // this is what we want.
885                             bestMatches.add(new ConfigMatch(testConfig, device, config.getName(),
886                                     bundle));
887                         }
888                     }
889                 }
890             }
891         }
892
893         if (bestMatches.size() == 0) {
894             if (favorCurrentConfig) {
895                 // quick check
896                 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
897                     AdtPlugin.log(IStatus.ERROR,
898                             "favorCurrentConfig can only be true if the current config is compatible");
899                 }
900
901                 // just display the warning
902                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
903                         String.format(
904                                 "'%1$s' is not a best match for any device/locale combination.",
905                                 mEditedConfig.toDisplayString()),
906                         String.format(
907                                 "Displaying it with '%1$s'",
908                                 mCurrentConfig.toDisplayString()));
909             } else if (anyMatches.size() > 0) {
910                 // select the best device anyway.
911                 ConfigMatch match = selectConfigMatch(anyMatches);
912                 selectDevice(mState.device = match.device);
913                 fillConfigCombo(match.name);
914                 mLocaleCombo.select(match.bundle.localeIndex);
915                 mDockCombo.select(match.bundle.dockModeIndex);
916                 mNightCombo.select(match.bundle.nightModeIndex);
917
918                 // TODO: display a better warning!
919                 computeCurrentConfig();
920                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
921                         String.format(
922                                 "'%1$s' is not a best match for any device/locale combination.",
923                                 mEditedConfig.toDisplayString()),
924                         String.format(
925                                 "Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.",
926                                 mCurrentConfig.toDisplayString()));
927
928             } else {
929                 // TODO: there is no device/config able to display the layout, create one.
930                 // For the base config values, we'll take the first device and config,
931                 // and replace whatever qualifier required by the layout file.
932             }
933         } else {
934             ConfigMatch match = selectConfigMatch(bestMatches);
935             selectDevice(mState.device = match.device);
936             fillConfigCombo(match.name);
937             mLocaleCombo.select(match.bundle.localeIndex);
938             mDockCombo.select(match.bundle.dockModeIndex);
939             mNightCombo.select(match.bundle.nightModeIndex);
940         }
941     }
942
943     /**
944      * Note: this comparator imposes orderings that are inconsistent with equals.
945      */
946     private static class TabletConfigComparator implements Comparator<ConfigMatch> {
947         public int compare(ConfigMatch o1, ConfigMatch o2) {
948             ScreenSize ss1 = o1.testConfig.getScreenSizeQualifier().getValue();
949             ScreenSize ss2 = o2.testConfig.getScreenSizeQualifier().getValue();
950
951             // X-LARGE is better than all others (which are considered identical)
952             // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
953
954             if (ss1 == ScreenSize.XLARGE) {
955                 if (ss2 == ScreenSize.XLARGE) {
956                     ScreenOrientation so1 =
957                         o1.testConfig.getScreenOrientationQualifier().getValue();
958                     ScreenOrientation so2 =
959                         o2.testConfig.getScreenOrientationQualifier().getValue();
960
961                     if (so1 == ScreenOrientation.LANDSCAPE) {
962                         if (so2 == ScreenOrientation.LANDSCAPE) {
963                             return 0;
964                         } else {
965                             return -1;
966                         }
967                     } else if (so2 == ScreenOrientation.LANDSCAPE) {
968                         return 1;
969                     } else {
970                         return 0;
971                     }
972                 } else {
973                     return -1;
974                 }
975             } else if (ss2 == ScreenSize.XLARGE) {
976                 return 1;
977             } else {
978                 return 0;
979             }
980         }
981     }
982
983     /**
984      * Note: this comparator imposes orderings that are inconsistent with equals.
985      */
986     private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
987         public int compare(ConfigMatch o1, ConfigMatch o2) {
988             int dpi1 = Density.DEFAULT_DENSITY;
989             if (o1.testConfig.getPixelDensityQualifier() != null) {
990                 dpi1 = o1.testConfig.getPixelDensityQualifier().getValue().getDpiValue();
991             }
992
993             int dpi2 = Density.DEFAULT_DENSITY;
994             if (o2.testConfig.getPixelDensityQualifier() != null) {
995                 dpi2 = o2.testConfig.getPixelDensityQualifier().getValue().getDpiValue();
996             }
997
998             if (dpi1 == dpi2) {
999                 // portrait is better
1000                 ScreenOrientation so1 =
1001                     o1.testConfig.getScreenOrientationQualifier().getValue();
1002                 ScreenOrientation so2 =
1003                     o2.testConfig.getScreenOrientationQualifier().getValue();
1004
1005                 if (so1 == ScreenOrientation.PORTRAIT) {
1006                     if (so2 == ScreenOrientation.PORTRAIT) {
1007                         return 0;
1008                     } else {
1009                         return -1;
1010                     }
1011                 } else if (so2 == ScreenOrientation.PORTRAIT) {
1012                     return 1;
1013                 } else {
1014                     return 0;
1015                 }
1016             }
1017
1018             return dpi1 - dpi2;
1019         }
1020     }
1021
1022     private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
1023         // API 11+ : look for a x-large device.
1024         if (mProjectTarget.getVersion().getApiLevel() >= 11) {
1025             // TODO: Revisit this once phones can run higher APIs.
1026             // Maybe check the compatible-screen tag in the manifest to figure out what kind of
1027             // device should be used for display.
1028             Collections.sort(matches, new TabletConfigComparator());
1029         } else {
1030             // lets look for a high density device
1031             Collections.sort(matches, new PhoneConfigComparator());
1032         }
1033
1034         // the list has been sorted so that the first item is the best config
1035         return matches.get(0);
1036     }
1037
1038     private void addDockModeToBundles(List<ConfigBundle> addConfig) {
1039         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1040
1041         // loop on each item and for each, add all variations of the dock modes
1042         for (ConfigBundle bundle : addConfig) {
1043             int index = 0;
1044             for (DockMode mode : DockMode.values()) {
1045                 ConfigBundle b = new ConfigBundle(bundle);
1046                 b.config.setDockModeQualifier(new DockModeQualifier(mode));
1047                 b.dockModeIndex = index++;
1048                 list.add(b);
1049             }
1050         }
1051
1052         addConfig.clear();
1053         addConfig.addAll(list);
1054     }
1055
1056     private void addNightModeToBundles(List<ConfigBundle> addConfig) {
1057         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1058
1059         // loop on each item and for each, add all variations of the night modes
1060         for (ConfigBundle bundle : addConfig) {
1061             int index = 0;
1062             for (NightMode mode : NightMode.values()) {
1063                 ConfigBundle b = new ConfigBundle(bundle);
1064                 b.config.setNightModeQualifier(new NightModeQualifier(mode));
1065                 b.nightModeIndex = index++;
1066                 list.add(b);
1067             }
1068         }
1069
1070         addConfig.clear();
1071         addConfig.addAll(list);
1072     }
1073
1074     /**
1075      * Adapts the current device/config selection so that it's compatible with
1076      * {@link #mEditedConfig}.
1077      * <p/>If the current selection is compatible, nothing is changed.
1078      * <p/>If it's not compatible, configs from the current devices are tested.
1079      * <p/>If none are compatible, it reverts to
1080      * {@link #findAndSetCompatibleConfig(FolderConfiguration)}
1081      */
1082     private void adaptConfigSelection(boolean needBestMatch) {
1083         // check the device config (ie sans locale)
1084         boolean needConfigChange = true; // if still true, we need to find another config.
1085         boolean currentConfigIsCompatible = false;
1086         int configIndex = mDeviceConfigCombo.getSelectionIndex();
1087         if (configIndex != -1) {
1088             String configName = mDeviceConfigCombo.getItem(configIndex);
1089             FolderConfiguration currentConfig = mState.device.getFolderConfigByName(configName);
1090             if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) {
1091                 currentConfigIsCompatible = true; // current config is compatible
1092                 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
1093                     needConfigChange = false;
1094                 }
1095             }
1096         }
1097
1098         if (needConfigChange) {
1099             // if the current config/locale isn't a correct match, then
1100             // look for another config/locale in the same device.
1101             FolderConfiguration testConfig = new FolderConfiguration();
1102
1103             // first look in the current device.
1104             String matchName = null;
1105             int localeIndex = -1;
1106             mainloop: for (DeviceConfig config : mState.device.getConfigs()) {
1107                 testConfig.set(config.getConfig());
1108
1109                 // loop on the locales.
1110                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
1111                     ResourceQualifier[] locale = mLocaleList.get(i);
1112
1113                     // update the test config with the locale qualifiers
1114                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
1115                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
1116
1117                     if (mEditedConfig.isMatchFor(testConfig) &&
1118                             isCurrentFileBestMatchFor(testConfig)) {
1119                         matchName = config.getName();
1120                         localeIndex = i;
1121                         break mainloop;
1122                     }
1123                 }
1124             }
1125
1126             if (matchName != null) {
1127                 selectConfig(matchName);
1128                 mLocaleCombo.select(localeIndex);
1129             } else {
1130                 // no match in current device with any config/locale
1131                 // attempt to find another device that can display this particular config.
1132                 findAndSetCompatibleConfig(currentConfigIsCompatible);
1133             }
1134         }
1135     }
1136
1137     /**
1138      * Finds a locale matching the config from a file.
1139      * @param language the language qualifier or null if none is set.
1140      * @param region the region qualifier or null if none is set.
1141      * @return true if there was a change in the combobox as a result of applying the locale
1142      */
1143     private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
1144         boolean changed = false;
1145
1146         // find the locale match. Since the locale list is based on the content of the
1147         // project resources there must be an exact match.
1148         // The only trick is that the region could be null in the fileConfig but in our
1149         // list of locales, this is represented as a RegionQualifier with value of
1150         // FAKE_LOCALE_VALUE.
1151         final int count = mLocaleList.size();
1152         for (int i = 0 ; i < count ; i++) {
1153             ResourceQualifier[] locale = mLocaleList.get(i);
1154
1155             // the language qualifier in the locale list is never null.
1156             if (locale[LOCALE_LANG].equals(language)) {
1157                 // region comparison is more complex, as the region could be null.
1158                 if (region == null) {
1159                     if (RegionQualifier.FAKE_REGION_VALUE.equals(
1160                             ((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
1161                         // match!
1162                         if (mLocaleCombo.getSelectionIndex() != i) {
1163                             mLocaleCombo.select(i);
1164                             changed = true;
1165                         }
1166                         break;
1167                     }
1168                 } else if (region.equals(locale[LOCALE_REGION])) {
1169                     // match!
1170                     if (mLocaleCombo.getSelectionIndex() != i) {
1171                         mLocaleCombo.select(i);
1172                         changed = true;
1173                     }
1174                     break;
1175                 }
1176             }
1177         }
1178
1179         return changed;
1180     }
1181
1182     private void updateConfigDisplay(FolderConfiguration fileConfig) {
1183         String current = fileConfig.toDisplayString();
1184         String layoutLabel = current != null ? current : "(Default)";
1185         mCurrentLayoutLabel.setText(layoutLabel);
1186         mCurrentLayoutLabel.setToolTipText(layoutLabel);
1187     }
1188
1189     private void saveState() {
1190         if (mDisableUpdates == 0) {
1191             int index = mDeviceConfigCombo.getSelectionIndex();
1192             if (index != -1) {
1193                 mState.configName = mDeviceConfigCombo.getItem(index);
1194             } else {
1195                 mState.configName = null;
1196             }
1197
1198             // since the locales are relative to the project, only keeping the index is enough
1199             index = mLocaleCombo.getSelectionIndex();
1200             if (index != -1) {
1201                 mState.locale = mLocaleList.get(index);
1202             } else {
1203                 mState.locale = null;
1204             }
1205
1206             index = mThemeCombo.getSelectionIndex();
1207             if (index != -1) {
1208                 mState.theme = mThemeCombo.getItem(index);
1209             }
1210
1211             index = mDockCombo.getSelectionIndex();
1212             if (index != -1) {
1213                 mState.dock = DockMode.getByIndex(index);
1214             }
1215
1216             index = mNightCombo.getSelectionIndex();
1217             if (index != -1) {
1218                 mState.night = NightMode.getByIndex(index);
1219             }
1220
1221             index = mTargetCombo.getSelectionIndex();
1222             if (index != -1) {
1223                 mState.target = mTargetList.get(index);
1224             }
1225         }
1226     }
1227
1228     /**
1229      * Stores the current config selection into the edited file.
1230      */
1231     public void storeState() {
1232         AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData());
1233     }
1234
1235     /**
1236      * Updates the locale combo.
1237      * This must be called from the UI thread.
1238      */
1239     public void updateLocales() {
1240         if (mListener == null) {
1241             return; // can't do anything w/o it.
1242         }
1243
1244         mDisableUpdates++;
1245
1246         try {
1247             // Reset the combo
1248             mLocaleCombo.removeAll();
1249             mLocaleList.clear();
1250
1251             SortedSet<String> languages = null;
1252             boolean hasLocale = false;
1253
1254             // get the languages from the project.
1255             ResourceRepository projectRes = mListener.getProjectResources();
1256
1257             // in cases where the opened file is not linked to a project, this could be null.
1258             if (projectRes != null) {
1259                 // now get the languages from the project.
1260                 languages = projectRes.getLanguages();
1261
1262                 for (String language : languages) {
1263                     hasLocale = true;
1264
1265                     LanguageQualifier langQual = new LanguageQualifier(language);
1266
1267                     // find the matching regions and add them
1268                     SortedSet<String> regions = projectRes.getRegions(language);
1269                     for (String region : regions) {
1270                         mLocaleCombo.add(
1271                                 String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$
1272                         RegionQualifier regionQual = new RegionQualifier(region);
1273                         mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
1274                     }
1275
1276                     // now the entry for the other regions the language alone
1277                     if (regions.size() > 0) {
1278                         mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$
1279                     } else {
1280                         mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$
1281                     }
1282                     // create a region qualifier that will never be matched by qualified resources.
1283                     mLocaleList.add(new ResourceQualifier[] {
1284                             langQual,
1285                             new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1286                     });
1287                 }
1288             }
1289
1290             // add a locale not present in the project resources. This will let the dev
1291             // tests his/her default values.
1292             if (hasLocale) {
1293                 mLocaleCombo.add("Other");
1294             } else {
1295                 mLocaleCombo.add("Any locale");
1296             }
1297
1298             // create language/region qualifier that will never be matched by qualified resources.
1299             mLocaleList.add(new ResourceQualifier[] {
1300                     new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
1301                     new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1302             });
1303
1304             if (mState.locale != null) {
1305                 // FIXME: this may fails if the layout was deleted (and was the last one to have
1306                 // that local. (we have other problem in this case though)
1307                 setLocaleCombo(mState.locale[LOCALE_LANG],
1308                         mState.locale[LOCALE_REGION]);
1309             } else {
1310                 mLocaleCombo.select(0);
1311             }
1312
1313             mThemeCombo.getParent().layout();
1314         } finally {
1315             mDisableUpdates--;
1316         }
1317     }
1318
1319     private int getLocaleMatch() {
1320         Locale locale = Locale.getDefault();
1321         if (locale != null) {
1322             String currentLanguage = locale.getLanguage();
1323             String currentRegion = locale.getCountry();
1324
1325             final int count = mLocaleList.size();
1326             for (int l = 0 ; l < count ; l++) {
1327                 ResourceQualifier[] localeArray = mLocaleList.get(l);
1328                 LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG];
1329                 RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION];
1330
1331                 // there's always a ##/Other or ##/Any (which is the same, the region
1332                 // contains FAKE_REGION_VALUE). If we don't find a perfect region match
1333                 // we take the fake region. Since it's last in the list, this makes the
1334                 // test easy.
1335                 if (langQ.getValue().equals(currentLanguage) &&
1336                         (regionQ.getValue().equals(currentRegion) ||
1337                          regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) {
1338                     return l;
1339                 }
1340             }
1341
1342             // if no locale match the current local locale, it's likely that it is
1343             // the default one which is the last one.
1344             return count - 1;
1345         }
1346
1347         return -1;
1348     }
1349
1350     /**
1351      * Updates the theme combo.
1352      * This must be called from the UI thread.
1353      */
1354     private void updateThemes() {
1355         if (mListener == null) {
1356             return; // can't do anything w/o it.
1357         }
1358
1359         ResourceRepository frameworkRes = mListener.getFrameworkResources(getRenderingTarget());
1360
1361         mDisableUpdates++;
1362
1363         try {
1364             // Reset the combo
1365             mThemeCombo.removeAll();
1366             mIsProjectTheme.clear();
1367
1368             ArrayList<String> themes = new ArrayList<String>();
1369             String includedIn = mListener != null ? mListener.getIncludedWithin() : null;
1370
1371             // First list any themes that are declared by the manifest
1372             if (mEditedFile != null) {
1373                 IProject project = mEditedFile.getProject();
1374                 ManifestInfo manifest = ManifestInfo.get(project);
1375
1376                 // Look up the screen size for the current configuration
1377                 ScreenSize screenSize = null;
1378                 if (mState.device != null) {
1379                     List<DeviceConfig> configs = mState.device.getConfigs();
1380                     for (DeviceConfig config : configs) {
1381                         ScreenSizeQualifier qualifier =
1382                             config.getConfig().getScreenSizeQualifier();
1383                         screenSize = qualifier.getValue();
1384                         break;
1385                     }
1386                 }
1387                 // Look up the default/fallback theme to use for this project (which
1388                 // depends on the screen size when no particular theme is specified
1389                 // in the manifest)
1390                 String defaultTheme = manifest.getDefaultTheme(screenSize);
1391
1392                 Map<String, String> activityThemes = manifest.getActivityThemes();
1393                 String pkg = manifest.getPackage();
1394                 String preferred = null;
1395                 boolean isIncluded = includedIn != null;
1396                 if (mState.theme == null || isIncluded) {
1397                     String layoutName = ResourceHelper.getLayoutName(mEditedFile);
1398
1399                     // If we are rendering a layout in included context, pick the theme
1400                     // from the outer layout instead
1401                     if (includedIn != null) {
1402                         layoutName = includedIn;
1403                     }
1404
1405                     String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
1406                     if (activity != null) {
1407                         preferred = activityThemes.get(activity);
1408                     }
1409                     if (preferred == null) {
1410                         preferred = defaultTheme;
1411                     }
1412                     String preferredTheme = ResourceHelper.styleToTheme(preferred);
1413                     if (includedIn == null) {
1414                         mState.theme = preferredTheme;
1415                     }
1416                     boolean isProjectTheme = !preferred.startsWith(PREFIX_ANDROID_STYLE);
1417                     mThemeCombo.add(preferredTheme);
1418                     mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1419
1420                     mThemeCombo.add(THEME_SEPARATOR);
1421                     mIsProjectTheme.add(Boolean.FALSE);
1422                 }
1423
1424                 // Create a sorted list of unique themes referenced in the manifest
1425                 // (sort alphabetically, but place the preferred theme at the
1426                 // top of the list)
1427                 Set<String> themeSet = new HashSet<String>(activityThemes.values());
1428                 themeSet.add(defaultTheme);
1429                 List<String> themeList = new ArrayList<String>(themeSet);
1430                 final String first = preferred;
1431                 Collections.sort(themeList, new Comparator<String>() {
1432                     public int compare(String s1, String s2) {
1433                         if (s1 == first) {
1434                             return -1;
1435                         } else if (s1 == first) {
1436                             return 1;
1437                         } else {
1438                             return s1.compareTo(s2);
1439                         }
1440                     }
1441                 });
1442
1443                 if (themeList.size() > 1 ||
1444                         (themeList.size() == 1 && (preferred == null ||
1445                                 !preferred.equals(themeList.get(0))))) {
1446                     for (String style : themeList) {
1447                         String theme = ResourceHelper.styleToTheme(style);
1448
1449                         // Initialize the chosen theme to the first item
1450                         // in the used theme list (that's what would be chosen
1451                         // anyway) such that we stop attempting to look up
1452                         // the associated activity (during initialization,
1453                         // this method can be called repeatedly.)
1454                         if (mState.theme == null) {
1455                             mState.theme = theme;
1456                         }
1457
1458                         boolean isProjectTheme = !style.startsWith(PREFIX_ANDROID_STYLE);
1459                         mThemeCombo.add(theme);
1460                         mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1461                     }
1462                     mThemeCombo.add(THEME_SEPARATOR);
1463                     mIsProjectTheme.add(Boolean.FALSE);
1464                 }
1465             }
1466
1467             // get the themes, and languages from the Framework.
1468             int platformThemeCount = 0;
1469             if (frameworkRes != null) {
1470                 // get the configured resources for the framework
1471                 Map<ResourceType, Map<String, ResourceValue>> frameworResources =
1472                     frameworkRes.getConfiguredResources(getCurrentConfig());
1473
1474                 if (frameworResources != null) {
1475                     // get the styles.
1476                     Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
1477
1478
1479                     // collect the themes out of all the styles.
1480                     for (ResourceValue value : styles.values()) {
1481                         String name = value.getName();
1482                         if (name.startsWith("Theme.") || name.equals("Theme")) {
1483                             themes.add(value.getName());
1484                         }
1485                     }
1486
1487                     // sort them and add them to the combo
1488                     Collections.sort(themes);
1489
1490                     for (String theme : themes) {
1491                         mThemeCombo.add(theme);
1492                         mIsProjectTheme.add(Boolean.FALSE);
1493                     }
1494
1495                     platformThemeCount = themes.size();
1496                     themes.clear();
1497                 }
1498             }
1499
1500             // now get the themes and languages from the project.
1501             ResourceRepository projectRes = mListener.getProjectResources();
1502             // in cases where the opened file is not linked to a project, this could be null.
1503             if (projectRes != null) {
1504                 // get the configured resources for the project
1505                 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
1506                     mListener.getConfiguredProjectResources();
1507
1508                 if (configuredProjectRes != null) {
1509                     // get the styles.
1510                     Map<String, ResourceValue> styleMap = configuredProjectRes.get(
1511                             ResourceType.STYLE);
1512
1513                     if (styleMap != null) {
1514                         // collect the themes out of all the styles, ie styles that extend,
1515                         // directly or indirectly a platform theme.
1516                         for (ResourceValue value : styleMap.values()) {
1517                             if (isTheme(value, styleMap)) {
1518                                 themes.add(value.getName());
1519                             }
1520                         }
1521
1522                         // sort them and add them the to the combo.
1523                         if (platformThemeCount > 0 && themes.size() > 0) {
1524                             mThemeCombo.add(THEME_SEPARATOR);
1525                             mIsProjectTheme.add(Boolean.FALSE);
1526                         }
1527
1528                         Collections.sort(themes);
1529
1530                         for (String theme : themes) {
1531                             mThemeCombo.add(theme);
1532                             mIsProjectTheme.add(Boolean.TRUE);
1533                         }
1534                     }
1535                 }
1536             }
1537
1538             // try to reselect the previous theme.
1539             boolean needDefaultSelection = true;
1540
1541             if (mState.theme != null && includedIn == null) {
1542                 final int count = mThemeCombo.getItemCount();
1543                 for (int i = 0 ; i < count ; i++) {
1544                     if (mState.theme.equals(mThemeCombo.getItem(i))) {
1545                         mThemeCombo.select(i);
1546                         needDefaultSelection = false;
1547                         mThemeCombo.setEnabled(true);
1548                         break;
1549                     }
1550                 }
1551             }
1552
1553             if (needDefaultSelection) {
1554                 if (mThemeCombo.getItemCount() > 0) {
1555                     mThemeCombo.select(0);
1556                     mThemeCombo.setEnabled(true);
1557                 } else {
1558                     mThemeCombo.setEnabled(false);
1559                 }
1560             }
1561
1562             mThemeCombo.getParent().layout();
1563         } finally {
1564             mDisableUpdates--;
1565         }
1566
1567         assert mIsProjectTheme.size() == mThemeCombo.getItemCount();
1568     }
1569
1570     // ---- getters for the config selection values ----
1571
1572     public FolderConfiguration getEditedConfig() {
1573         return mEditedConfig;
1574     }
1575
1576     public FolderConfiguration getCurrentConfig() {
1577         return mCurrentConfig;
1578     }
1579
1580     public void getCurrentConfig(FolderConfiguration config) {
1581         config.set(mCurrentConfig);
1582     }
1583
1584     /**
1585      * Returns the currently selected {@link Density}. This is guaranteed to be non null.
1586      */
1587     public Density getDensity() {
1588         if (mCurrentConfig != null) {
1589             PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier();
1590             if (qual != null) {
1591                 // just a sanity check
1592                 Density d = qual.getValue();
1593                 if (d != Density.NODPI) {
1594                     return d;
1595                 }
1596             }
1597         }
1598
1599         // no config? return medium as the default density.
1600         return Density.MEDIUM;
1601     }
1602
1603     /**
1604      * Returns the current device xdpi.
1605      */
1606     public float getXDpi() {
1607         if (mState.device != null) {
1608             float dpi = mState.device.getXDpi();
1609             if (Float.isNaN(dpi) == false) {
1610                 return dpi;
1611             }
1612         }
1613
1614         // get the pixel density as the density.
1615         return getDensity().getDpiValue();
1616     }
1617
1618     /**
1619      * Returns the current device ydpi.
1620      */
1621     public float getYDpi() {
1622         if (mState.device != null) {
1623             float dpi = mState.device.getYDpi();
1624             if (Float.isNaN(dpi) == false) {
1625                 return dpi;
1626             }
1627         }
1628
1629         // get the pixel density as the density.
1630         return getDensity().getDpiValue();
1631     }
1632
1633     public Rectangle getScreenBounds() {
1634         // get the orientation from the current device config
1635         ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
1636         ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
1637         if (qual != null) {
1638             orientation = qual.getValue();
1639         }
1640
1641         // get the device screen dimension
1642         ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
1643         int s1, s2;
1644         if (qual2 != null) {
1645             s1 = qual2.getValue1();
1646             s2 = qual2.getValue2();
1647         } else {
1648             s1 = 480;
1649             s2 = 320;
1650         }
1651
1652         switch (orientation) {
1653             default:
1654             case PORTRAIT:
1655                 return new Rectangle(0, 0, s2, s1);
1656             case LANDSCAPE:
1657                 return new Rectangle(0, 0, s1, s2);
1658             case SQUARE:
1659                 return new Rectangle(0, 0, s1, s1);
1660         }
1661     }
1662
1663     /**
1664      * Returns the current theme, or null if the combo has no selection.
1665      *
1666      * @return the theme name, or null
1667      */
1668     public String getTheme() {
1669         int themeIndex = mThemeCombo.getSelectionIndex();
1670         if (themeIndex != -1) {
1671             return mThemeCombo.getItem(themeIndex);
1672         }
1673
1674         return null;
1675     }
1676
1677     /**
1678      * Returns the current device string, or null if the combo has no selection.
1679      *
1680      * @return the device name, or null
1681      */
1682     public String getDevice() {
1683         int deviceIndex = mDeviceCombo.getSelectionIndex();
1684         if (deviceIndex != -1) {
1685             return mDeviceCombo.getItem(deviceIndex);
1686         }
1687
1688         return null;
1689     }
1690
1691     /**
1692      * Returns whether the current theme selection is a project theme.
1693      * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
1694      * @return true for project theme, false for framework theme
1695      */
1696     public boolean isProjectTheme() {
1697         return mIsProjectTheme.get(mThemeCombo.getSelectionIndex()).booleanValue();
1698     }
1699
1700     public IAndroidTarget getRenderingTarget() {
1701         int index = mTargetCombo.getSelectionIndex();
1702         if (index >= 0) {
1703             return mTargetList.get(index);
1704         }
1705
1706         return null;
1707     }
1708
1709     /**
1710      * Loads the list of {@link IAndroidTarget} and inits the UI with it.
1711      */
1712     private void initTargets() {
1713         mTargetCombo.removeAll();
1714         mTargetList.clear();
1715
1716         Sdk currentSdk = Sdk.getCurrent();
1717         if (currentSdk != null) {
1718             IAndroidTarget[] targets = currentSdk.getTargets();
1719             int match = -1;
1720             for (int i = 0 ; i < targets.length; i++) {
1721                 // FIXME: support add-on rendering and check based on project minSdkVersion
1722                 if (targets[i].isPlatform()) {
1723                     mTargetCombo.add(targets[i].getFullName());
1724                     mTargetList.add(targets[i]);
1725
1726                     if (mRenderingTarget != null) {
1727                         // use equals because the rendering could be from a previous SDK, so
1728                         // it may not be the same instance.
1729                         if (mRenderingTarget.equals(targets[i])) {
1730                             match = mTargetList.indexOf(targets[i]);
1731                         }
1732                     } else if (mProjectTarget == targets[i]) {
1733                         match = mTargetList.indexOf(targets[i]);
1734                     }
1735                 }
1736             }
1737
1738             mTargetCombo.setEnabled(mTargetList.size() > 1);
1739             if (match == -1) {
1740                 mTargetCombo.deselectAll();
1741
1742                 // the rendering target is the same as the project.
1743                 mRenderingTarget = mProjectTarget;
1744             } else {
1745                 mTargetCombo.select(match);
1746
1747                 // set the rendering target to the new object.
1748                 mRenderingTarget = mTargetList.get(match);
1749             }
1750         }
1751     }
1752
1753     /**
1754      * Loads the list of {@link LayoutDevice} and inits the UI with it.
1755      */
1756     private void initDevices() {
1757         mDeviceList = null;
1758
1759         Sdk sdk = Sdk.getCurrent();
1760         if (sdk != null) {
1761             LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
1762             mDeviceList = manager.getCombinedList();
1763         }
1764
1765
1766         // remove older devices if applicable
1767         mDeviceCombo.removeAll();
1768         mDeviceConfigCombo.removeAll();
1769
1770         // fill with the devices
1771         if (mDeviceList != null) {
1772             for (LayoutDevice device : mDeviceList) {
1773                 mDeviceCombo.add(device.getName());
1774             }
1775             mDeviceCombo.select(0);
1776
1777             if (mDeviceList.size() > 0) {
1778                 List<DeviceConfig> configs = mDeviceList.get(0).getConfigs();
1779                 for (DeviceConfig config : configs) {
1780                     mDeviceConfigCombo.add(config.getName());
1781                 }
1782                 mDeviceConfigCombo.select(0);
1783                 if (configs.size() == 1) {
1784                     mDeviceConfigCombo.setEnabled(false);
1785                 }
1786             }
1787         }
1788
1789         // add the custom item
1790         mDeviceCombo.add("Custom...");
1791     }
1792
1793     /**
1794      * Selects a given {@link LayoutDevice} in the device combo, if it is found.
1795      * @param device the device to select
1796      * @return true if the device was found.
1797      */
1798     private boolean selectDevice(LayoutDevice device) {
1799         final int count = mDeviceList.size();
1800         for (int i = 0 ; i < count ; i++) {
1801             // since device comes from mDeviceList, we can use the == operator.
1802             if (device == mDeviceList.get(i)) {
1803                 mDeviceCombo.select(i);
1804                 return true;
1805             }
1806         }
1807
1808         return false;
1809     }
1810
1811     /**
1812      * Selects a config by name.
1813      * @param name the name of the config to select.
1814      */
1815     private void selectConfig(String name) {
1816         final int count = mDeviceConfigCombo.getItemCount();
1817         for (int i = 0 ; i < count ; i++) {
1818             String item = mDeviceConfigCombo.getItem(i);
1819             if (name.equals(item)) {
1820                 mDeviceConfigCombo.select(i);
1821                 return;
1822             }
1823         }
1824     }
1825
1826     /**
1827      * Called when the selection of the device combo changes.
1828      * @param recomputeLayout
1829      */
1830     private void onDeviceChange(boolean recomputeLayout) {
1831         // because changing the content of a combo triggers a change event, respect the
1832         // mDisableUpdates flag
1833         if (mDisableUpdates > 0) {
1834             return;
1835         }
1836
1837         String newConfigName = null;
1838
1839         int deviceIndex = mDeviceCombo.getSelectionIndex();
1840         if (deviceIndex != -1) {
1841             // check if the user is asking for the custom item
1842             if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
1843                 onCustomDeviceConfig();
1844                 return;
1845             }
1846
1847             // get the previous config, so that we can look for a close match
1848             if (mState.device != null) {
1849                 int index = mDeviceConfigCombo.getSelectionIndex();
1850                 if (index != -1) {
1851                     FolderConfiguration oldConfig = mState.device.getFolderConfigByName(
1852                             mDeviceConfigCombo.getItem(index));
1853
1854                     LayoutDevice newDevice = mDeviceList.get(deviceIndex);
1855
1856                     newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
1857                 }
1858             }
1859
1860             mState.device = mDeviceList.get(deviceIndex);
1861         } else {
1862             mState.device = null;
1863         }
1864
1865         fillConfigCombo(newConfigName);
1866
1867         computeCurrentConfig();
1868
1869         if (recomputeLayout) {
1870             onDeviceConfigChange();
1871         }
1872     }
1873
1874     /**
1875      * Handles a user request for the {@link ConfigManagerDialog}.
1876      */
1877     private void onCustomDeviceConfig() {
1878         ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
1879         dialog.open();
1880
1881         // save the user devices
1882         Sdk.getCurrent().getLayoutDeviceManager().save();
1883
1884         // Update the UI with no triggered event
1885         mDisableUpdates++;
1886
1887         try {
1888             LayoutDevice oldCurrent = mState.device;
1889
1890             // but first, update the device combo
1891             initDevices();
1892
1893             // attempts to reselect the current device.
1894             if (selectDevice(oldCurrent)) {
1895                 // current device still exists.
1896                 // reselect the config
1897                 selectConfig(mState.configName);
1898
1899                 // reset the UI as if it was just a replacement file, since we can keep
1900                 // the current device (and possibly config).
1901                 adaptConfigSelection(false /*needBestMatch*/);
1902
1903             } else {
1904                 // find a new device/config to match the current file.
1905                 findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
1906             }
1907         } finally {
1908             mDisableUpdates--;
1909         }
1910
1911         // recompute the current config
1912         computeCurrentConfig();
1913
1914         // force a redraw
1915         onDeviceChange(true /*recomputeLayout*/);
1916     }
1917
1918     /**
1919      * Attempts to find a close config among a list
1920      * @param oldConfig the reference config.
1921      * @param configs the list of config to search through
1922      * @return the name of the closest config match, or possibly null if no configs are compatible
1923      * (this can only happen if the configs don't have a single qualifier that is the same).
1924      */
1925     private String getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs) {
1926
1927         // create 2 lists as we're going to go through one and put the candidates in the other.
1928         ArrayList<DeviceConfig> list1 = new ArrayList<DeviceConfig>();
1929         ArrayList<DeviceConfig> list2 = new ArrayList<DeviceConfig>();
1930
1931         list1.addAll(configs);
1932
1933         final int count = FolderConfiguration.getQualifierCount();
1934         for (int i = 0 ; i < count ; i++) {
1935             // compute the new candidate list by only taking configs that have
1936             // the same i-th qualifier as the old config
1937             for (DeviceConfig c : list1) {
1938                 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
1939
1940                 FolderConfiguration folderConfig = c.getConfig();
1941                 ResourceQualifier newQualifier = folderConfig.getQualifier(i);
1942
1943                 if (oldQualifier == null) {
1944                     if (newQualifier == null) {
1945                         list2.add(c);
1946                     }
1947                 } else if (oldQualifier.equals(newQualifier)) {
1948                     list2.add(c);
1949                 }
1950             }
1951
1952             // at any moment if the new candidate list contains only one match, its name
1953             // is returned.
1954             if (list2.size() == 1) {
1955                 return list2.get(0).getName();
1956             }
1957
1958             // if the list is empty, then all the new configs failed. It is considered ok, and
1959             // we move to the next qualifier anyway. This way, if a qualifier is different for
1960             // all new configs it is simply ignored.
1961             if (list2.size() != 0) {
1962                 // move the candidates back into list1.
1963                 list1.clear();
1964                 list1.addAll(list2);
1965                 list2.clear();
1966             }
1967         }
1968
1969         // the only way to reach this point is if there's an exact match.
1970         // (if there are more than one, then there's a duplicate config and it doesn't matter,
1971         // we take the first one).
1972         if (list1.size() > 0) {
1973             return list1.get(0).getName();
1974         }
1975
1976         return null;
1977     }
1978
1979     /**
1980      * fills the config combo with new values based on {@link #mState}.device.
1981      * @param refName an optional name. if set the selection will match this name (if found)
1982      */
1983     private void fillConfigCombo(String refName) {
1984         mDeviceConfigCombo.removeAll();
1985
1986         if (mState.device != null) {
1987             int selectionIndex = 0;
1988             int i = 0;
1989
1990             for (DeviceConfig config : mState.device.getConfigs()) {
1991                 mDeviceConfigCombo.add(config.getName());
1992
1993                 if (config.getName().equals(refName)) {
1994                     selectionIndex = i;
1995                 }
1996                 i++;
1997             }
1998
1999             mDeviceConfigCombo.select(selectionIndex);
2000             mDeviceConfigCombo.setEnabled(mState.device.getConfigs().size() > 1);
2001         }
2002     }
2003
2004     /**
2005      * Called when the device config selection changes.
2006      */
2007     private void onDeviceConfigChange() {
2008         // because changing the content of a combo triggers a change event, respect the
2009         // mDisableUpdates flag
2010         if (mDisableUpdates > 0) {
2011             return;
2012         }
2013
2014         if (computeCurrentConfig() && mListener != null) {
2015             mListener.onConfigurationChange();
2016             mListener.onDevicePostChange();
2017         }
2018     }
2019
2020     /**
2021      * Call back for language combo selection
2022      */
2023     private void onLocaleChange() {
2024         // because mLocaleList triggers onLocaleChange at each modification, the filling
2025         // of the combo with data will trigger notifications, and we don't want that.
2026         if (mDisableUpdates > 0) {
2027             return;
2028         }
2029
2030         if (computeCurrentConfig() &&  mListener != null) {
2031             mListener.onConfigurationChange();
2032         }
2033
2034         // Store locale project-wide setting
2035         saveRenderState();
2036     }
2037
2038     private void onDockChange() {
2039         if (computeCurrentConfig() &&  mListener != null) {
2040             mListener.onConfigurationChange();
2041         }
2042     }
2043
2044     private void onDayChange() {
2045         if (computeCurrentConfig() &&  mListener != null) {
2046             mListener.onConfigurationChange();
2047         }
2048     }
2049
2050     /**
2051      * Call back for api level combo selection
2052      */
2053     private void onRenderingTargetChange() {
2054         // because mApiCombo triggers onApiLevelChange at each modification, the filling
2055         // of the combo with data will trigger notifications, and we don't want that.
2056         if (mDisableUpdates > 0) {
2057             return;
2058         }
2059
2060         // tell the listener a new rendering target is being set. Need to do this before updating
2061         // mRenderingTarget.
2062         if (mListener != null && mRenderingTarget != null) {
2063             mListener.onRenderingTargetPreChange(mRenderingTarget);
2064         }
2065
2066         int index = mTargetCombo.getSelectionIndex();
2067         mRenderingTarget = mTargetList.get(index);
2068
2069         boolean computeOk = computeCurrentConfig();
2070
2071         // force a theme update to reflect the new rendering target.
2072         // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2073         // to figure out the theme list.
2074         updateThemes();
2075
2076         // since the state is saved in computeCurrentConfig, we need to resave it since theme
2077         // change could have impacted it.
2078         saveState();
2079
2080         if (mListener != null && mRenderingTarget != null) {
2081             mListener.onRenderingTargetPostChange(mRenderingTarget);
2082         }
2083
2084         if (computeOk &&  mListener != null) {
2085             mListener.onConfigurationChange();
2086         }
2087
2088         // Store project-wide render-target setting
2089         saveRenderState();
2090     }
2091
2092     /**
2093      * Saves the current state and the current configuration
2094      *
2095      * @see #saveState()
2096      */
2097     private boolean computeCurrentConfig() {
2098         saveState();
2099
2100         if (mState.device != null) {
2101             // get the device config from the device/config combos.
2102             int configIndex = mDeviceConfigCombo.getSelectionIndex();
2103             String name = mDeviceConfigCombo.getItem(configIndex);
2104             FolderConfiguration config = mState.device.getFolderConfigByName(name);
2105
2106             // replace the config with the one from the device
2107             mCurrentConfig.set(config);
2108
2109             // replace the locale qualifiers with the one coming from the locale combo
2110             int index = mLocaleCombo.getSelectionIndex();
2111             if (index != -1) {
2112                 ResourceQualifier[] localeQualifiers = mLocaleList.get(index);
2113
2114                 mCurrentConfig.setLanguageQualifier(
2115                         (LanguageQualifier)localeQualifiers[LOCALE_LANG]);
2116                 mCurrentConfig.setRegionQualifier(
2117                         (RegionQualifier)localeQualifiers[LOCALE_REGION]);
2118             }
2119
2120             index = mDockCombo.getSelectionIndex();
2121             if (index == -1) {
2122                 index = 0; // no selection = 0
2123             }
2124             mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index)));
2125
2126             index = mNightCombo.getSelectionIndex();
2127             if (index == -1) {
2128                 index = 0; // no selection = 0
2129             }
2130             mCurrentConfig.setNightModeQualifier(
2131                     new NightModeQualifier(NightMode.getByIndex(index)));
2132
2133             // replace the API level by the selection of the combo
2134             index = mTargetCombo.getSelectionIndex();
2135             if (index == -1) {
2136                 index = mTargetList.indexOf(mProjectTarget);
2137             }
2138             if (index != -1) {
2139                 IAndroidTarget target = mTargetList.get(index);
2140
2141                 if (target != null) {
2142                     mCurrentConfig.setVersionQualifier(
2143                             new VersionQualifier(target.getVersion().getApiLevel()));
2144                 }
2145             }
2146
2147             // update the create button.
2148             checkCreateEnable();
2149
2150             return true;
2151         }
2152
2153         return false;
2154     }
2155
2156     private void onThemeChange() {
2157         saveState();
2158
2159         int themeIndex = mThemeCombo.getSelectionIndex();
2160         if (themeIndex != -1) {
2161             String theme = mThemeCombo.getItem(themeIndex);
2162
2163             if (theme.equals(THEME_SEPARATOR)) {
2164                 mThemeCombo.select(0);
2165             }
2166
2167             if (mListener != null) {
2168                 mListener.onThemeChange();
2169             }
2170         }
2171     }
2172
2173     /**
2174      * Returns whether the given <var>style</var> is a theme.
2175      * This is done by making sure the parent is a theme.
2176      * @param value the style to check
2177      * @param styleMap the map of styles for the current project. Key is the style name.
2178      * @return True if the given <var>style</var> is a theme.
2179      */
2180     private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap) {
2181         if (value instanceof StyleResourceValue) {
2182             StyleResourceValue style = (StyleResourceValue)value;
2183
2184             boolean frameworkStyle = false;
2185             String parentStyle = style.getParentStyle();
2186             if (parentStyle == null) {
2187                 // if there is no specified parent style we look an implied one.
2188                 // For instance 'Theme.light' is implied child style of 'Theme',
2189                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
2190                 String name = style.getName();
2191                 int index = name.lastIndexOf('.');
2192                 if (index != -1) {
2193                     parentStyle = name.substring(0, index);
2194                 }
2195             } else {
2196                 // remove the useless @ if it's there
2197                 if (parentStyle.startsWith("@")) {
2198                     parentStyle = parentStyle.substring(1);
2199                 }
2200
2201                 // check for framework identifier.
2202                 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
2203                     frameworkStyle = true;
2204                     parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
2205                 }
2206
2207                 // at this point we could have the format style/<name>. we want only the name
2208                 if (parentStyle.startsWith("style/")) {
2209                     parentStyle = parentStyle.substring("style/".length());
2210                 }
2211             }
2212
2213             if (parentStyle != null) {
2214                 if (frameworkStyle) {
2215                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
2216                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
2217                 } else {
2218                     // if it's a project style, we check this is a theme.
2219                     value = styleMap.get(parentStyle);
2220                     if (value != null) {
2221                         return isTheme(value, styleMap);
2222                     }
2223                 }
2224             }
2225         }
2226
2227         return false;
2228     }
2229
2230     private void checkCreateEnable() {
2231         mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
2232     }
2233
2234     /**
2235      * Checks whether the current edited file is the best match for a given config.
2236      * <p/>
2237      * This tests against other versions of the same layout in the project.
2238      * <p/>
2239      * The given config must be compatible with the current edited file.
2240      * @param config the config to test.
2241      * @return true if the current edited file is the best match in the project for the
2242      * given config.
2243      */
2244     private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
2245         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
2246                 ResourceFolderType.LAYOUT, config);
2247
2248         if (match != null) {
2249             return match.getFile().equals(mEditedFile);
2250         } else {
2251             // if we stop here that means the current file is not even a match!
2252             AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
2253         }
2254
2255         return false;
2256     }
2257
2258     /**
2259      * Resets the configuration chooser to reflect the given file configuration. This is
2260      * intended to be used by the "Show Included In" functionality where the user has
2261      * picked a non-default configuration (such as a particular landscape layout) and the
2262      * configuration chooser must be switched to a landscape layout. This method will
2263      * trigger a model change.
2264      * <p>
2265      * This will NOT trigger a redraw event!
2266      * <p>
2267      * FIXME: We are currently setting the configuration file to be the configuration for
2268      * the "outer" (the including) file, rather than the inner file, which is the file the
2269      * user is actually editing. We need to refine this, possibly with a way for the user
2270      * to choose which configuration they are editing. And in particular, we should be
2271      * filtering the configuration chooser to only show options in the outer configuration
2272      * that are compatible with the inner included file.
2273      *
2274      * @param file the file to be configured
2275      */
2276     public void resetConfigFor(IFile file) {
2277         setFile(file);
2278         mEditedConfig = null;
2279         onXmlModelLoaded();
2280     }
2281
2282     /**
2283      * Syncs this configuration to the project wide locale and render target settings. The
2284      * locale may ignore the project-wide setting if it is a locale-specific
2285      * configuration.
2286      *
2287      * @return true if one or both of the toggles were changed, false if there were no
2288      *         changes
2289      */
2290     public boolean syncRenderState() {
2291         if (mEditedConfig == null) {
2292             // Startup; ignore
2293             return false;
2294         }
2295
2296         boolean localeChanged = false;
2297         boolean renderTargetChanged = false;
2298
2299         // When a page is re-activated, force the toggles to reflect the current project
2300         // state
2301
2302         Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
2303
2304         // Only sync the locale if this layout is not already a locale-specific layout!
2305         if (!isLocaleSpecificLayout()) {
2306             ResourceQualifier[] locale = pair.getFirst();
2307             if (locale != null) {
2308                 localeChanged = setLocaleCombo(locale[0], locale[1]);
2309             }
2310         }
2311
2312         // Sync render target
2313         IAndroidTarget target = pair.getSecond();
2314         if (target != null) {
2315             int targetIndex = mTargetList.indexOf(target);
2316             if (targetIndex != mTargetCombo.getSelectionIndex()) {
2317                 mTargetCombo.select(targetIndex);
2318                 renderTargetChanged = true;
2319             }
2320         }
2321
2322         if (!renderTargetChanged && !localeChanged) {
2323             return false;
2324         }
2325
2326         // Update the locale and/or the render target. This code contains a logical
2327         // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
2328         // such that we don't duplicate work.
2329
2330         if (renderTargetChanged) {
2331             if (mListener != null && mRenderingTarget != null) {
2332                 mListener.onRenderingTargetPreChange(mRenderingTarget);
2333             }
2334             int targetIndex = mTargetCombo.getSelectionIndex();
2335             mRenderingTarget = mTargetList.get(targetIndex);
2336         }
2337
2338         // Compute the new configuration; we want to do this both for locale changes
2339         // and for render targets.
2340         boolean computeOk = computeCurrentConfig();
2341
2342         if (renderTargetChanged) {
2343             // force a theme update to reflect the new rendering target.
2344             // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2345             // to figure out the theme list.
2346             updateThemes();
2347
2348             if (mListener != null && mRenderingTarget != null) {
2349                 mListener.onRenderingTargetPostChange(mRenderingTarget);
2350             }
2351         }
2352
2353         // For both locale and render target changes
2354         if (computeOk &&  mListener != null) {
2355             mListener.onConfigurationChange();
2356         }
2357
2358         return true;
2359     }
2360
2361     /**
2362      * Loads the render state (the locale and the render target, which are shared among
2363      * all the layouts meaning that changing it in one will change it in all) and returns
2364      * the current project-wide locale and render target to be used.
2365      *
2366      * @return a pair of locale resource qualifiers and render target
2367      */
2368     private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() {
2369         IProject project = mEditedFile.getProject();
2370         try {
2371             String data = project.getPersistentProperty(NAME_RENDER_STATE);
2372             if (data != null) {
2373                 ResourceQualifier[] locale = null;
2374                 IAndroidTarget target = null;
2375
2376                 String[] values = data.split(SEP);
2377                 if (values.length == 2) {
2378                     locale = new ResourceQualifier[2];
2379                     String locales[] = values[0].split(SEP_LOCALE);
2380                     if (locales.length >= 2) {
2381                         if (locales[0].length() > 0) {
2382                             locale[0] = new LanguageQualifier(locales[0]);
2383                         }
2384                         if (locales[1].length() > 0) {
2385                             locale[1] = new RegionQualifier(locales[1]);
2386                         }
2387                     }
2388                     target = stringToTarget(values[1]);
2389                 }
2390
2391                 return Pair.of(locale, target);
2392             }
2393
2394             ResourceQualifier[] any = new ResourceQualifier[] {
2395                     new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
2396                     new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
2397             };
2398
2399             return Pair.of(any, findDefaultRenderTarget());
2400         } catch (CoreException e) {
2401             AdtPlugin.log(e, null);
2402         }
2403
2404         return null;
2405     }
2406
2407     /** Returns true if the current layout is locale-specific */
2408     private boolean isLocaleSpecificLayout() {
2409         return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null;
2410     }
2411
2412     /**
2413      * Saves the render state (the current locale and render target settings) into the
2414      * project wide settings storage
2415      */
2416     private void saveRenderState() {
2417         IProject project = mEditedFile.getProject();
2418         try {
2419             int index = mLocaleCombo.getSelectionIndex();
2420             ResourceQualifier[] locale = mLocaleList.get(index);
2421             index = mTargetCombo.getSelectionIndex();
2422             IAndroidTarget target = mTargetList.get(index);
2423
2424             // Generate a persistent string from locale+target
2425             StringBuilder sb = new StringBuilder();
2426             if (locale != null) {
2427                 if (locale[0] != null && locale[1] != null) {
2428                     // locale[0]/[1] can be null sometimes when starting Eclipse
2429                     sb.append(((LanguageQualifier) locale[0]).getValue());
2430                     sb.append(SEP_LOCALE);
2431                     sb.append(((RegionQualifier) locale[1]).getValue());
2432                 }
2433             }
2434             sb.append(SEP);
2435             if (target != null) {
2436                 sb.append(targetToString(target));
2437                 sb.append(SEP);
2438             }
2439
2440             String data = sb.toString();
2441             project.setPersistentProperty(NAME_RENDER_STATE, data);
2442         } catch (CoreException e) {
2443             AdtPlugin.log(e, null);
2444         }
2445     }
2446 }