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