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 com.android.ide.eclipse.adt.AdtPlugin;
     20 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     21 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
     22 import com.android.ide.eclipse.adt.internal.resources.configurations.DockModeQualifier;
     23 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
     24 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
     25 import com.android.ide.eclipse.adt.internal.resources.configurations.NightModeQualifier;
     26 import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier;
     27 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
     28 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
     29 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenDimensionQualifier;
     30 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenOrientationQualifier;
     31 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
     32 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
     33 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
     34 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
     35 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     37 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
     38 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
     39 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
     40 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
     42 import com.android.layoutlib.api.IResourceValue;
     43 import com.android.layoutlib.api.IStyleResourceValue;
     44 import com.android.sdklib.IAndroidTarget;
     45 import com.android.sdklib.resources.Density;
     46 import com.android.sdklib.resources.DockMode;
     47 import com.android.sdklib.resources.NightMode;
     48 import com.android.sdklib.resources.ScreenOrientation;
     49 
     50 import org.eclipse.core.resources.IFile;
     51 import org.eclipse.core.resources.IFolder;
     52 import org.eclipse.core.resources.IProject;
     53 import org.eclipse.core.runtime.CoreException;
     54 import org.eclipse.core.runtime.IStatus;
     55 import org.eclipse.core.runtime.QualifiedName;
     56 import org.eclipse.draw2d.geometry.Rectangle;
     57 import org.eclipse.swt.SWT;
     58 import org.eclipse.swt.events.SelectionAdapter;
     59 import org.eclipse.swt.events.SelectionEvent;
     60 import org.eclipse.swt.events.SelectionListener;
     61 import org.eclipse.swt.graphics.Image;
     62 import org.eclipse.swt.layout.GridData;
     63 import org.eclipse.swt.layout.GridLayout;
     64 import org.eclipse.swt.widgets.Button;
     65 import org.eclipse.swt.widgets.Combo;
     66 import org.eclipse.swt.widgets.Composite;
     67 import org.eclipse.swt.widgets.Label;
     68 
     69 import java.util.ArrayList;
     70 import java.util.Collections;
     71 import java.util.List;
     72 import java.util.Map;
     73 import java.util.Set;
     74 import java.util.SortedSet;
     75 import java.util.Map.Entry;
     76 
     77 /**
     78  * A composite that displays the current configuration displayed in a Graphical Layout Editor.
     79  * <p/>
     80  * The composite has several entry points:<br>
     81  * - {@link #setFile(File)}<br>
     82  *   Called after the constructor to set the file being edited. Nothing else is performed.<br>
     83  *<br>
     84  * - {@link #onXmlModelLoaded()}<br>
     85  *   Called when the XML model is loaded, either the first time or when the Target/SDK changes.
     86  *   This initializes the UI, either with the first compatible configuration found, or attempts
     87  *   to restore a configuration if one is found to have been saved in the file persistent storage.
     88  *   (see {@link #storeState()})<br>
     89  *<br>
     90  * - {@link #replaceFile(File)}<br>
     91  *   Called when a file, representing the same resource but with a different config is opened<br>
     92  *   by the user.<br>
     93  *<br>
     94  * - {@link #changeFileOnNewConfig(FolderConfiguration)}<br>
     95  *   Called when config change triggers the editing of a file with a different config.
     96  *<p/>
     97  * Additionally, the composite can handle the following events.<br>
     98  * - SDK reload. This is when the main SDK is finished loading.<br>
     99  * - Target reload. This is when the target used by the project is the edited file has finished<br>
    100  *   loading.<br>
    101  */
    102 public class ConfigurationComposite extends Composite {
    103 
    104     private final static String CONFIG_STATE = "state";  //$NON-NLS-1$
    105     private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
    106 
    107     private final static int LOCALE_LANG = 0;
    108     private final static int LOCALE_REGION = 1;
    109 
    110     private Button mClippingButton;
    111     private Label mCurrentLayoutLabel;
    112 
    113     private Combo mDeviceCombo;
    114     private Combo mDeviceConfigCombo;
    115     private Combo mLocaleCombo;
    116     private Combo mDockCombo;
    117     private Combo mNightCombo;
    118     private Combo mThemeCombo;
    119     private Button mCreateButton;
    120 
    121     private int mPlatformThemeCount = 0;
    122     /** updates are disabled if > 0 */
    123     private int mDisableUpdates = 0;
    124 
    125     private List<LayoutDevice> mDeviceList;
    126 
    127     private final ArrayList<ResourceQualifier[] > mLocaleList =
    128         new ArrayList<ResourceQualifier[]>();
    129 
    130     /**
    131      * clipping value. If true, the rendering is limited to the screensize. This is the default
    132      * value
    133      */
    134     private boolean mClipping = true;
    135 
    136     private final ConfigState mState = new ConfigState();
    137 
    138     private boolean mSdkChanged = false;
    139     private boolean mFirstXmlModelChange = true;
    140 
    141     /** The config listener given to the constructor. Never null. */
    142     private final IConfigListener mListener;
    143 
    144     /** The {@link FolderConfiguration} representing the state of the UI controls */
    145     private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
    146 
    147     /** The file being edited */
    148     private IFile mEditedFile;
    149     /** The {@link ProjectResources} for the edited file's project */
    150     private ProjectResources mResources;
    151     /** The target of the project of the file being edited. */
    152     private IAndroidTarget mTarget;
    153     /** The {@link FolderConfiguration} being edited. */
    154     private FolderConfiguration mEditedConfig;
    155 
    156 
    157     /**
    158      * Interface implemented by the part which owns a {@link ConfigurationComposite}.
    159      * This notifies the owners when the configuration change.
    160      * The owner must also provide methods to provide the configuration that will
    161      * be displayed.
    162      */
    163     public interface IConfigListener {
    164         void onConfigurationChange();
    165         void onThemeChange();
    166         void onCreate();
    167         void onClippingChange();
    168 
    169         ProjectResources getProjectResources();
    170         ProjectResources getFrameworkResources();
    171         Map<String, Map<String, IResourceValue>> getConfiguredProjectResources();
    172         Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources();
    173     }
    174 
    175     /**
    176      * State of the current config. This is used during UI reset to attempt to return the
    177      * rendering to its original configuration.
    178      */
    179     private class ConfigState {
    180         private final static String SEP = ":"; //$NON-NLS-1$
    181         private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
    182 
    183         LayoutDevice device;
    184         String configName;
    185         ResourceQualifier[] locale;
    186         String theme;
    187         /** dock mode. Guaranteed to be non null */
    188         DockMode dock = DockMode.NONE;
    189         /** night mode. Guaranteed to be non null */
    190         NightMode night = NightMode.NOTNIGHT;
    191 
    192         String getData() {
    193             StringBuilder sb = new StringBuilder();
    194             if (device != null) {
    195                 sb.append(device.getName());
    196                 sb.append(SEP);
    197                 sb.append(configName);
    198                 sb.append(SEP);
    199                 if (locale != null) {
    200                     if (locale[0] != null && locale[1] != null) {
    201                         // locale[0]/[1] can be null sometimes when starting Eclipse
    202                         sb.append(((LanguageQualifier) locale[0]).getValue());
    203                         sb.append(SEP_LOCALE);
    204                         sb.append(((RegionQualifier) locale[1]).getValue());
    205                     }
    206                 }
    207                 sb.append(SEP);
    208                 sb.append(theme);
    209                 sb.append(SEP);
    210                 sb.append(dock.getResourceValue());
    211                 sb.append(SEP);
    212                 sb.append(night.getResourceValue());
    213                 sb.append(SEP);
    214             }
    215 
    216             return sb.toString();
    217         }
    218 
    219         boolean setData(String data) {
    220             String[] values = data.split(SEP);
    221             if (values.length == 6) {
    222                 for (LayoutDevice d : mDeviceList) {
    223                     if (d.getName().equals(values[0])) {
    224                         device = d;
    225                         FolderConfiguration config = device.getConfigs().get(values[1]);
    226                         if (config != null) {
    227                             configName = values[1];
    228 
    229                             locale = new ResourceQualifier[2];
    230                             String locales[] = values[2].split(SEP_LOCALE);
    231                             if (locales.length >= 2) {
    232                                 if (locales[0].length() > 0) {
    233                                     locale[0] = new LanguageQualifier(locales[0]);
    234                                 }
    235                                 if (locales[1].length() > 0) {
    236                                     locale[1] = new RegionQualifier(locales[1]);
    237                                 }
    238                             }
    239 
    240                             theme = values[3];
    241                             dock = DockMode.getEnum(values[4]);
    242                             if (dock == null) {
    243                                 dock = DockMode.NONE;
    244                             }
    245                             night = NightMode.getEnum(values[5]);
    246                             if (night == null) {
    247                                 night = NightMode.NOTNIGHT;
    248                             }
    249 
    250                             return true;
    251                         }
    252                     }
    253                 }
    254             }
    255 
    256             return false;
    257         }
    258 
    259         @Override
    260         public String toString() {
    261             StringBuilder sb = new StringBuilder();
    262             if (device != null) {
    263                 sb.append(device.getName());
    264             } else {
    265                 sb.append("null");
    266             }
    267             sb.append(SEP);
    268             sb.append(configName);
    269             sb.append(SEP);
    270             if (locale != null) {
    271                 sb.append(((LanguageQualifier) locale[0]).getValue());
    272                 sb.append(SEP_LOCALE);
    273                 sb.append(((RegionQualifier) locale[1]).getValue());
    274             }
    275             sb.append(SEP);
    276             sb.append(theme);
    277             sb.append(SEP);
    278             sb.append(dock.getResourceValue());
    279             sb.append(SEP);
    280             sb.append(night.getResourceValue());
    281             sb.append(SEP);
    282 
    283             return sb.toString();
    284         }
    285     }
    286 
    287     /**
    288      * Interface implemented by the part which owns a {@link ConfigurationComposite}
    289      * to define and handle custom toggle buttons in the button bar. Each toggle is
    290      * implemented using a button, with a callback when the button is selected.
    291      */
    292     public static abstract class CustomToggle {
    293 
    294         /** The UI label of the toggle. Can be null if the image exists. */
    295         private final String mUiLabel;
    296 
    297         /** The image to use for this toggle. Can be null if the label exists. */
    298         private final Image mImage;
    299 
    300         /** The tooltip for the toggle. Can be null. */
    301         private final String mUiTooltip;
    302 
    303         /**
    304          * Initializes a new {@link CustomToggle}. The values set here will be used
    305          * later to create the actual toggle.
    306          *
    307          * @param uiLabel   The UI label of the toggle. Can be null if the image exists.
    308          * @param image     The image to use for this toggle. Can be null if the label exists.
    309          * @param uiTooltip The tooltip for the toggle. Can be null.
    310          */
    311         public CustomToggle(
    312                 String uiLabel,
    313                 Image image,
    314                 String uiTooltip) {
    315             mUiLabel = uiLabel;
    316             mImage = image;
    317             mUiTooltip = uiTooltip;
    318         }
    319 
    320         /** Called by the {@link ConfigurationComposite} when the button is selected. */
    321         public abstract void onSelected(boolean newState);
    322 
    323         private void createToggle(Composite parent) {
    324             final Button b = new Button(parent, SWT.TOGGLE | SWT.FLAT);
    325 
    326             if (mUiTooltip != null) {
    327                 b.setToolTipText(mUiTooltip);
    328             }
    329             if (mImage != null) {
    330                 b.setImage(mImage);
    331             }
    332             if (mUiLabel != null) {
    333                 b.setText(mUiLabel);
    334             }
    335 
    336             b.addSelectionListener(new SelectionAdapter() {
    337                 @Override
    338                 public void widgetSelected(SelectionEvent e) {
    339                     onSelected(b.getSelection());
    340                 }
    341             });
    342         }
    343     }
    344 
    345     /**
    346      * Creates a new {@link ConfigurationComposite} and adds it to the parent.
    347      *
    348      * @param listener An {@link IConfigListener} that gets and sets configuration properties.
    349      *          Mandatory, cannot be null.
    350      * @param customToggles An array of {@link CustomToggle} to define extra toggles button
    351      *          to display at the top of the composite. Can be empty or null.
    352      * @param parent The parent composite.
    353      * @param style The style of this composite.
    354      */
    355     public ConfigurationComposite(IConfigListener listener,
    356             CustomToggle[] customToggles,
    357             Composite parent, int style) {
    358         super(parent, style);
    359         mListener = listener;
    360 
    361         if (customToggles == null) {
    362             customToggles = new CustomToggle[0];
    363         }
    364 
    365         GridLayout gl;
    366         GridData gd;
    367         int cols = 9;  // device+config+locale+dock+day/night+separator*2+theme+createBtn
    368 
    369         // ---- First line: custom buttons, clipping button, editing config display.
    370         Composite labelParent = new Composite(this, SWT.NONE);
    371         labelParent.setLayout(gl = new GridLayout(3 + customToggles.length, false));
    372         gl.marginWidth = gl.marginHeight = 0;
    373         labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    374         gd.horizontalSpan = cols;
    375 
    376         new Label(labelParent, SWT.NONE).setText("Editing config:");
    377         mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
    378         mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    379         gd.widthHint = 50;
    380 
    381         for (CustomToggle toggle : customToggles) {
    382             toggle.createToggle(labelParent);
    383         }
    384 
    385         mClippingButton = new Button(labelParent, SWT.TOGGLE | SWT.FLAT);
    386         mClippingButton.setSelection(mClipping);
    387         mClippingButton.setToolTipText("Toggles screen clipping on/off");
    388         mClippingButton.setImage(IconFactory.getInstance().getIcon("clipping")); //$NON-NLS-1$
    389         mClippingButton.addSelectionListener(new SelectionAdapter() {
    390             @Override
    391             public void widgetSelected(SelectionEvent e) {
    392                 onClippingChange();
    393             }
    394         });
    395 
    396         // ---- 2nd line: device/config/locale/theme Combos, create button.
    397 
    398         setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    399         setLayout(gl = new GridLayout(cols, false));
    400         gl.marginHeight = 0;
    401         gl.horizontalSpacing = 0;
    402 
    403         mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
    404         mDeviceCombo.setLayoutData(new GridData(
    405                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    406         mDeviceCombo.addSelectionListener(new SelectionAdapter() {
    407             @Override
    408             public void widgetSelected(SelectionEvent e) {
    409                 onDeviceChange(true /* recomputeLayout*/);
    410             }
    411         });
    412 
    413         mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
    414         mDeviceConfigCombo.setLayoutData(new GridData(
    415                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    416         mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
    417             @Override
    418              public void widgetSelected(SelectionEvent e) {
    419                 onDeviceConfigChange();
    420             }
    421         });
    422 
    423         mLocaleCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
    424         mLocaleCombo.setLayoutData(new GridData(
    425                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    426         mLocaleCombo.addSelectionListener(new SelectionListener() {
    427             public void widgetDefaultSelected(SelectionEvent e) {
    428                 onLocaleChange();
    429             }
    430             public void widgetSelected(SelectionEvent e) {
    431                 onLocaleChange();
    432             }
    433         });
    434 
    435         mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
    436         mDockCombo.setLayoutData(new GridData(
    437                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    438         for (DockMode mode : DockMode.values()) {
    439             mDockCombo.add(mode.getLongDisplayValue());
    440         }
    441         mDockCombo.addSelectionListener(new SelectionListener() {
    442             public void widgetDefaultSelected(SelectionEvent e) {
    443                 onDockChange();
    444             }
    445             public void widgetSelected(SelectionEvent e) {
    446                 onDockChange();
    447             }
    448         });
    449 
    450         mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
    451         mNightCombo.setLayoutData(new GridData(
    452                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    453         for (NightMode mode : NightMode.values()) {
    454             mNightCombo.add(mode.getLongDisplayValue());
    455         }
    456         mNightCombo.addSelectionListener(new SelectionListener() {
    457             public void widgetDefaultSelected(SelectionEvent e) {
    458                 onDayChange();
    459             }
    460             public void widgetSelected(SelectionEvent e) {
    461                 onDayChange();
    462             }
    463         });
    464 
    465         // first separator
    466         Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
    467         separator.setLayoutData(gd = new GridData(
    468                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
    469         gd.heightHint = 0;
    470 
    471         mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
    472         mThemeCombo.setLayoutData(new GridData(
    473                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
    474         mThemeCombo.setEnabled(false);
    475 
    476         mThemeCombo.addSelectionListener(new SelectionAdapter() {
    477             @Override
    478             public void widgetSelected(SelectionEvent e) {
    479                 onThemeChange();
    480             }
    481         });
    482 
    483         // second separator
    484         separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
    485         separator.setLayoutData(gd = new GridData(
    486                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
    487         gd.heightHint = 0;
    488 
    489         mCreateButton = new Button(this, SWT.PUSH | SWT.FLAT);
    490         mCreateButton.setText("Create...");
    491         mCreateButton.setEnabled(false);
    492         mCreateButton.addSelectionListener(new SelectionAdapter() {
    493             @Override
    494             public void widgetSelected(SelectionEvent e) {
    495                 if (mListener != null) {
    496                     mListener.onCreate();
    497                 }
    498             }
    499         });
    500     }
    501 
    502     // ---- Init and reset/reload methods ----
    503 
    504     /**
    505      * Sets the reference to the file being edited.
    506      * <p/>The UI is intialized in {@link #onXmlModelLoaded()} which is called as the XML model is
    507      * loaded (or reloaded as the SDK/target changes).
    508      *
    509      * @param file the file being opened
    510      *
    511      * @see #onXmlModelLoaded()
    512      * @see #replaceFile(FolderConfiguration)
    513      * @see #changeFileOnNewConfig(FolderConfiguration)
    514      */
    515     public void setFile(IFile file) {
    516         mEditedFile = file;
    517     }
    518 
    519     /**
    520      * Replaces the UI with a given file configuration. This is meant to answer the user
    521      * explicitly opening a different version of the same layout from the Package Explorer.
    522      * <p/>This attempts to keep the current config, but may change it if it's not compatible or
    523      * not the best match
    524      * <p/>This will NOT trigger a redraw event (will not call
    525      * {@link IConfigListener#onConfigurationChange()}.)
    526      * @param file the file being opened.
    527      * @param fileConfig The {@link FolderConfiguration} of the opened file.
    528      * @param target the {@link IAndroidTarget} of the file's project.
    529      *
    530      * @see #replaceFile(FolderConfiguration)
    531      */
    532     public void replaceFile(IFile file) {
    533         // if there is no previous selection, revert to default mode.
    534         if (mState.device == null) {
    535             setFile(file); // onTargetChanged will be called later.
    536             return;
    537         }
    538 
    539         mEditedFile = file;
    540         IProject iProject = mEditedFile.getProject();
    541         mResources = ResourceManager.getInstance().getProjectResources(iProject);
    542 
    543         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
    544         mEditedConfig = resFolder.getConfiguration();
    545 
    546         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    547                            // new values in the widgets.
    548 
    549         // only attempt to do anything if the SDK and targets are loaded.
    550         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
    551         if (sdkStatus == LoadStatus.LOADED) {
    552             LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null);
    553 
    554             if (targetStatus == LoadStatus.LOADED) {
    555 
    556                 // update the current config selection to make sure it's
    557                 // compatible with the new file
    558                 adaptConfigSelection(true /*needBestMatch*/);
    559 
    560                 // compute the final current config
    561                 computeCurrentConfig(true /*force*/);
    562 
    563                 // update the string showing the config value
    564                 updateConfigDisplay(mEditedConfig);
    565             }
    566         }
    567 
    568         mDisableUpdates--;
    569     }
    570 
    571     /**
    572      * Updates the UI with a new file that was opened in response to a config change.
    573      * @param file the file being opened.
    574      *
    575      * @see #openFile(FolderConfiguration, IAndroidTarget)
    576      * @see #replaceFile(FolderConfiguration)
    577      */
    578     public void changeFileOnNewConfig(IFile file) {
    579         mEditedFile = file;
    580         IProject iProject = mEditedFile.getProject();
    581         mResources = ResourceManager.getInstance().getProjectResources(iProject);
    582 
    583         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
    584         mEditedConfig = resFolder.getConfiguration();
    585 
    586         // All that's needed is to update the string showing the config value
    587         // (since the config combo were chosen by the user).
    588         updateConfigDisplay(mEditedConfig);
    589     }
    590 
    591     /**
    592      * Responds to the event that the basic SDK information finished loading.
    593      * @param target the possibly new target object associated with the file being edited (in case
    594      * the SDK path was changed).
    595      */
    596     public void onSdkLoaded(IAndroidTarget target) {
    597         // a change to the SDK means that we need to check for new/removed devices.
    598         mSdkChanged = true;
    599 
    600         // store the new target.
    601         mTarget = target;
    602 
    603         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    604                            // new values in the widgets.
    605 
    606         // this is going to be followed by a call to onTargetLoaded.
    607         // So we can only care about the layout devices in this case.
    608         initDevices();
    609 
    610         mDisableUpdates--;
    611     }
    612 
    613     /**
    614      * Answers to the XML model being loaded, either the first time or when the Targget/SDK changes.
    615      * <p>This initializes the UI, either with the first compatible configuration found,
    616      * or attempts to restore a configuration if one is found to have been saved in the file
    617      * persistent storage.
    618      * <p>If the SDK or target are not loaded, nothing will happend (but the method must be called
    619      * back when those are loaded).
    620      * <p>The method automatically handles being called the first time after editor creation, or
    621      * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
    622      * is properly called).
    623      *
    624      * @see #storeState()
    625      * @see #onSdkLoaded(IAndroidTarget)
    626      */
    627     public void onXmlModelLoaded() {
    628         // only attempt to do anything if the SDK and targets are loaded.
    629         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
    630         if (sdkStatus == LoadStatus.LOADED) {
    631             mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    632 
    633             // init the devices if needed (new SDK or first time going through here)
    634             if (mSdkChanged || mFirstXmlModelChange) {
    635                 initDevices();
    636             }
    637 
    638             IProject iProject = mEditedFile.getProject();
    639 
    640             Sdk currentSdk = Sdk.getCurrent();
    641             if (currentSdk != null) {
    642                 mTarget = currentSdk.getTarget(iProject);
    643             }
    644 
    645             LoadStatus targetStatus = LoadStatus.FAILED;
    646             if (mTarget != null) {
    647                 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null);
    648             }
    649 
    650             if (targetStatus == LoadStatus.LOADED) {
    651                 if (mResources == null) {
    652                     mResources = ResourceManager.getInstance().getProjectResources(iProject);
    653                 }
    654                 if (mEditedConfig == null) {
    655                     ResourceFolder resFolder = mResources.getResourceFolder(
    656                             (IFolder) mEditedFile.getParent());
    657                     mEditedConfig = resFolder.getConfiguration();
    658                 }
    659 
    660                 // update the clipping state
    661                 AndroidTargetData targetData = Sdk.getCurrent().getTargetData(mTarget);
    662                 if (targetData != null) {
    663                     LayoutBridge bridge = targetData.getLayoutBridge();
    664                     setClippingSupport(bridge.apiLevel >= 4);
    665                 }
    666 
    667                 // get the file stored state
    668                 boolean loadedConfigData = false;
    669                 try {
    670                     QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE);
    671                     String data = mEditedFile.getPersistentProperty(qname);
    672                     if (data != null) {
    673                         loadedConfigData = mState.setData(data);
    674                     }
    675                 } catch (CoreException e) {
    676                     // pass
    677                 }
    678 
    679                 // update the themes and locales.
    680                 updateThemes();
    681                 updateLocales();
    682 
    683                 // If the current state was loaded from the persistent storage, we update the
    684                 // UI with it and then try to adapt it (which will handle incompatible
    685                 // configuration).
    686                 // Otherwise, just look for the first compatible configuration.
    687                 if (loadedConfigData) {
    688                     // first make sure we have the config to adapt
    689                     selectDevice(mState.device);
    690                     fillConfigCombo(mState.configName);
    691 
    692                     adaptConfigSelection(false /*needBestMatch*/);
    693 
    694                     mDockCombo.select(DockMode.getIndex(mState.dock));
    695                     mNightCombo.select(NightMode.getIndex(mState.night));
    696                 } else {
    697                     findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
    698 
    699                     mDockCombo.select(0);
    700                     mNightCombo.select(0);
    701                 }
    702 
    703                 // update the string showing the config value
    704                 updateConfigDisplay(mEditedConfig);
    705 
    706                 // compute the final current config
    707                 computeCurrentConfig(true /*force*/);
    708             }
    709 
    710             mDisableUpdates--;
    711             mFirstXmlModelChange  = false;
    712         }
    713     }
    714 
    715     /**
    716      * Finds a device/config that can display {@link #mEditedConfig}.
    717      * <p/>Once found the device and config combos are set to the config.
    718      * <p/>If there is no compatible configuration, a custom one is created.
    719      * @param favorCurrentConfig if true, and no best match is found, don't change
    720      * the current config. This must only be true if the current config is compatible.
    721      */
    722     private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
    723         LayoutDevice anyDeviceMatch = null; // a compatible device/config/locale
    724         String anyConfigMatchName = null;
    725         int anyLocaleIndex = -1;
    726 
    727         LayoutDevice bestDeviceMatch = null; // an actual best match
    728         String bestConfigMatchName = null;
    729         int bestLocaleIndex = -1;
    730 
    731         FolderConfiguration testConfig = new FolderConfiguration();
    732 
    733         mainloop: for (LayoutDevice device : mDeviceList) {
    734             for (Entry<String, FolderConfiguration> entry :
    735                     device.getConfigs().entrySet()) {
    736                 testConfig.set(entry.getValue());
    737 
    738                 // look on the locales.
    739                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
    740                     ResourceQualifier[] locale = mLocaleList.get(i);
    741 
    742                     // update the test config with the locale qualifiers
    743                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
    744                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
    745 
    746                     if (mEditedConfig.isMatchFor(testConfig)) {
    747                         // this is a basic match. record it in case we don't find a match
    748                         // where the edited file is a best config.
    749                         if (anyDeviceMatch == null) {
    750                             anyDeviceMatch = device;
    751                             anyConfigMatchName = entry.getKey();
    752                             anyLocaleIndex = i;
    753                         }
    754 
    755                         if (isCurrentFileBestMatchFor(testConfig)) {
    756                             // this is what we want.
    757                             bestDeviceMatch = device;
    758                             bestConfigMatchName = entry.getKey();
    759                             bestLocaleIndex = i;
    760                             break mainloop;
    761                         }
    762                     }
    763                 }
    764             }
    765         }
    766 
    767         if (bestDeviceMatch == null) {
    768             if (favorCurrentConfig) {
    769                 // quick check
    770                 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
    771                     AdtPlugin.log(IStatus.ERROR,
    772                             "favorCurrentConfig can only be true if the current config is compatible");
    773                 }
    774 
    775                 // just display the warning
    776                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
    777                         String.format(
    778                                 "'%1$s' is not a best match for any device/locale combination.",
    779                                 mEditedConfig.toDisplayString()),
    780                         String.format(
    781                                 "Displaying it with '%1$s'",
    782                                 mCurrentConfig.toDisplayString()));
    783             } else if (anyDeviceMatch != null) {
    784                 // select the device anyway.
    785                 selectDevice(mState.device = anyDeviceMatch);
    786                 fillConfigCombo(anyConfigMatchName);
    787                 mLocaleCombo.select(anyLocaleIndex);
    788 
    789                 // TODO: display a better warning!
    790                 computeCurrentConfig(false /*force*/);
    791                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
    792                         String.format(
    793                                 "'%1$s' is not a best match for any device/locale combination.",
    794                                 mEditedConfig.toDisplayString()),
    795                         String.format(
    796                                 "Displaying it with '%1$s'",
    797                                 mCurrentConfig.toDisplayString()));
    798 
    799             } else {
    800                 // TODO: there is no device/config able to display the layout, create one.
    801                 // For the base config values, we'll take the first device and config,
    802                 // and replace whatever qualifier required by the layout file.
    803             }
    804         } else {
    805             selectDevice(mState.device = bestDeviceMatch);
    806             fillConfigCombo(bestConfigMatchName);
    807             mLocaleCombo.select(bestLocaleIndex);
    808         }
    809     }
    810 
    811     /**
    812      * Adapts the current device/config selection so that it's compatible with
    813      * {@link #mEditedConfig}.
    814      * <p/>If the current selection is compatible, nothing is changed.
    815      * <p/>If it's not compatible, configs from the current devices are tested.
    816      * <p/>If none are compatible, it reverts to
    817      * {@link #findAndSetCompatibleConfig(FolderConfiguration)}
    818      */
    819     private void adaptConfigSelection(boolean needBestMatch) {
    820         // check the device config (ie sans locale)
    821         boolean needConfigChange = true; // if still true, we need to find another config.
    822         boolean currentConfigIsCompatible = false;
    823         int configIndex = mDeviceConfigCombo.getSelectionIndex();
    824         if (configIndex != -1) {
    825             String configName = mDeviceConfigCombo.getItem(configIndex);
    826             FolderConfiguration currentConfig = mState.device.getConfigs().get(configName);
    827             if (mEditedConfig.isMatchFor(currentConfig)) {
    828                 currentConfigIsCompatible = true; // current config is compatible
    829                 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
    830                     needConfigChange = false;
    831                 }
    832             }
    833         }
    834 
    835         if (needConfigChange) {
    836             // if the current config/locale isn't a correct match, then
    837             // look for another config/locale in the same device.
    838             FolderConfiguration testConfig = new FolderConfiguration();
    839 
    840             // first look in the current device.
    841             String matchName = null;
    842             int localeIndex = -1;
    843             Map<String, FolderConfiguration> configs = mState.device.getConfigs();
    844             mainloop: for (Entry<String, FolderConfiguration> entry : configs.entrySet()) {
    845                 testConfig.set(entry.getValue());
    846 
    847                 // loop on the locales.
    848                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
    849                     ResourceQualifier[] locale = mLocaleList.get(i);
    850 
    851                     // update the test config with the locale qualifiers
    852                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
    853                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
    854 
    855                     if (mEditedConfig.isMatchFor(testConfig) &&
    856                             isCurrentFileBestMatchFor(testConfig)) {
    857                         matchName = entry.getKey();
    858                         localeIndex = i;
    859                         break mainloop;
    860                     }
    861                 }
    862             }
    863 
    864             if (matchName != null) {
    865                 selectConfig(matchName);
    866                 mLocaleCombo.select(localeIndex);
    867             } else {
    868                 // no match in current device with any config/locale
    869                 // attempt to find another device that can display this particular config.
    870                 findAndSetCompatibleConfig(currentConfigIsCompatible);
    871             }
    872         }
    873     }
    874 
    875     /**
    876      * Finds a locale matching the config from a file.
    877      * @param language the language qualifier or null if none is set.
    878      * @param region the region qualifier or null if none is set.
    879      */
    880     private void setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
    881         // find the locale match. Since the locale list is based on the content of the
    882         // project resources there must be an exact match.
    883         // The only trick is that the region could be null in the fileConfig but in our
    884         // list of locales, this is represented as a RegionQualifier with value of
    885         // FAKE_LOCALE_VALUE.
    886         final int count = mLocaleList.size();
    887         for (int i = 0 ; i < count ; i++) {
    888             ResourceQualifier[] locale = mLocaleList.get(i);
    889 
    890             // the language qualifier in the locale list is never null.
    891             if (locale[LOCALE_LANG].equals(language)) {
    892                 // region comparison is more complex, as the region could be null.
    893                 if (region == null) {
    894                     if (RegionQualifier.FAKE_REGION_VALUE.equals(
    895                             ((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
    896                         // match!
    897                         mLocaleCombo.select(i);
    898                         break;
    899                     }
    900                 } else if (region.equals(locale[LOCALE_REGION])) {
    901                     // match!
    902                     mLocaleCombo.select(i);
    903                     break;
    904                 }
    905             }
    906         }
    907     }
    908 
    909     private void updateConfigDisplay(FolderConfiguration fileConfig) {
    910         String current = fileConfig.toDisplayString();
    911         mCurrentLayoutLabel.setText(current != null ? current : "(Default)");
    912     }
    913 
    914     private void saveState(boolean force) {
    915         if (mDisableUpdates == 0) {
    916             int index = mDeviceConfigCombo.getSelectionIndex();
    917             if (index != -1) {
    918                 mState.configName = mDeviceConfigCombo.getItem(index);
    919             } else {
    920                 mState.configName = null;
    921             }
    922 
    923             // since the locales are relative to the project, only keeping the index is enough
    924             index = mLocaleCombo.getSelectionIndex();
    925             if (index != -1) {
    926                 mState.locale = mLocaleList.get(index);
    927             } else {
    928                 mState.locale = null;
    929             }
    930 
    931             index = mThemeCombo.getSelectionIndex();
    932             if (index != -1) {
    933                 mState.theme = mThemeCombo.getItem(index);
    934             }
    935 
    936             index = mDockCombo.getSelectionIndex();
    937             if (index != -1) {
    938                 mState.dock = DockMode.getByIndex(index);
    939             }
    940 
    941             index = mNightCombo.getSelectionIndex();
    942             if (index != -1) {
    943                 mState.night = NightMode.getByIndex(index);
    944             }
    945         }
    946     }
    947 
    948     /**
    949      * Stores the current config selection into the edited file.
    950      */
    951     public void storeState() {
    952         try {
    953             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE); //$NON-NLS-1$
    954             mEditedFile.setPersistentProperty(qname, mState.getData());
    955         } catch (CoreException e) {
    956             // pass
    957         }
    958     }
    959 
    960     /**
    961      * Updates the locale combo.
    962      * This must be called from the UI thread.
    963      */
    964     public void updateLocales() {
    965         if (mListener == null) {
    966             return; // can't do anything w/o it.
    967         }
    968 
    969         mDisableUpdates++;
    970 
    971         // Reset the combo
    972         mLocaleCombo.removeAll();
    973         mLocaleList.clear();
    974 
    975         SortedSet<String> languages = null;
    976         boolean hasLocale = false;
    977 
    978         // get the languages from the project.
    979         ProjectResources project = mListener.getProjectResources();
    980 
    981         // in cases where the opened file is not linked to a project, this could be null.
    982         if (project != null) {
    983             // now get the languages from the project.
    984             languages = project.getLanguages();
    985 
    986             for (String language : languages) {
    987                 hasLocale = true;
    988 
    989                 LanguageQualifier langQual = new LanguageQualifier(language);
    990 
    991                 // find the matching regions and add them
    992                 SortedSet<String> regions = project.getRegions(language);
    993                 for (String region : regions) {
    994                     mLocaleCombo.add(String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$
    995                     RegionQualifier regionQual = new RegionQualifier(region);
    996                     mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
    997                 }
    998 
    999                 // now the entry for the other regions the language alone
   1000                 if (regions.size() > 0) {
   1001                     mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$
   1002                 } else {
   1003                     mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$
   1004                 }
   1005                 // create a region qualifier that will never be matched by qualified resources.
   1006                 mLocaleList.add(new ResourceQualifier[] {
   1007                         langQual,
   1008                         new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
   1009                 });
   1010             }
   1011         }
   1012 
   1013         // add a locale not present in the project resources. This will let the dev
   1014         // tests his/her default values.
   1015         if (hasLocale) {
   1016             mLocaleCombo.add("Other");
   1017         } else {
   1018             mLocaleCombo.add("Any locale");
   1019         }
   1020 
   1021         // create language/region qualifier that will never be matched by qualified resources.
   1022         mLocaleList.add(new ResourceQualifier[] {
   1023                 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
   1024                 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
   1025         });
   1026 
   1027         if (mState.locale != null) {
   1028             // FIXME: this may fails if the layout was deleted (and was the last one to have that local.
   1029             // (we have other problem in this case though)
   1030             setLocaleCombo(mState.locale[LOCALE_LANG],
   1031                     mState.locale[LOCALE_REGION]);
   1032         } else {
   1033             mLocaleCombo.select(0);
   1034         }
   1035 
   1036         mThemeCombo.getParent().layout();
   1037 
   1038         mDisableUpdates--;
   1039     }
   1040 
   1041     /**
   1042      * Updates the theme combo.
   1043      * This must be called from the UI thread.
   1044      */
   1045     private void updateThemes() {
   1046         if (mListener == null) {
   1047             return; // can't do anything w/o it.
   1048         }
   1049 
   1050         ProjectResources frameworkProject = mListener.getFrameworkResources();
   1051 
   1052         mDisableUpdates++;
   1053 
   1054         // Reset the combo
   1055         mThemeCombo.removeAll();
   1056         mPlatformThemeCount = 0;
   1057 
   1058         ArrayList<String> themes = new ArrayList<String>();
   1059 
   1060         // get the themes, and languages from the Framework.
   1061         if (frameworkProject != null) {
   1062             // get the configured resources for the framework
   1063             Map<String, Map<String, IResourceValue>> frameworResources =
   1064                 mListener.getConfiguredFrameworkResources();
   1065 
   1066             if (frameworResources != null) {
   1067                 // get the styles.
   1068                 Map<String, IResourceValue> styles = frameworResources.get(
   1069                         ResourceType.STYLE.getName());
   1070 
   1071 
   1072                 // collect the themes out of all the styles.
   1073                 for (IResourceValue value : styles.values()) {
   1074                     String name = value.getName();
   1075                     if (name.startsWith("Theme.") || name.equals("Theme")) {
   1076                         themes.add(value.getName());
   1077                         mPlatformThemeCount++;
   1078                     }
   1079                 }
   1080 
   1081                 // sort them and add them to the combo
   1082                 Collections.sort(themes);
   1083 
   1084                 for (String theme : themes) {
   1085                     mThemeCombo.add(theme);
   1086                 }
   1087 
   1088                 mPlatformThemeCount = themes.size();
   1089                 themes.clear();
   1090             }
   1091         }
   1092 
   1093         // now get the themes and languages from the project.
   1094         ProjectResources project = mListener.getProjectResources();
   1095         // in cases where the opened file is not linked to a project, this could be null.
   1096         if (project != null) {
   1097             // get the configured resources for the project
   1098             Map<String, Map<String, IResourceValue>> configuredProjectRes =
   1099                 mListener.getConfiguredProjectResources();
   1100 
   1101             if (configuredProjectRes != null) {
   1102                 // get the styles.
   1103                 Map<String, IResourceValue> styleMap = configuredProjectRes.get(
   1104                         ResourceType.STYLE.getName());
   1105 
   1106                 if (styleMap != null) {
   1107                     // collect the themes out of all the styles, ie styles that extend,
   1108                     // directly or indirectly a platform theme.
   1109                     for (IResourceValue value : styleMap.values()) {
   1110                         if (isTheme(value, styleMap)) {
   1111                             themes.add(value.getName());
   1112                         }
   1113                     }
   1114 
   1115                     // sort them and add them the to the combo.
   1116                     if (mPlatformThemeCount > 0 && themes.size() > 0) {
   1117                         mThemeCombo.add(THEME_SEPARATOR);
   1118                     }
   1119 
   1120                     Collections.sort(themes);
   1121 
   1122                     for (String theme : themes) {
   1123                         mThemeCombo.add(theme);
   1124                     }
   1125                 }
   1126             }
   1127         }
   1128 
   1129         // try to reselect the previous theme.
   1130         if (mState.theme != null) {
   1131             final int count = mThemeCombo.getItemCount();
   1132             for (int i = 0 ; i < count ; i++) {
   1133                 if (mState.theme.equals(mThemeCombo.getItem(i))) {
   1134                     mThemeCombo.select(i);
   1135                     break;
   1136                 }
   1137             }
   1138             mThemeCombo.setEnabled(true);
   1139         } else if (mThemeCombo.getItemCount() > 0) {
   1140             mThemeCombo.select(0);
   1141             mThemeCombo.setEnabled(true);
   1142         } else {
   1143             mThemeCombo.setEnabled(false);
   1144         }
   1145 
   1146         mThemeCombo.getParent().layout();
   1147 
   1148         mDisableUpdates--;
   1149     }
   1150 
   1151     // ---- getters for the config selection values ----
   1152 
   1153     public FolderConfiguration getEditedConfig() {
   1154         return mEditedConfig;
   1155     }
   1156 
   1157     public FolderConfiguration getCurrentConfig() {
   1158         return mCurrentConfig;
   1159     }
   1160 
   1161     public void getCurrentConfig(FolderConfiguration config) {
   1162         config.set(mCurrentConfig);
   1163     }
   1164 
   1165     /**
   1166      * Returns the currently selected {@link Density}. This is guaranteed to be non null.
   1167      */
   1168     public Density getDensity() {
   1169         if (mCurrentConfig != null) {
   1170             PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier();
   1171             if (qual != null) {
   1172                 // just a sanity check
   1173                 Density d = qual.getValue();
   1174                 if (d != Density.NODPI) {
   1175                     return d;
   1176                 }
   1177             }
   1178         }
   1179 
   1180         // no config? return medium as the default density.
   1181         return Density.MEDIUM;
   1182     }
   1183 
   1184     /**
   1185      * Returns the current device xdpi.
   1186      */
   1187     public float getXDpi() {
   1188         if (mState.device != null) {
   1189             float dpi = mState.device.getXDpi();
   1190             if (Float.isNaN(dpi) == false) {
   1191                 return dpi;
   1192             }
   1193         }
   1194 
   1195         // get the pixel density as the density.
   1196         return getDensity().getDpiValue();
   1197     }
   1198 
   1199     /**
   1200      * Returns the current device ydpi.
   1201      */
   1202     public float getYDpi() {
   1203         if (mState.device != null) {
   1204             float dpi = mState.device.getYDpi();
   1205             if (Float.isNaN(dpi) == false) {
   1206                 return dpi;
   1207             }
   1208         }
   1209 
   1210         // get the pixel density as the density.
   1211         return getDensity().getDpiValue();
   1212     }
   1213 
   1214     public Rectangle getScreenBounds() {
   1215         // get the orientation from the current device config
   1216         ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
   1217         ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
   1218         if (qual != null) {
   1219             orientation = qual.getValue();
   1220         }
   1221 
   1222         // get the device screen dimension
   1223         ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
   1224         int s1, s2;
   1225         if (qual2 != null) {
   1226             s1 = qual2.getValue1();
   1227             s2 = qual2.getValue2();
   1228         } else {
   1229             s1 = 480;
   1230             s2 = 320;
   1231         }
   1232 
   1233         switch (orientation) {
   1234             default:
   1235             case PORTRAIT:
   1236                 return new Rectangle(0, 0, s2, s1);
   1237             case LANDSCAPE:
   1238                 return new Rectangle(0, 0, s1, s2);
   1239             case SQUARE:
   1240                 return new Rectangle(0, 0, s1, s1);
   1241         }
   1242     }
   1243 
   1244 
   1245     /**
   1246      * Returns the current theme, or null if the combo has no selection.
   1247      */
   1248     public String getTheme() {
   1249         int themeIndex = mThemeCombo.getSelectionIndex();
   1250         if (themeIndex != -1) {
   1251             return mThemeCombo.getItem(themeIndex);
   1252         }
   1253 
   1254         return null;
   1255     }
   1256 
   1257     /**
   1258      * Returns whether the current theme selection is a project theme.
   1259      * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
   1260      * @return true for project theme, false for framework theme
   1261      */
   1262     public boolean isProjectTheme() {
   1263         return mThemeCombo.getSelectionIndex() >= mPlatformThemeCount;
   1264     }
   1265 
   1266     public boolean getClipping() {
   1267         return mClipping;
   1268     }
   1269 
   1270     private void setClippingSupport(boolean b) {
   1271         mClippingButton.setEnabled(b);
   1272         if (b) {
   1273             mClippingButton.setToolTipText("Toggles screen clipping on/off");
   1274         } else {
   1275             mClipping = true;
   1276             mClippingButton.setSelection(true);
   1277             mClippingButton.setToolTipText("Non clipped rendering is not supported");
   1278         }
   1279     }
   1280 
   1281     /**
   1282      * Loads the list of {@link LayoutDevice} and inits the UI with it.
   1283      */
   1284     private void initDevices() {
   1285         mDeviceList = null;
   1286 
   1287         Sdk sdk = Sdk.getCurrent();
   1288         if (sdk != null) {
   1289             LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
   1290             mDeviceList = manager.getCombinedList();
   1291         }
   1292 
   1293 
   1294         // remove older devices if applicable
   1295         mDeviceCombo.removeAll();
   1296         mDeviceConfigCombo.removeAll();
   1297 
   1298         // fill with the devices
   1299         if (mDeviceList != null) {
   1300             for (LayoutDevice device : mDeviceList) {
   1301                 mDeviceCombo.add(device.getName());
   1302             }
   1303             mDeviceCombo.select(0);
   1304 
   1305             if (mDeviceList.size() > 0) {
   1306                 Map<String, FolderConfiguration> configs = mDeviceList.get(0).getConfigs();
   1307                 Set<String> configNames = configs.keySet();
   1308                 for (String name : configNames) {
   1309                     mDeviceConfigCombo.add(name);
   1310                 }
   1311                 mDeviceConfigCombo.select(0);
   1312                 if (configNames.size() == 1) {
   1313                     mDeviceConfigCombo.setEnabled(false);
   1314                 }
   1315             }
   1316         }
   1317 
   1318         // add the custom item
   1319         mDeviceCombo.add("Custom...");
   1320     }
   1321 
   1322     /**
   1323      * Selects a given {@link LayoutDevice} in the device combo, if it is found.
   1324      * @param device the device to select
   1325      * @return true if the device was found.
   1326      */
   1327     private boolean selectDevice(LayoutDevice device) {
   1328         final int count = mDeviceList.size();
   1329         for (int i = 0 ; i < count ; i++) {
   1330             // since device comes from mDeviceList, we can use the == operator.
   1331             if (device == mDeviceList.get(i)) {
   1332                 mDeviceCombo.select(i);
   1333                 return true;
   1334             }
   1335         }
   1336 
   1337         return false;
   1338     }
   1339 
   1340     /**
   1341      * Selects a config by name.
   1342      * @param name the name of the config to select.
   1343      */
   1344     private void selectConfig(String name) {
   1345         final int count = mDeviceConfigCombo.getItemCount();
   1346         for (int i = 0 ; i < count ; i++) {
   1347             String item = mDeviceConfigCombo.getItem(i);
   1348             if (name.equals(item)) {
   1349                 mDeviceConfigCombo.select(i);
   1350                 return;
   1351             }
   1352         }
   1353     }
   1354 
   1355     /**
   1356      * Called when the selection of the device combo changes.
   1357      * @param recomputeLayout
   1358      */
   1359     private void onDeviceChange(boolean recomputeLayout) {
   1360         // because changing the content of a combo triggers a change event, respect the
   1361         // mDisableUpdates flag
   1362         if (mDisableUpdates > 0) {
   1363             return;
   1364         }
   1365 
   1366         String newConfigName = null;
   1367 
   1368         int deviceIndex = mDeviceCombo.getSelectionIndex();
   1369         if (deviceIndex != -1) {
   1370             // check if the user is asking for the custom item
   1371             if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
   1372                 onCustomDeviceConfig();
   1373                 return;
   1374             }
   1375 
   1376             // get the previous config, so that we can look for a close match
   1377             if (mState.device != null) {
   1378                 int index = mDeviceConfigCombo.getSelectionIndex();
   1379                 if (index != -1) {
   1380                     FolderConfiguration oldConfig = mState.device.getConfigs().get(
   1381                             mDeviceConfigCombo.getItem(index));
   1382 
   1383                     LayoutDevice newDevice = mDeviceList.get(deviceIndex);
   1384 
   1385                     newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
   1386                 }
   1387             }
   1388 
   1389             mState.device = mDeviceList.get(deviceIndex);
   1390         } else {
   1391             mState.device = null;
   1392         }
   1393 
   1394         fillConfigCombo(newConfigName);
   1395 
   1396         computeCurrentConfig(false /*force*/);
   1397 
   1398         if (recomputeLayout) {
   1399             onDeviceConfigChange();
   1400         }
   1401     }
   1402 
   1403     /**
   1404      * Handles a user request for the {@link ConfigManagerDialog}.
   1405      */
   1406     private void onCustomDeviceConfig() {
   1407         ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
   1408         dialog.open();
   1409 
   1410         // save the user devices
   1411         Sdk.getCurrent().getLayoutDeviceManager().save();
   1412 
   1413         // Update the UI with no triggered event
   1414         mDisableUpdates++;
   1415 
   1416         LayoutDevice oldCurrent = mState.device;
   1417 
   1418         // but first, update the device combo
   1419         initDevices();
   1420 
   1421         // attempts to reselect the current device.
   1422         if (selectDevice(oldCurrent)) {
   1423             // current device still exists.
   1424             // reselect the config
   1425             selectConfig(mState.configName);
   1426 
   1427             // reset the UI as if it was just a replacement file, since we can keep
   1428             // the current device (and possibly config).
   1429             adaptConfigSelection(false /*needBestMatch*/);
   1430 
   1431         } else {
   1432             // find a new device/config to match the current file.
   1433             findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
   1434         }
   1435 
   1436         mDisableUpdates--;
   1437 
   1438         // recompute the current config
   1439         computeCurrentConfig(false /*force*/);
   1440 
   1441         // force a redraw
   1442         onDeviceChange(true /*recomputeLayout*/);
   1443     }
   1444 
   1445     /**
   1446      * Attempts to find a close config among a list
   1447      * @param oldConfig the reference config.
   1448      * @param configs the list of config to search through
   1449      * @return the name of the closest config match, or possibly null if no configs are compatible
   1450      * (this can only happen if the configs don't have a single qualifier that is the same).
   1451      */
   1452     private String getClosestMatch(FolderConfiguration oldConfig,
   1453             Map<String, FolderConfiguration> configs) {
   1454 
   1455         // create 2 lists as we're going to go through one and put the candidates in the other.
   1456         ArrayList<Entry<String, FolderConfiguration>> list1 =
   1457             new ArrayList<Entry<String,FolderConfiguration>>();
   1458         ArrayList<Entry<String, FolderConfiguration>> list2 =
   1459             new ArrayList<Entry<String,FolderConfiguration>>();
   1460 
   1461         list1.addAll(configs.entrySet());
   1462 
   1463         final int count = FolderConfiguration.getQualifierCount();
   1464         for (int i = 0 ; i < count ; i++) {
   1465             // compute the new candidate list by only taking configs that have
   1466             // the same i-th qualifier as the old config
   1467             for (Entry<String, FolderConfiguration> entry : list1) {
   1468                 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
   1469 
   1470                 FolderConfiguration config = entry.getValue();
   1471                 ResourceQualifier newQualifier = config.getQualifier(i);
   1472 
   1473                 if (oldQualifier == null) {
   1474                     if (newQualifier == null) {
   1475                         list2.add(entry);
   1476                     }
   1477                 } else if (oldQualifier.equals(newQualifier)) {
   1478                     list2.add(entry);
   1479                 }
   1480             }
   1481 
   1482             // at any moment if the new candidate list contains only one match, its name
   1483             // is returned.
   1484             if (list2.size() == 1) {
   1485                 return list2.get(0).getKey();
   1486             }
   1487 
   1488             // if the list is empty, then all the new configs failed. It is considered ok, and
   1489             // we move to the next qualifier anyway. This way, if a qualifier is different for
   1490             // all new configs it is simply ignored.
   1491             if (list2.size() != 0) {
   1492                 // move the candidates back into list1.
   1493                 list1.clear();
   1494                 list1.addAll(list2);
   1495                 list2.clear();
   1496             }
   1497         }
   1498 
   1499         // the only way to reach this point is if there's an exact match.
   1500         // (if there are more than one, then there's a duplicate config and it doesn't matter,
   1501         // we take the first one).
   1502         if (list1.size() > 0) {
   1503             return list1.get(0).getKey();
   1504         }
   1505 
   1506         return null;
   1507     }
   1508 
   1509     /**
   1510      * fills the config combo with new values based on {@link #mCurrentState#device}.
   1511      * @param refName an optional name. if set the selection will match this name (if found)
   1512      */
   1513     private void fillConfigCombo(String refName) {
   1514         mDeviceConfigCombo.removeAll();
   1515 
   1516         if (mState.device != null) {
   1517             Set<String> configNames = mState.device.getConfigs().keySet();
   1518 
   1519             int selectionIndex = 0;
   1520             int i = 0;
   1521 
   1522             for (String name : configNames) {
   1523                 mDeviceConfigCombo.add(name);
   1524 
   1525                 if (name.equals(refName)) {
   1526                     selectionIndex = i;
   1527                 }
   1528                 i++;
   1529             }
   1530 
   1531             mDeviceConfigCombo.select(selectionIndex);
   1532             mDeviceConfigCombo.setEnabled(configNames.size() > 1);
   1533         }
   1534     }
   1535 
   1536     /**
   1537      * Called when the device config selection changes.
   1538      */
   1539     private void onDeviceConfigChange() {
   1540         // because changing the content of a combo triggers a change event, respect the
   1541         // mDisableUpdates flag
   1542         if (mDisableUpdates > 0) {
   1543             return;
   1544         }
   1545 
   1546         if (computeCurrentConfig(false /*force*/) && mListener != null) {
   1547             mListener.onConfigurationChange();
   1548         }
   1549     }
   1550 
   1551     /**
   1552      * Call back for language combo selection
   1553      */
   1554     private void onLocaleChange() {
   1555         // because mLocaleList triggers onLanguageChange at each modification, the filling
   1556         // of the combo with data will trigger notifications, and we don't want that.
   1557         if (mDisableUpdates > 0) {
   1558             return;
   1559         }
   1560 
   1561         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
   1562             mListener.onConfigurationChange();
   1563         }
   1564     }
   1565 
   1566     private void onDockChange() {
   1567         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
   1568             mListener.onConfigurationChange();
   1569         }
   1570     }
   1571 
   1572     private void onDayChange() {
   1573         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
   1574             mListener.onConfigurationChange();
   1575         }
   1576     }
   1577 
   1578     /**
   1579      * Saves the current state and the current configuration
   1580      * @param force forces saving the states even if updates are disabled
   1581      *
   1582      * @see #saveState(boolean)
   1583      */
   1584     private boolean computeCurrentConfig(boolean force) {
   1585         saveState(force);
   1586 
   1587         if (mState.device != null) {
   1588             // get the device config from the device/config combos.
   1589             int configIndex = mDeviceConfigCombo.getSelectionIndex();
   1590             String name = mDeviceConfigCombo.getItem(configIndex);
   1591             FolderConfiguration config = mState.device.getConfigs().get(name);
   1592 
   1593             // replace the config with the one from the device
   1594             mCurrentConfig.set(config);
   1595 
   1596             // replace the locale qualifiers with the one coming from the locale combo
   1597             int localeIndex = mLocaleCombo.getSelectionIndex();
   1598             if (localeIndex != -1) {
   1599                 ResourceQualifier[] localeQualifiers = mLocaleList.get(localeIndex);
   1600 
   1601                 mCurrentConfig.setLanguageQualifier(
   1602                         (LanguageQualifier)localeQualifiers[LOCALE_LANG]);
   1603                 mCurrentConfig.setRegionQualifier(
   1604                         (RegionQualifier)localeQualifiers[LOCALE_REGION]);
   1605             }
   1606 
   1607             int index = mDockCombo.getSelectionIndex();
   1608             if (index == -1) {
   1609                 index = 0; // no selection = 0
   1610             }
   1611             mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index)));
   1612 
   1613             index = mNightCombo.getSelectionIndex();
   1614             if (index == -1) {
   1615                 index = 0; // no selection = 0
   1616             }
   1617             mCurrentConfig.setNightModeQualifier(
   1618                     new NightModeQualifier(NightMode.getByIndex(index)));
   1619 
   1620             // update the create button.
   1621             checkCreateEnable();
   1622 
   1623             return true;
   1624         }
   1625 
   1626         return false;
   1627     }
   1628 
   1629     private void onThemeChange() {
   1630         saveState(false /*force*/);
   1631 
   1632         int themeIndex = mThemeCombo.getSelectionIndex();
   1633         if (themeIndex != -1) {
   1634             String theme = mThemeCombo.getItem(themeIndex);
   1635 
   1636             if (theme.equals(THEME_SEPARATOR)) {
   1637                 mThemeCombo.select(0);
   1638             }
   1639 
   1640             if (mListener != null) {
   1641                 mListener.onThemeChange();
   1642             }
   1643         }
   1644     }
   1645 
   1646     private void onClippingChange() {
   1647         mClipping = mClippingButton.getSelection();
   1648         if (mListener != null) {
   1649             mListener.onClippingChange();
   1650         }
   1651     }
   1652 
   1653     /**
   1654      * Returns whether the given <var>style</var> is a theme.
   1655      * This is done by making sure the parent is a theme.
   1656      * @param value the style to check
   1657      * @param styleMap the map of styles for the current project. Key is the style name.
   1658      * @return True if the given <var>style</var> is a theme.
   1659      */
   1660     private boolean isTheme(IResourceValue value, Map<String, IResourceValue> styleMap) {
   1661         if (value instanceof IStyleResourceValue) {
   1662             IStyleResourceValue style = (IStyleResourceValue)value;
   1663 
   1664             boolean frameworkStyle = false;
   1665             String parentStyle = style.getParentStyle();
   1666             if (parentStyle == null) {
   1667                 // if there is no specified parent style we look an implied one.
   1668                 // For instance 'Theme.light' is implied child style of 'Theme',
   1669                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
   1670                 String name = style.getName();
   1671                 int index = name.lastIndexOf('.');
   1672                 if (index != -1) {
   1673                     parentStyle = name.substring(0, index);
   1674                 }
   1675             } else {
   1676                 // remove the useless @ if it's there
   1677                 if (parentStyle.startsWith("@")) {
   1678                     parentStyle = parentStyle.substring(1);
   1679                 }
   1680 
   1681                 // check for framework identifier.
   1682                 if (parentStyle.startsWith("android:")) {
   1683                     frameworkStyle = true;
   1684                     parentStyle = parentStyle.substring("android:".length());
   1685                 }
   1686 
   1687                 // at this point we could have the format style/<name>. we want only the name
   1688                 if (parentStyle.startsWith("style/")) {
   1689                     parentStyle = parentStyle.substring("style/".length());
   1690                 }
   1691             }
   1692 
   1693             if (parentStyle != null) {
   1694                 if (frameworkStyle) {
   1695                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
   1696                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
   1697                 } else {
   1698                     // if it's a project style, we check this is a theme.
   1699                     value = styleMap.get(parentStyle);
   1700                     if (value != null) {
   1701                         return isTheme(value, styleMap);
   1702                     }
   1703                 }
   1704             }
   1705         }
   1706 
   1707         return false;
   1708     }
   1709 
   1710     private void checkCreateEnable() {
   1711         mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
   1712     }
   1713 
   1714     /**
   1715      * Checks whether the current edited file is the best match for a given config.
   1716      * <p/>
   1717      * This tests against other versions of the same layout in the project.
   1718      * <p/>
   1719      * The given config must be compatible with the current edited file.
   1720      * @param config the config to test.
   1721      * @return true if the current edited file is the best match in the project for the
   1722      * given config.
   1723      */
   1724     private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
   1725         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
   1726                 ResourceFolderType.LAYOUT, config);
   1727 
   1728         if (match != null) {
   1729             return match.getFile().equals(mEditedFile);
   1730         } else {
   1731             // if we stop here that means the current file is not even a match!
   1732             AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
   1733         }
   1734 
   1735         return false;
   1736     }
   1737 }
   1738 
   1739