Home | History | Annotate | Download | only in configuration
      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          */
    207         void onConfigurationChange();
    208 
    209         /**
    210          * Called after a device has changed (in addition to {@link #onConfigurationChange}
    211          * getting called)
    212          */
    213         void onDevicePostChange();
    214 
    215         /**
    216          * Called when the current theme changes. The theme can be queried with
    217          * {@link ConfigurationComposite#getTheme()}.
    218          */
    219         void onThemeChange();
    220 
    221         /**
    222          * Called when the "Create" button is clicked.
    223          */
    224         void onCreate();
    225 
    226         /**
    227          * Called before the rendering target changes.
    228          * @param oldTarget the old rendering target
    229          */
    230         void onRenderingTargetPreChange(IAndroidTarget oldTarget);
    231 
    232         /**
    233          * Called after the rendering target changes.
    234          *
    235          * @param target the new rendering target
    236          */
    237         void onRenderingTargetPostChange(IAndroidTarget target);
    238 
    239         ResourceRepository getProjectResources();
    240         ResourceRepository getFrameworkResources();
    241         ResourceRepository getFrameworkResources(IAndroidTarget target);
    242         Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
    243         Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
    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 
    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 
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 */
    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 
    829         ConfigBundle() {
    830             config = new FolderConfiguration();
    831             localeIndex = 0;
    832             dockModeIndex = 0;
    833             nightModeIndex = 0;
    834         }
    835 
    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 
    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
    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      */
    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
   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 
   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
   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 
   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 
   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 
   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 
   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      */
   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      */
   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 
   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 
   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      */
   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      */
   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 
   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      */
   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 
   1681     public FolderConfiguration getEditedConfig() {
   1682         return mEditedConfig;
   1683     }
   1684 
   1685     public FolderConfiguration getCurrentConfig() {
   1686         return mCurrentConfig;
   1687     }
   1688 
   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      */
   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      */
   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      */
   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 
   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      */
   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      */
   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      */
   1805     public boolean isProjectTheme() {
   1806         return mIsProjectTheme.get(mThemeCombo.getSelectionIndex()).booleanValue();
   1807     }
   1808 
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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 
   2147     private void onDockChange() {
   2148         if (computeCurrentConfig() &&  mListener != null) {
   2149             mListener.onConfigurationChange();
   2150         }
   2151     }
   2152 
   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      */
   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      */
   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 
   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      */
   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 
   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      */
   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      */
   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      */
   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      */
   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 */
   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      */
   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