Home | History | Annotate | Download | only in configuration
      1 /*
      2  * Copyright (C) 2012 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.SdkConstants.ANDROID_NS_NAME_PREFIX;
     20 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
     21 import static com.android.SdkConstants.ATTR_CONTEXT;
     22 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     23 import static com.android.SdkConstants.RES_QUALIFIER_SEP;
     24 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
     25 import static com.android.SdkConstants.TOOLS_URI;
     26 import static com.android.ide.eclipse.adt.AdtUtils.isUiThread;
     27 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
     28 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
     29 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER;
     30 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_LOCALE;
     31 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET;
     32 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_THEME;
     33 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
     34 import static com.google.common.base.Objects.equal;
     35 
     36 import com.android.annotations.NonNull;
     37 import com.android.annotations.Nullable;
     38 import com.android.ide.common.rendering.api.ResourceValue;
     39 import com.android.ide.common.rendering.api.StyleResourceValue;
     40 import com.android.ide.common.resources.LocaleManager;
     41 import com.android.ide.common.resources.ResourceFile;
     42 import com.android.ide.common.resources.ResourceFolder;
     43 import com.android.ide.common.resources.ResourceRepository;
     44 import com.android.ide.common.resources.configuration.DeviceConfigHelper;
     45 import com.android.ide.common.resources.configuration.FolderConfiguration;
     46 import com.android.ide.common.resources.configuration.LanguageQualifier;
     47 import com.android.ide.common.resources.configuration.RegionQualifier;
     48 import com.android.ide.common.resources.configuration.ResourceQualifier;
     49 import com.android.ide.common.sdk.LoadStatus;
     50 import com.android.ide.eclipse.adt.AdtPlugin;
     51 import com.android.ide.eclipse.adt.AdtUtils;
     52 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     53 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
     54 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
     55 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     56 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     57 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     58 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
     59 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
     60 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     61 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
     62 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
     63 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     64 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     65 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     66 import com.android.resources.ResourceType;
     67 import com.android.resources.ScreenOrientation;
     68 import com.android.sdklib.AndroidVersion;
     69 import com.android.sdklib.IAndroidTarget;
     70 import com.android.sdklib.devices.Device;
     71 import com.android.sdklib.devices.DeviceManager;
     72 import com.android.sdklib.devices.DeviceManager.DevicesChangedListener;
     73 import com.android.sdklib.devices.State;
     74 import com.android.utils.Pair;
     75 import com.google.common.base.Objects;
     76 import com.google.common.base.Strings;
     77 
     78 import org.eclipse.core.resources.IFile;
     79 import org.eclipse.core.resources.IFolder;
     80 import org.eclipse.core.resources.IProject;
     81 import org.eclipse.jface.resource.ImageDescriptor;
     82 import org.eclipse.swt.SWT;
     83 import org.eclipse.swt.events.DisposeEvent;
     84 import org.eclipse.swt.events.DisposeListener;
     85 import org.eclipse.swt.events.SelectionAdapter;
     86 import org.eclipse.swt.events.SelectionEvent;
     87 import org.eclipse.swt.events.SelectionListener;
     88 import org.eclipse.swt.graphics.Image;
     89 import org.eclipse.swt.graphics.Point;
     90 import org.eclipse.swt.layout.GridData;
     91 import org.eclipse.swt.layout.GridLayout;
     92 import org.eclipse.swt.widgets.Composite;
     93 import org.eclipse.swt.widgets.ToolBar;
     94 import org.eclipse.swt.widgets.ToolItem;
     95 import org.eclipse.ui.IEditorPart;
     96 import org.w3c.dom.Document;
     97 import org.w3c.dom.Element;
     98 
     99 import java.util.ArrayList;
    100 import java.util.Collection;
    101 import java.util.Collections;
    102 import java.util.IdentityHashMap;
    103 import java.util.List;
    104 import java.util.Map;
    105 import java.util.SortedSet;
    106 
    107 /**
    108  * The {@linkplain ConfigurationChooser} allows the user to pick a
    109  * {@link Configuration} by configuring various constraints.
    110  */
    111 public class ConfigurationChooser extends Composite
    112         implements DevicesChangedListener, DisposeListener {
    113     private static final String ICON_SQUARE = "square";           //$NON-NLS-1$
    114     private static final String ICON_LANDSCAPE = "landscape";     //$NON-NLS-1$
    115     private static final String ICON_PORTRAIT = "portrait";       //$NON-NLS-1$
    116     private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$
    117     private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$
    118     private static final String ICON_DISPLAY = "display";         //$NON-NLS-1$
    119     private static final String ICON_THEMES = "themes";           //$NON-NLS-1$
    120     private static final String ICON_ACTIVITY = "activity";       //$NON-NLS-1$
    121 
    122     /** The configuration state associated with this editor */
    123     private @NonNull Configuration mConfiguration = Configuration.create(this);
    124 
    125     /** Serialized state to use when initializing the configuration after the SDK is loaded */
    126     private String mInitialState;
    127 
    128     /** The client of the configuration editor */
    129     private final ConfigurationClient mClient;
    130 
    131     /** Counter for programmatic UI changes: if greater than 0, we're within a call */
    132     private int mDisableUpdates = 0;
    133 
    134     /** List of available devices */
    135     private List<Device> mDeviceList = Collections.emptyList();
    136 
    137     /** List of available targets */
    138     private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
    139 
    140     /** List of available themes */
    141     private final List<String> mThemeList = new ArrayList<String>();
    142 
    143     /** List of available locales */
    144     private final List<Locale > mLocaleList = new ArrayList<Locale>();
    145 
    146     /** The file being edited */
    147     private IFile mEditedFile;
    148 
    149     /** The {@link ProjectResources} for the edited file's project */
    150     private ProjectResources mResources;
    151 
    152     /** The target of the project of the file being edited. */
    153     private IAndroidTarget mProjectTarget;
    154 
    155     /** Dropdown for configurations */
    156     private ToolItem mConfigCombo;
    157 
    158     /** Dropdown for devices */
    159     private ToolItem mDeviceCombo;
    160 
    161     /** Dropdown for device states */
    162     private ToolItem mOrientationCombo;
    163 
    164     /** Dropdown for themes */
    165     private ToolItem mThemeCombo;
    166 
    167     /** Dropdown for locales */
    168     private ToolItem mLocaleCombo;
    169 
    170     /** Dropdown for activities */
    171     private ToolItem mActivityCombo;
    172 
    173     /** Dropdown for rendering targets */
    174     private ToolItem mTargetCombo;
    175 
    176     /** Whether the SDK has changed since the last model reload; if so we must reload targets */
    177     private boolean mSdkChanged = true;
    178 
    179     /**
    180      * Creates a new {@linkplain ConfigurationChooser} and adds it to the
    181      * parent. The method also receives custom buttons to set into the
    182      * configuration composite. The list is organized as an array of arrays.
    183      * Each array represents a group of buttons thematically grouped together.
    184      *
    185      * @param client the client embedding this configuration chooser
    186      * @param parent The parent composite.
    187      * @param initialState The initial state (serialized form) to use for the
    188      *            configuration
    189      */
    190     public ConfigurationChooser(
    191             @NonNull ConfigurationClient client,
    192             Composite parent,
    193             @Nullable String initialState) {
    194         super(parent, SWT.NONE);
    195         mClient = client;
    196 
    197         setVisible(false); // Delayed until the targets are loaded
    198 
    199         mInitialState = initialState;
    200         setLayout(new GridLayout(1, false));
    201 
    202         IconFactory icons = IconFactory.getInstance();
    203 
    204         // TODO: Consider switching to a CoolBar instead
    205         ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
    206         toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
    207 
    208         mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN );
    209         mConfigCombo.setImage(icons.getIcon("android_file")); //$NON-NLS-1$
    210         mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse");
    211 
    212         @SuppressWarnings("unused")
    213         ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR);
    214 
    215         mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
    216         mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY));
    217 
    218         @SuppressWarnings("unused")
    219         ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR);
    220 
    221         mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
    222         mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT));
    223         mOrientationCombo.setToolTipText("Go to next state");
    224 
    225         @SuppressWarnings("unused")
    226         ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR);
    227 
    228         mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
    229         mThemeCombo.setImage(icons.getIcon(ICON_THEMES));
    230 
    231         @SuppressWarnings("unused")
    232         ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR);
    233 
    234         mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
    235         mActivityCombo.setToolTipText("Associated activity or fragment providing context");
    236         // The JDT class icon is lopsided, presumably because they've left room in the
    237         // bottom right corner for badges (for static, final etc). Unfortunately, this
    238         // means that the icon looks out of place when sitting close to the language globe
    239         // icon, the theme icon, etc so that it looks vertically misaligned:
    240         //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS));
    241         // ...so use one that is centered instead:
    242         mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY));
    243 
    244         @SuppressWarnings("unused")
    245         ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR);
    246 
    247         //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
    248         //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
    249         ToolBar rightToolBar = toolBar;
    250 
    251         mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
    252         mLocaleCombo.setImage(FlagManager.getGlobeIcon());
    253         mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse");
    254 
    255         @SuppressWarnings("unused")
    256         ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR);
    257 
    258         mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
    259         mTargetCombo.setImage(AdtPlugin.getAndroidLogo());
    260         mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse");
    261 
    262         SelectionListener listener = new SelectionAdapter() {
    263             @Override
    264             public void widgetSelected(SelectionEvent e) {
    265                 Object source = e.getSource();
    266 
    267                 if (source == mConfigCombo) {
    268                     ConfigurationMenuListener.show(ConfigurationChooser.this, mConfigCombo);
    269                 } else if (source == mActivityCombo) {
    270                     ActivityMenuListener.show(ConfigurationChooser.this, mActivityCombo);
    271                 } else if (source == mLocaleCombo) {
    272                     LocaleMenuListener.show(ConfigurationChooser.this, mLocaleCombo);
    273                 } else if (source == mDeviceCombo) {
    274                     DeviceMenuListener.show(ConfigurationChooser.this, mDeviceCombo);
    275                 } else if (source == mTargetCombo) {
    276                     TargetMenuListener.show(ConfigurationChooser.this, mTargetCombo);
    277                 } else if (source == mThemeCombo) {
    278                     ThemeMenuAction.showThemeMenu(ConfigurationChooser.this, mThemeCombo,
    279                             mThemeList);
    280                 } else if (source == mOrientationCombo) {
    281                     if (e.detail == SWT.ARROW) {
    282                         OrientationMenuAction.showMenu(ConfigurationChooser.this,
    283                                 mOrientationCombo);
    284                     } else {
    285                         gotoNextState();
    286                     }
    287                 }
    288             }
    289         };
    290         mConfigCombo.addSelectionListener(listener);
    291         mActivityCombo.addSelectionListener(listener);
    292         mLocaleCombo.addSelectionListener(listener);
    293         mDeviceCombo.addSelectionListener(listener);
    294         mTargetCombo.addSelectionListener(listener);
    295         mThemeCombo.addSelectionListener(listener);
    296         mOrientationCombo.addSelectionListener(listener);
    297 
    298         addDisposeListener(this);
    299 
    300         initDevices();
    301         initTargets();
    302     }
    303 
    304     /**
    305      * Returns the edited file
    306      *
    307      * @return the file
    308      */
    309     @Nullable
    310     public IFile getEditedFile() {
    311         return mEditedFile;
    312     }
    313 
    314     /**
    315      * Returns the project of the edited file
    316      *
    317      * @return the project
    318      */
    319     @Nullable
    320     public IProject getProject() {
    321         if (mEditedFile != null) {
    322             return mEditedFile.getProject();
    323         } else {
    324             return null;
    325         }
    326     }
    327 
    328     ConfigurationClient getClient() {
    329         return mClient;
    330     }
    331 
    332     /**
    333      * Returns the project resources for the project being configured by this
    334      * chooser
    335      *
    336      * @return the project resources
    337      */
    338     @Nullable
    339     public ProjectResources getResources() {
    340         return mResources;
    341     }
    342 
    343     /**
    344      * Returns the full, complete {@link FolderConfiguration}
    345      *
    346      * @return the full configuration
    347      */
    348     public FolderConfiguration getFullConfiguration() {
    349         return mConfiguration.getFullConfig();
    350     }
    351 
    352     /**
    353      * Returns the project target
    354      *
    355      * @return the project target
    356      */
    357     public IAndroidTarget getProjectTarget() {
    358         return mProjectTarget;
    359     }
    360 
    361     /**
    362      * Returns the configuration being edited by this {@linkplain ConfigurationChooser}
    363      *
    364      * @return the configuration
    365      */
    366     public Configuration getConfiguration() {
    367         return mConfiguration;
    368     }
    369 
    370     /**
    371      * Returns the list of locales
    372      * @return a list of {@link ResourceQualifier} pairs
    373      */
    374     @NonNull
    375     public List<Locale> getLocaleList() {
    376         return mLocaleList;
    377     }
    378 
    379     /**
    380      * Returns the list of available devices
    381      *
    382      * @return a list of {@link Device} objects
    383      */
    384     @NonNull
    385     public List<Device> getDeviceList() {
    386         return mDeviceList;
    387     }
    388 
    389     /**
    390      * Returns the list of available render targets
    391      *
    392      * @return a list of {@link IAndroidTarget} objects
    393      */
    394     @NonNull
    395     public List<IAndroidTarget> getTargetList() {
    396         return mTargetList;
    397     }
    398 
    399     // ---- Configuration State Lookup ----
    400 
    401     /**
    402      * Returns the rendering target to be used
    403      *
    404      * @return the target
    405      */
    406     @NonNull
    407     public IAndroidTarget getTarget() {
    408         IAndroidTarget target = mConfiguration.getTarget();
    409         if (target == null) {
    410             target = mProjectTarget;
    411         }
    412 
    413         return target;
    414     }
    415 
    416     /**
    417      * Returns the current device string, or null if no device is selected
    418      *
    419      * @return the device name, or null
    420      */
    421     @Nullable
    422     public String getDeviceName() {
    423         Device device = mConfiguration.getDevice();
    424         if (device != null) {
    425             return device.getName();
    426         }
    427 
    428         return null;
    429     }
    430 
    431     /**
    432      * Returns the current theme, or null if none has been selected
    433      *
    434      * @return the theme name, or null
    435      */
    436     @Nullable
    437     public String getThemeName() {
    438         String theme = mConfiguration.getTheme();
    439         if (theme != null) {
    440             theme = ResourceHelper.styleToTheme(theme);
    441         }
    442 
    443         return theme;
    444     }
    445 
    446     /** Move to the next device state, changing the icon if it changes orientation */
    447     private void gotoNextState() {
    448         State state = mConfiguration.getDeviceState();
    449         State flipped = mConfiguration.getNextDeviceState(state);
    450         if (flipped != state) {
    451             selectDeviceState(flipped);
    452             onDeviceConfigChange();
    453         }
    454     }
    455 
    456     // ---- Implements DisposeListener ----
    457 
    458     @Override
    459     public void widgetDisposed(DisposeEvent e) {
    460         dispose();
    461     }
    462 
    463     @Override
    464     public void dispose() {
    465         if (!isDisposed()) {
    466             super.dispose();
    467 
    468             final Sdk sdk = Sdk.getCurrent();
    469             if (sdk != null) {
    470                 DeviceManager manager = sdk.getDeviceManager();
    471                 manager.unregisterListener(this);
    472             }
    473         }
    474     }
    475 
    476     // ---- Init and reset/reload methods ----
    477 
    478     /**
    479      * Sets the reference to the file being edited.
    480      * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
    481      * loaded (or reloaded as the SDK/target changes).
    482      *
    483      * @param file the file being opened
    484      *
    485      * @see #onXmlModelLoaded()
    486      * @see #replaceFile(IFile)
    487      * @see #changeFileOnNewConfig(IFile)
    488      */
    489     public void setFile(IFile file) {
    490         mEditedFile = file;
    491         ensureInitialized();
    492     }
    493 
    494     /**
    495      * Replaces the UI with a given file configuration. This is meant to answer the user
    496      * explicitly opening a different version of the same layout from the Package Explorer.
    497      * <p/>This attempts to keep the current config, but may change it if it's not compatible or
    498      * not the best match
    499      * @param file the file being opened.
    500      */
    501     public void replaceFile(IFile file) {
    502         // if there is no previous selection, revert to default mode.
    503         if (mConfiguration.getDevice() == null) {
    504             setFile(file); // onTargetChanged will be called later.
    505             return;
    506         }
    507 
    508         setFile(file);
    509         IProject project = mEditedFile.getProject();
    510         mResources = ResourceManager.getInstance().getProjectResources(project);
    511 
    512         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
    513         mConfiguration.setEditedConfig(resFolder.getConfiguration());
    514 
    515         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    516                            // new values in the widgets.
    517 
    518         try {
    519             // only attempt to do anything if the SDK and targets are loaded.
    520             LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
    521 
    522             if (sdkStatus == LoadStatus.LOADED) {
    523                 setVisible(true);
    524 
    525                 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
    526                         null /*project*/);
    527 
    528                 if (targetStatus == LoadStatus.LOADED) {
    529 
    530                     // update the current config selection to make sure it's
    531                     // compatible with the new file
    532                     ConfigurationMatcher matcher = new ConfigurationMatcher(this);
    533                     matcher.adaptConfigSelection(true /*needBestMatch*/);
    534                     mConfiguration.syncFolderConfig();
    535 
    536                     // update the string showing the config value
    537                     selectConfiguration(mConfiguration.getEditedConfig());
    538                     updateActivity();
    539                 }
    540             } else if (sdkStatus == LoadStatus.FAILED) {
    541                 setVisible(true);
    542             }
    543         } finally {
    544             mDisableUpdates--;
    545         }
    546     }
    547 
    548     /**
    549      * Updates the UI with a new file that was opened in response to a config change.
    550      * @param file the file being opened.
    551      *
    552      * @see #replaceFile(IFile)
    553      */
    554     public void changeFileOnNewConfig(IFile file) {
    555         setFile(file);
    556         IProject project = mEditedFile.getProject();
    557         mResources = ResourceManager.getInstance().getProjectResources(project);
    558 
    559         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
    560         FolderConfiguration config = resFolder.getConfiguration();
    561         mConfiguration.setEditedConfig(config);
    562 
    563         // All that's needed is to update the string showing the config value
    564         // (since the config combo settings chosen by the user).
    565         selectConfiguration(config);
    566     }
    567 
    568     /**
    569      * Resets the configuration chooser to reflect the given file configuration. This is
    570      * intended to be used by the "Show Included In" functionality where the user has
    571      * picked a non-default configuration (such as a particular landscape layout) and the
    572      * configuration chooser must be switched to a landscape layout. This method will
    573      * trigger a model change.
    574      * <p>
    575      * This will NOT trigger a redraw event!
    576      * <p>
    577      * FIXME: We are currently setting the configuration file to be the configuration for
    578      * the "outer" (the including) file, rather than the inner file, which is the file the
    579      * user is actually editing. We need to refine this, possibly with a way for the user
    580      * to choose which configuration they are editing. And in particular, we should be
    581      * filtering the configuration chooser to only show options in the outer configuration
    582      * that are compatible with the inner included file.
    583      *
    584      * @param file the file to be configured
    585      */
    586     public void resetConfigFor(IFile file) {
    587         setFile(file);
    588 
    589         IFolder parent = (IFolder) mEditedFile.getParent();
    590         ResourceFolder resFolder = mResources.getResourceFolder(parent);
    591         if (resFolder != null) {
    592             mConfiguration.setEditedConfig(resFolder.getConfiguration());
    593         } else {
    594             FolderConfiguration config = FolderConfiguration.getConfig(
    595                     parent.getName().split(RES_QUALIFIER_SEP));
    596             if (config != null) {
    597                 mConfiguration.setEditedConfig(config);
    598             } else {
    599                 mConfiguration.setEditedConfig(new FolderConfiguration());
    600             }
    601         }
    602 
    603         onXmlModelLoaded();
    604     }
    605 
    606 
    607     /**
    608      * Sets the current configuration to match the given folder configuration,
    609      * the given theme name, the given device and device state.
    610      *
    611      * @param configuration new folder configuration to use
    612      */
    613     public void setConfiguration(@NonNull Configuration configuration) {
    614         if (mClient != null) {
    615             mClient.aboutToChange(MASK_ALL);
    616         }
    617 
    618         Configuration oldConfiguration = mConfiguration;
    619         mConfiguration = configuration;
    620         mConfiguration.setChooser(this);
    621 
    622         selectTheme(configuration.getTheme());
    623         selectLocale(configuration.getLocale());
    624         selectDevice(configuration.getDevice());
    625         selectDeviceState(configuration.getDeviceState());
    626         selectTarget(configuration.getTarget());
    627         selectActivity(configuration.getActivity());
    628 
    629         // This may be a second refresh after triggered by theme above
    630         if (mClient != null) {
    631             LayoutCanvas canvas = mClient.getCanvas();
    632             if (canvas != null) {
    633                 assert mConfiguration != oldConfiguration;
    634                 canvas.getPreviewManager().updateChooserConfig(oldConfiguration, mConfiguration);
    635             }
    636 
    637             boolean accepted = mClient.changed(MASK_ALL);
    638             if (!accepted) {
    639                 configuration = oldConfiguration;
    640                 selectTheme(configuration.getTheme());
    641                 selectLocale(configuration.getLocale());
    642                 selectDevice(configuration.getDevice());
    643                 selectDeviceState(configuration.getDeviceState());
    644                 selectTarget(configuration.getTarget());
    645                 selectActivity(configuration.getActivity());
    646                 if (canvas != null && mConfiguration != oldConfiguration) {
    647                     canvas.getPreviewManager().updateChooserConfig(mConfiguration,
    648                             oldConfiguration);
    649                 }
    650                 return;
    651             } else {
    652                 int changed = 0;
    653                 if (!equal(oldConfiguration.getTheme(), mConfiguration.getTheme())) {
    654                     changed |= CFG_THEME;
    655                 }
    656                 if (!equal(oldConfiguration.getDevice(), mConfiguration.getDevice())) {
    657                     changed |= CFG_DEVICE | CFG_DEVICE_STATE;
    658                 }
    659                 if (changed != 0) {
    660                     syncToVariations(changed, mEditedFile, mConfiguration, false, true);
    661                 }
    662             }
    663         }
    664 
    665         saveConstraints();
    666     }
    667 
    668     /**
    669      * Responds to the event that the basic SDK information finished loading.
    670      * @param target the possibly new target object associated with the file being edited (in case
    671      * the SDK path was changed).
    672      */
    673     public void onSdkLoaded(IAndroidTarget target) {
    674         // a change to the SDK means that we need to check for new/removed devices.
    675         mSdkChanged = true;
    676 
    677         // store the new target.
    678         mProjectTarget = target;
    679 
    680         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    681                            // new values in the widgets.
    682         try {
    683             updateDevices();
    684             updateTargets();
    685             ensureInitialized();
    686         } finally {
    687             mDisableUpdates--;
    688         }
    689     }
    690 
    691     /**
    692      * Responds to the XML model being loaded, either the first time or when the
    693      * Target/SDK changes.
    694      * <p>
    695      * This initializes the UI, either with the first compatible configuration
    696      * found, or it will attempt to restore a configuration if one is found to
    697      * have been saved in the file persistent storage.
    698      * <p>
    699      * If the SDK or target are not loaded, nothing will happen (but the method
    700      * must be called back when they are.)
    701      * <p>
    702      * The method automatically handles being called the first time after editor
    703      * creation, or being called after during SDK/Target changes (as long as
    704      * {@link #onSdkLoaded(IAndroidTarget)} is properly called).
    705      *
    706      * @return the target data for the rendering target used to render the
    707      *         layout
    708      *
    709      * @see #saveConstraints()
    710      * @see #onSdkLoaded(IAndroidTarget)
    711      */
    712     public AndroidTargetData onXmlModelLoaded() {
    713         AndroidTargetData targetData = null;
    714 
    715         // only attempt to do anything if the SDK and targets are loaded.
    716         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
    717         if (sdkStatus == LoadStatus.LOADED) {
    718             mDisableUpdates++; // we do not want to trigger onXXXChange when setting
    719 
    720             try {
    721                 // init the devices if needed (new SDK or first time going through here)
    722                 if (mSdkChanged) {
    723                     updateDevices();
    724                     updateTargets();
    725                     ensureInitialized();
    726                     mSdkChanged = false;
    727                 }
    728 
    729                 IProject project = mEditedFile.getProject();
    730 
    731                 Sdk currentSdk = Sdk.getCurrent();
    732                 if (currentSdk != null) {
    733                     mProjectTarget = currentSdk.getTarget(project);
    734                 }
    735 
    736                 LoadStatus targetStatus = LoadStatus.FAILED;
    737                 if (mProjectTarget != null) {
    738                     targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
    739                     updateTargets();
    740                     ensureInitialized();
    741                 }
    742 
    743                 if (targetStatus == LoadStatus.LOADED) {
    744                     setVisible(true);
    745                     if (mResources == null) {
    746                         mResources = ResourceManager.getInstance().getProjectResources(project);
    747                     }
    748                     if (mConfiguration.getEditedConfig() == null) {
    749                         IFolder parent = (IFolder) mEditedFile.getParent();
    750                         ResourceFolder resFolder = mResources.getResourceFolder(parent);
    751                         if (resFolder != null) {
    752                             mConfiguration.setEditedConfig(resFolder.getConfiguration());
    753                         } else {
    754                             FolderConfiguration config = FolderConfiguration.getConfig(
    755                                     parent.getName().split(RES_QUALIFIER_SEP));
    756                             if (config != null) {
    757                                 mConfiguration.setEditedConfig(config);
    758                             } else {
    759                                 mConfiguration.setEditedConfig(new FolderConfiguration());
    760                             }
    761                         }
    762                     }
    763 
    764                     targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
    765 
    766                     // get the file stored state
    767                     ensureInitialized();
    768                     boolean loadedConfigData = mConfiguration.getDevice() != null &&
    769                             mConfiguration.getDeviceState() != null;
    770 
    771                     // Load locale list. This must be run after we initialize the
    772                     // configuration above, since it attempts to sync the UI with
    773                     // the value loaded into the configuration.
    774                     updateLocales();
    775 
    776                     // If the current state was loaded from the persistent storage, we update the
    777                     // UI with it and then try to adapt it (which will handle incompatible
    778                     // configuration).
    779                     // Otherwise, just look for the first compatible configuration.
    780                     ConfigurationMatcher matcher = new ConfigurationMatcher(this);
    781                     if (loadedConfigData) {
    782                         // first make sure we have the config to adapt
    783                         selectDevice(mConfiguration.getDevice());
    784                         selectDeviceState(mConfiguration.getDeviceState());
    785                         mConfiguration.syncFolderConfig();
    786 
    787                         matcher.adaptConfigSelection(false);
    788 
    789                         IAndroidTarget target = mConfiguration.getTarget();
    790                         selectTarget(target);
    791                         targetData = Sdk.getCurrent().getTargetData(target);
    792                     } else {
    793                         matcher.findAndSetCompatibleConfig(false);
    794 
    795                         // Default to modern layout lib
    796                         IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(this);
    797                         if (target != null) {
    798                             targetData = Sdk.getCurrent().getTargetData(target);
    799                             selectTarget(target);
    800                             mConfiguration.setTarget(target, true);
    801                         }
    802                     }
    803 
    804                     // Update activity: This is done before updateThemes() since
    805                     // the themes selection can depend on the currently selected activity
    806                     // (e.g. when there are manifest registrations for the theme to use
    807                     // for a given activity)
    808                     updateActivity();
    809 
    810                     // Update themes. This is done after updating the devices above,
    811                     // since we want to look at the chosen device size to decide
    812                     // what the default theme (for example, with Honeycomb we choose
    813                     // Holo as the default theme but only if the screen size is XLARGE
    814                     // (and of course only if the manifest does not specify another
    815                     // default theme).
    816                     updateThemes();
    817 
    818                     // update the string showing the config value
    819                     selectConfiguration(mConfiguration.getEditedConfig());
    820 
    821                     // compute the final current config
    822                     mConfiguration.syncFolderConfig();
    823                 } else if (targetStatus == LoadStatus.FAILED) {
    824                     setVisible(true);
    825                 }
    826             } finally {
    827                 mDisableUpdates--;
    828             }
    829         }
    830 
    831         return targetData;
    832     }
    833 
    834     /**
    835      * This is a temporary workaround for a infrequently happening bug; apparently
    836      * there are cases where the configuration chooser isn't shown
    837      */
    838     public void ensureVisible() {
    839         if (!isVisible()) {
    840             LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
    841             if (sdkStatus == LoadStatus.LOADED) {
    842                 onXmlModelLoaded();
    843             }
    844         }
    845     }
    846 
    847     /**
    848      * An alternate layout for this layout has been created. This means that the
    849      * current layout may no longer be a best fit. However, since we support multiple
    850      * layouts being open at the same time, we need to adjust the current configuration
    851      * back to something where this layout <b>is</b> a best match.
    852      */
    853     public void onAlternateLayoutCreated() {
    854         IFile best = ConfigurationMatcher.getBestFileMatch(this);
    855         if (best != null && !best.equals(mEditedFile)) {
    856             ConfigurationMatcher matcher = new ConfigurationMatcher(this);
    857             matcher.adaptConfigSelection(true /*needBestMatch*/);
    858             mConfiguration.syncFolderConfig();
    859             if (mClient != null) {
    860                 mClient.changed(MASK_ALL);
    861             }
    862         }
    863     }
    864 
    865     /**
    866      * Loads the list of {@link Device}s and inits the UI with it.
    867      */
    868     private void initDevices() {
    869         final Sdk sdk = Sdk.getCurrent();
    870         if (sdk != null) {
    871             DeviceManager manager = sdk.getDeviceManager();
    872             // This method can be called more than once, so avoid duplicate entries
    873             manager.unregisterListener(this);
    874             manager.registerListener(this);
    875             mDeviceList = manager.getDevices(DeviceManager.ALL_DEVICES);
    876         } else {
    877             mDeviceList = new ArrayList<Device>();
    878         }
    879     }
    880 
    881     /**
    882      * Loads the list of {@link IAndroidTarget} and inits the UI with it.
    883      */
    884     private boolean initTargets() {
    885         mTargetList.clear();
    886 
    887         Sdk currentSdk = Sdk.getCurrent();
    888         if (currentSdk != null) {
    889             IAndroidTarget[] targets = currentSdk.getTargets();
    890             for (int i = 0 ; i < targets.length; i++) {
    891                 if (targets[i].hasRenderingLibrary()) {
    892                     mTargetList.add(targets[i]);
    893                 }
    894             }
    895 
    896             return true;
    897         }
    898 
    899         return false;
    900     }
    901 
    902     /** Ensures that the configuration has been initialized */
    903     public void ensureInitialized() {
    904         if (mConfiguration.getDevice() == null && mEditedFile != null) {
    905             String data = ConfigurationDescription.getDescription(mEditedFile);
    906             if (mInitialState != null) {
    907                 data = mInitialState;
    908                 mInitialState = null;
    909             }
    910             if (data != null) {
    911                 mConfiguration.initialize(data);
    912                 mConfiguration.syncFolderConfig();
    913             }
    914         }
    915     }
    916 
    917     private void updateDevices() {
    918         if (mDeviceList.size() == 0) {
    919             initDevices();
    920         }
    921     }
    922 
    923     private void updateTargets() {
    924         if (mTargetList.size() == 0) {
    925             if (!initTargets()) {
    926                 return;
    927             }
    928         }
    929 
    930         IAndroidTarget renderingTarget = mConfiguration.getTarget();
    931 
    932         IAndroidTarget match = null;
    933         for (IAndroidTarget target : mTargetList) {
    934             if (renderingTarget != null) {
    935                 // use equals because the rendering could be from a previous SDK, so
    936                 // it may not be the same instance.
    937                 if (renderingTarget.equals(target)) {
    938                     match = target;
    939                 }
    940             } else if (mProjectTarget == target) {
    941                 match = target;
    942             }
    943 
    944         }
    945 
    946         if (match == null) {
    947             // the rendering target is the same as the project.
    948             renderingTarget = mProjectTarget;
    949         } else {
    950             // set the rendering target to the new object.
    951             renderingTarget = match;
    952         }
    953 
    954         mConfiguration.setTarget(renderingTarget, true);
    955         selectTarget(renderingTarget);
    956     }
    957 
    958     /** Update the toolbar whenever a label has changed, to not only
    959      * cause the layout in the current toolbar to update, but to possibly
    960      * wrap the toolbars and update the layout of the surrounding area.
    961      */
    962     private void resizeToolBar() {
    963         Point size = getSize();
    964         Point newSize = computeSize(size.x, SWT.DEFAULT, true);
    965         setSize(newSize);
    966         Composite parent = getParent();
    967         parent.layout();
    968         parent.redraw();
    969     }
    970 
    971 
    972     Image getOrientationIcon(ScreenOrientation orientation, boolean flip) {
    973         IconFactory icons = IconFactory.getInstance();
    974         switch (orientation) {
    975             case LANDSCAPE:
    976                 return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
    977             case SQUARE:
    978                 return icons.getIcon(ICON_SQUARE);
    979             case PORTRAIT:
    980             default:
    981                 return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
    982         }
    983     }
    984 
    985     ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) {
    986         IconFactory icons = IconFactory.getInstance();
    987         switch (orientation) {
    988             case LANDSCAPE:
    989                 return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
    990             case SQUARE:
    991                 return icons.getImageDescriptor(ICON_SQUARE);
    992             case PORTRAIT:
    993             default:
    994                 return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
    995         }
    996     }
    997 
    998     @NonNull
    999     ScreenOrientation getOrientation(State state) {
   1000         FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state);
   1001         ScreenOrientation orientation = null;
   1002         if (config != null && config.getScreenOrientationQualifier() != null) {
   1003             orientation = config.getScreenOrientationQualifier().getValue();
   1004         }
   1005 
   1006         if (orientation == null) {
   1007             orientation = ScreenOrientation.PORTRAIT;
   1008         }
   1009 
   1010         return orientation;
   1011     }
   1012 
   1013     /**
   1014      * Stores the current config selection into the edited file such that we can
   1015      * bring it back the next time this layout is opened.
   1016      */
   1017     public void saveConstraints() {
   1018         String description = mConfiguration.toPersistentString();
   1019         if (description != null && !description.isEmpty()) {
   1020             ConfigurationDescription.setDescription(mEditedFile, description);
   1021         }
   1022     }
   1023 
   1024     // ---- Setting the current UI state ----
   1025 
   1026     void selectDeviceState(@Nullable State state) {
   1027         assert isUiThread();
   1028         try {
   1029             mDisableUpdates++;
   1030             mOrientationCombo.setData(state);
   1031 
   1032             State nextState = mConfiguration.getNextDeviceState(state);
   1033             mOrientationCombo.setImage(getOrientationIcon(getOrientation(state),
   1034                     nextState != state));
   1035         } finally {
   1036             mDisableUpdates--;
   1037         }
   1038     }
   1039 
   1040     void selectTarget(IAndroidTarget target) {
   1041         assert isUiThread();
   1042         try {
   1043             mDisableUpdates++;
   1044             mTargetCombo.setData(target);
   1045             String label = getRenderingTargetLabel(target, true);
   1046             mTargetCombo.setText(label);
   1047             resizeToolBar();
   1048         } finally {
   1049             mDisableUpdates--;
   1050         }
   1051     }
   1052 
   1053     /**
   1054      * Selects a given {@link Device} in the device combo, if it is found.
   1055      * @param device the device to select
   1056      * @return true if the device was found.
   1057      */
   1058     boolean selectDevice(@Nullable Device device) {
   1059         assert isUiThread();
   1060         try {
   1061             mDisableUpdates++;
   1062             mDeviceCombo.setData(device);
   1063             if (device != null) {
   1064                 mDeviceCombo.setText(getDeviceLabel(device, true));
   1065             } else {
   1066                 mDeviceCombo.setText("Device");
   1067             }
   1068             resizeToolBar();
   1069         } finally {
   1070             mDisableUpdates--;
   1071         }
   1072 
   1073         return false;
   1074     }
   1075 
   1076     void selectActivity(@Nullable String fqcn) {
   1077         assert isUiThread();
   1078         try {
   1079             mDisableUpdates++;
   1080             if (fqcn != null) {
   1081                 mActivityCombo.setData(fqcn);
   1082                 String label = getActivityLabel(fqcn, true);
   1083                 mActivityCombo.setText(label);
   1084             } else {
   1085                 mActivityCombo.setText("(Select)");
   1086             }
   1087             resizeToolBar();
   1088         } finally {
   1089             mDisableUpdates--;
   1090         }
   1091     }
   1092 
   1093     void selectTheme(@Nullable String theme) {
   1094         assert isUiThread();
   1095         try {
   1096             mDisableUpdates++;
   1097             assert theme == null ||  theme.startsWith(STYLE_RESOURCE_PREFIX)
   1098                     || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme;
   1099             mThemeCombo.setData(theme);
   1100             if (theme != null) {
   1101                 mThemeCombo.setText(getThemeLabel(theme, true));
   1102             } else {
   1103                 // FIXME eclipse claims this is dead code.
   1104                 mThemeCombo.setText("(Set Theme)");
   1105             }
   1106             resizeToolBar();
   1107         } finally {
   1108             mDisableUpdates--;
   1109         }
   1110     }
   1111 
   1112     void selectLocale(@Nullable Locale locale) {
   1113         assert isUiThread();
   1114         try {
   1115             mDisableUpdates++;
   1116             mLocaleCombo.setData(locale);
   1117             String label = Strings.nullToEmpty(getLocaleLabel(this, locale, true));
   1118             mLocaleCombo.setText(label);
   1119 
   1120             Image image = getFlagImage(locale);
   1121             mLocaleCombo.setImage(image);
   1122 
   1123             resizeToolBar();
   1124         } finally {
   1125             mDisableUpdates--;
   1126         }
   1127     }
   1128 
   1129     @NonNull
   1130     Image getFlagImage(@Nullable Locale locale) {
   1131         if (locale != null) {
   1132             return locale.getFlagImage();
   1133         }
   1134 
   1135         return FlagManager.getGlobeIcon();
   1136     }
   1137 
   1138     private void selectConfiguration(FolderConfiguration fileConfig) {
   1139         /* For now, don't show any text in the configuration combo, use just an
   1140            icon. This has the advantage that the configuration contents don't
   1141            shift around, so you can for example click back and forth between
   1142            portrait and landscape without the icon moving under the mouse.
   1143            If this works well, remove this whole method post ADT 21.
   1144         assert isUiThread();
   1145         try {
   1146             String current = mEditedFile.getParent().getName();
   1147             if (current.equals(FD_RES_LAYOUT)) {
   1148                 current = "default";
   1149             }
   1150 
   1151             // Pretty things up a bit
   1152             //if (current == null || current.equals("default")) {
   1153             //    current = "Default Configuration";
   1154             //}
   1155             mConfigCombo.setText(current);
   1156             resizeToolBar();
   1157         } finally {
   1158             mDisableUpdates--;
   1159         }
   1160          */
   1161     }
   1162 
   1163     /**
   1164      * Finds a locale matching the config from a file.
   1165      *
   1166      * @param language the language qualifier or null if none is set.
   1167      * @param region the region qualifier or null if none is set.
   1168      * @return true if there was a change in the combobox as a result of
   1169      *         applying the locale
   1170      */
   1171     private boolean setLocale(@Nullable Locale locale) {
   1172         boolean changed = !Objects.equal(mConfiguration.getLocale(), locale);
   1173         selectLocale(locale);
   1174 
   1175         return changed;
   1176     }
   1177 
   1178     // ---- Creating UI labels ----
   1179 
   1180     /**
   1181      * Returns a suitable label to use to display the given activity
   1182      *
   1183      * @param fqcn the activity class to look up a label for
   1184      * @param brief if true, generate a brief label (suitable for a toolbar
   1185      *            button), otherwise a fuller name (suitable for a menu item)
   1186      * @return the label
   1187      */
   1188     public static String getActivityLabel(String fqcn, boolean brief) {
   1189         if (brief) {
   1190             String label = fqcn;
   1191             int packageIndex = label.lastIndexOf('.');
   1192             if (packageIndex != -1) {
   1193                 label = label.substring(packageIndex + 1);
   1194             }
   1195             int innerClass = label.lastIndexOf('$');
   1196             if (innerClass != -1) {
   1197                 label = label.substring(innerClass + 1);
   1198             }
   1199 
   1200             // Also strip out the "Activity" or "Fragment" common suffix
   1201             // if this is a long name
   1202             if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix
   1203                 label = label.substring(0, label.length() - 8);
   1204             } else if (label.endsWith("Fragment") && label.length() > 8 + 12) {
   1205                 label = label.substring(0, label.length() - 8);
   1206             }
   1207 
   1208             return label;
   1209         }
   1210 
   1211         return fqcn;
   1212     }
   1213 
   1214     /**
   1215      * Returns a suitable label to use to display the given theme
   1216      *
   1217      * @param theme the theme to produce a label for
   1218      * @param brief if true, generate a brief label (suitable for a toolbar
   1219      *            button), otherwise a fuller name (suitable for a menu item)
   1220      * @return the label
   1221      */
   1222     public static String getThemeLabel(String theme, boolean brief) {
   1223         theme = ResourceHelper.styleToTheme(theme);
   1224 
   1225         if (brief) {
   1226             int index = theme.lastIndexOf('.');
   1227             if (index < theme.length() - 1) {
   1228                 return theme.substring(index + 1);
   1229             }
   1230         }
   1231         return theme;
   1232     }
   1233 
   1234     /**
   1235      * Returns a suitable label to use to display the given rendering target
   1236      *
   1237      * @param target the target to produce a label for
   1238      * @param brief if true, generate a brief label (suitable for a toolbar
   1239      *            button), otherwise a fuller name (suitable for a menu item)
   1240      * @return the label
   1241      */
   1242     public static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) {
   1243         if (target == null) {
   1244             return "<null>";
   1245         }
   1246 
   1247         AndroidVersion version = target.getVersion();
   1248 
   1249         if (brief) {
   1250             if (target.isPlatform()) {
   1251                 return Integer.toString(version.getApiLevel());
   1252             } else {
   1253                 return target.getName() + ':' + Integer.toString(version.getApiLevel());
   1254             }
   1255         }
   1256 
   1257         String label = String.format("API %1$d: %2$s",
   1258                 version.getApiLevel(),
   1259                 target.getShortClasspathName());
   1260 
   1261         return label;
   1262     }
   1263 
   1264     /**
   1265      * Returns a suitable label to use to display the given device
   1266      *
   1267      * @param device the device to produce a label for
   1268      * @param brief if true, generate a brief label (suitable for a toolbar
   1269      *            button), otherwise a fuller name (suitable for a menu item)
   1270      * @return the label
   1271      */
   1272     public static String getDeviceLabel(@Nullable Device device, boolean brief) {
   1273         if (device == null) {
   1274             return "";
   1275         }
   1276         String name = device.getName();
   1277 
   1278         if (brief) {
   1279             // Produce a really brief summary of the device name, suitable for
   1280             // use in the narrow space available in the toolbar for example
   1281             int nexus = name.indexOf("Nexus"); //$NON-NLS-1$
   1282             if (nexus != -1) {
   1283                 int begin = name.indexOf('(');
   1284                 if (begin != -1) {
   1285                     begin++;
   1286                     int end = name.indexOf(')', begin);
   1287                     if (end != -1) {
   1288                         return name.substring(begin, end).trim();
   1289                     }
   1290                 }
   1291             }
   1292         }
   1293 
   1294         return name;
   1295     }
   1296 
   1297     /**
   1298      * Returns a suitable label to use to display the given locale
   1299      *
   1300      * @param chooser the chooser, if known
   1301      * @param locale the locale to look up a label for
   1302      * @param brief if true, generate a brief label (suitable for a toolbar
   1303      *            button), otherwise a fuller name (suitable for a menu item)
   1304      * @return the label
   1305      */
   1306     @Nullable
   1307     public static String getLocaleLabel(
   1308             @Nullable ConfigurationChooser chooser,
   1309             @Nullable Locale locale,
   1310             boolean brief) {
   1311         if (locale == null) {
   1312             return null;
   1313         }
   1314 
   1315         if (!locale.hasLanguage()) {
   1316             if (brief) {
   1317                 // Just use the icon
   1318                 return "";
   1319             }
   1320 
   1321             boolean hasLocale = false;
   1322             ResourceRepository projectRes = chooser != null ? chooser.mClient.getProjectResources()
   1323                     : null;
   1324             if (projectRes != null) {
   1325                 hasLocale = projectRes.getLanguages().size() > 0;
   1326             }
   1327 
   1328             if (hasLocale) {
   1329                 return "Other";
   1330             } else {
   1331                 return "Any";
   1332             }
   1333         }
   1334 
   1335         String languageCode = locale.language.getValue();
   1336         String languageName = LocaleManager.getLanguageName(languageCode);
   1337 
   1338         if (!locale.hasRegion()) {
   1339             // TODO: Make the region string use "Other" instead of "Any" if
   1340             // there is more than one region for a given language
   1341             //if (regions.size() > 0) {
   1342             //    return String.format("%1$s / Other", language);
   1343             //} else {
   1344             //    return String.format("%1$s / Any", language);
   1345             //}
   1346             if (!brief && languageName != null) {
   1347                 return String.format("%1$s (%2$s)", languageName, languageCode);
   1348             } else {
   1349                 return languageCode;
   1350             }
   1351         } else {
   1352             String regionCode = locale.region.getValue();
   1353             if (!brief && languageName != null) {
   1354                 String regionName = LocaleManager.getRegionName(regionCode);
   1355                 if (regionName != null) {
   1356                     return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode,
   1357                             regionName, regionCode);
   1358                 }
   1359                 return String.format("%1$s (%2$s) in %3$s", languageName, languageCode,
   1360                         regionCode);
   1361             }
   1362             return String.format("%1$s / %2$s", languageCode, regionCode);
   1363         }
   1364     }
   1365 
   1366     // ---- Implements DevicesChangedListener ----
   1367 
   1368     @Override
   1369     public void onDevicesChanged() {
   1370         final Sdk sdk = Sdk.getCurrent();
   1371         if (sdk != null) {
   1372             mDeviceList = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES);
   1373         } else {
   1374             mDeviceList = new ArrayList<Device>();
   1375         }
   1376     }
   1377 
   1378     // ---- Reacting to UI changes ----
   1379 
   1380     /**
   1381      * Called when the selection of the device combo changes.
   1382      */
   1383     void onDeviceChange() {
   1384         // because changing the content of a combo triggers a change event, respect the
   1385         // mDisableUpdates flag
   1386         if (mDisableUpdates > 0) {
   1387             return;
   1388         }
   1389 
   1390         // Attempt to preserve the device state
   1391         String stateName = null;
   1392         Device prevDevice = mConfiguration.getDevice();
   1393         State prevState = mConfiguration.getDeviceState();
   1394         Device device = (Device) mDeviceCombo.getData();
   1395         if (prevDevice != null && prevState != null && device != null) {
   1396             // get the previous config, so that we can look for a close match
   1397             FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState);
   1398             if (oldConfig != null) {
   1399                 stateName = ConfigurationMatcher.getClosestMatch(oldConfig, device.getAllStates());
   1400             }
   1401         }
   1402         mConfiguration.setDevice(device, true);
   1403         State newState = Configuration.getState(device, stateName);
   1404         mConfiguration.setDeviceState(newState, true);
   1405         selectDeviceState(newState);
   1406         mConfiguration.syncFolderConfig();
   1407 
   1408         // Notify
   1409         IFile file = mEditedFile;
   1410         boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
   1411         if (!accepted) {
   1412             mConfiguration.setDevice(prevDevice, true);
   1413             mConfiguration.setDeviceState(prevState, true);
   1414             mConfiguration.syncFolderConfig();
   1415             selectDevice(prevDevice);
   1416             selectDeviceState(prevState);
   1417             return;
   1418         } else {
   1419             syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, file, mConfiguration, false, true);
   1420         }
   1421 
   1422         saveConstraints();
   1423     }
   1424 
   1425     /**
   1426      * Synchronizes changes to the given attributes (indicated by the mask
   1427      * referencing the {@code CFG_} configuration attribute bit flags in
   1428      * {@link Configuration} to the layout variations of the given updated file.
   1429      *
   1430      * @param flags the attributes which were updated
   1431      * @param updatedFile the file which was updated
   1432      * @param base the base configuration to base the chooser off of
   1433      * @param includeSelf whether the updated file itself should be updated
   1434      * @param async whether the updates should be performed asynchronously
   1435      */
   1436     public void syncToVariations(
   1437             final int flags,
   1438             final @NonNull IFile updatedFile,
   1439             final @NonNull Configuration base,
   1440             final boolean includeSelf,
   1441             boolean async) {
   1442         if (async) {
   1443             getDisplay().asyncExec(new Runnable() {
   1444                 @Override
   1445                 public void run() {
   1446                     doSyncToVariations(flags, updatedFile, includeSelf, base);
   1447                 }
   1448             });
   1449         } else {
   1450             doSyncToVariations(flags, updatedFile, includeSelf, base);
   1451         }
   1452     }
   1453 
   1454     private void doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf,
   1455             Configuration base) {
   1456         // Synchronize the given changes to other configurations as well
   1457         List<IFile> files = AdtUtils.getResourceVariations(updatedFile, includeSelf);
   1458         for (IFile file : files) {
   1459             Configuration configuration = Configuration.create(base, file);
   1460             configuration.setTheme(base.getTheme());
   1461             configuration.setActivity(base.getActivity());
   1462             Collection<IEditorPart> editors = AdtUtils.findEditorsFor(file, false);
   1463             boolean found = false;
   1464             for (IEditorPart editor : editors) {
   1465                 if (editor instanceof CommonXmlEditor) {
   1466                     CommonXmlDelegate delegate = ((CommonXmlEditor) editor).getDelegate();
   1467                     if (delegate instanceof LayoutEditorDelegate) {
   1468                         editor = ((LayoutEditorDelegate) delegate).getGraphicalEditor();
   1469                     }
   1470                 }
   1471                 if (editor instanceof GraphicalEditorPart) {
   1472                     ConfigurationChooser chooser =
   1473                         ((GraphicalEditorPart) editor).getConfigurationChooser();
   1474                     chooser.setConfiguration(configuration);
   1475                     found = true;
   1476                 }
   1477             }
   1478             if (!found) {
   1479                 // Just update the file persistence
   1480                 String description = configuration.toPersistentString();
   1481                 ConfigurationDescription.setDescription(file, description);
   1482             }
   1483         }
   1484     }
   1485 
   1486     /**
   1487      * Called when the device config selection changes.
   1488      */
   1489     void onDeviceConfigChange() {
   1490         // because changing the content of a combo triggers a change event, respect the
   1491         // mDisableUpdates flag
   1492         if (mDisableUpdates > 0) {
   1493             return;
   1494         }
   1495 
   1496         State prev = mConfiguration.getDeviceState();
   1497         State state = (State) mOrientationCombo.getData();
   1498         mConfiguration.setDeviceState(state, false);
   1499 
   1500         if (mClient != null) {
   1501             boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
   1502             if (!accepted) {
   1503                 mConfiguration.setDeviceState(prev, false);
   1504                 selectDeviceState(prev);
   1505                 return;
   1506             }
   1507         }
   1508 
   1509         saveConstraints();
   1510     }
   1511 
   1512     /**
   1513      * Call back for language combo selection
   1514      */
   1515     void onLocaleChange() {
   1516         // because mLocaleList triggers onLocaleChange at each modification, the filling
   1517         // of the combo with data will trigger notifications, and we don't want that.
   1518         if (mDisableUpdates > 0) {
   1519             return;
   1520         }
   1521 
   1522         Locale prev = mConfiguration.getLocale();
   1523         Locale locale = (Locale) mLocaleCombo.getData();
   1524         if (locale == null) {
   1525             locale = Locale.ANY;
   1526         }
   1527         mConfiguration.setLocale(locale, false);
   1528 
   1529         if (mClient != null) {
   1530             boolean accepted = mClient.changed(CFG_LOCALE);
   1531             if (!accepted) {
   1532                 mConfiguration.setLocale(prev, false);
   1533                 selectLocale(prev);
   1534             }
   1535         }
   1536 
   1537         // Store locale project-wide setting
   1538         mConfiguration.saveRenderState();
   1539     }
   1540 
   1541 
   1542     void onThemeChange() {
   1543         if (mDisableUpdates > 0) {
   1544             return;
   1545         }
   1546 
   1547         String prev = mConfiguration.getTheme();
   1548         mConfiguration.setTheme((String) mThemeCombo.getData());
   1549 
   1550         if (mClient != null) {
   1551             boolean accepted = mClient.changed(CFG_THEME);
   1552             if (!accepted) {
   1553                 mConfiguration.setTheme(prev);
   1554                 selectTheme(prev);
   1555                 return;
   1556             } else {
   1557                 syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, mEditedFile, mConfiguration,
   1558                         false, true);
   1559             }
   1560         }
   1561 
   1562         saveConstraints();
   1563     }
   1564 
   1565     void notifyFolderConfigChanged() {
   1566         if (mDisableUpdates > 0 || mClient == null) {
   1567             return;
   1568         }
   1569 
   1570         if (mClient.changed(CFG_FOLDER)) {
   1571             saveConstraints();
   1572         }
   1573     }
   1574 
   1575     void onSelectActivity() {
   1576         if (mDisableUpdates > 0) {
   1577             return;
   1578         }
   1579 
   1580         String activity = (String) mActivityCombo.getData();
   1581         mConfiguration.setActivity(activity);
   1582 
   1583         if (activity == null) {
   1584             return;
   1585         }
   1586 
   1587         // See if there is a default theme assigned to this activity, and if so, use it
   1588         ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject());
   1589         Map<String, String> activityThemes = manifest.getActivityThemes();
   1590         String preferred = activityThemes.get(activity);
   1591         if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) {
   1592             // Yes, switch to it
   1593             selectTheme(preferred);
   1594             onThemeChange();
   1595         }
   1596 
   1597         // Persist in XML
   1598         if (mClient != null) {
   1599             mClient.setActivity(activity);
   1600         }
   1601 
   1602         saveConstraints();
   1603     }
   1604 
   1605     /**
   1606      * Call back for api level combo selection
   1607      */
   1608     void onRenderingTargetChange() {
   1609         // because mApiCombo triggers onApiLevelChange at each modification, the filling
   1610         // of the combo with data will trigger notifications, and we don't want that.
   1611         if (mDisableUpdates > 0) {
   1612             return;
   1613         }
   1614 
   1615         IAndroidTarget prevTarget = mConfiguration.getTarget();
   1616         String prevTheme = mConfiguration.getTheme();
   1617 
   1618         int changeFlags = 0;
   1619 
   1620         // tell the listener a new rendering target is being set. Need to do this before updating
   1621         // mRenderingTarget.
   1622         if (prevTarget != null) {
   1623             changeFlags |= CFG_TARGET;
   1624             mClient.aboutToChange(changeFlags);
   1625         }
   1626 
   1627         IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData();
   1628         mConfiguration.setTarget(target, true);
   1629 
   1630         // force a theme update to reflect the new rendering target.
   1631         // This must be done after computeCurrentConfig since it'll depend on the currentConfig
   1632         // to figure out the theme list.
   1633         String oldTheme = mConfiguration.getTheme();
   1634         updateThemes();
   1635         // updateThemes may change the theme (based on theme availability in the new rendering
   1636         // target) so mark theme change if necessary
   1637         if (!Objects.equal(oldTheme, mConfiguration.getTheme())) {
   1638             changeFlags |= CFG_THEME;
   1639         }
   1640 
   1641         if (target != null) {
   1642             changeFlags |= CFG_TARGET;
   1643             changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier
   1644         }
   1645 
   1646         // Store project-wide render-target setting
   1647         mConfiguration.saveRenderState();
   1648 
   1649         mConfiguration.syncFolderConfig();
   1650 
   1651         if (mClient != null) {
   1652             boolean accepted = mClient.changed(changeFlags);
   1653             if (!accepted) {
   1654                 mConfiguration.setTarget(prevTarget, true);
   1655                 mConfiguration.setTheme(prevTheme);
   1656                 mConfiguration.syncFolderConfig();
   1657                 selectTheme(prevTheme);
   1658                 selectTarget(prevTarget);
   1659             }
   1660         }
   1661     }
   1662 
   1663     /**
   1664      * Syncs this configuration to the project wide locale and render target settings. The
   1665      * locale may ignore the project-wide setting if it is a locale-specific
   1666      * configuration.
   1667      *
   1668      * @return true if one or both of the toggles were changed, false if there were no
   1669      *         changes
   1670      */
   1671     public boolean syncRenderState() {
   1672         if (mConfiguration.getEditedConfig() == null) {
   1673             // Startup; ignore
   1674             return false;
   1675         }
   1676 
   1677         boolean renderTargetChanged = false;
   1678 
   1679         // When a page is re-activated, force the toggles to reflect the current project
   1680         // state
   1681 
   1682         Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this);
   1683 
   1684         int changeFlags = 0;
   1685         // Only sync the locale if this layout is not already a locale-specific layout!
   1686         if (pair != null && !mConfiguration.isLocaleSpecificLayout()) {
   1687             Locale locale = pair.getFirst();
   1688             if (locale != null) {
   1689                 boolean localeChanged = setLocale(locale);
   1690                 if (localeChanged) {
   1691                     changeFlags |= CFG_LOCALE;
   1692                 }
   1693             } else {
   1694                 locale = Locale.ANY;
   1695             }
   1696             mConfiguration.setLocale(locale, true);
   1697         }
   1698 
   1699         // Sync render target
   1700         IAndroidTarget configurationTarget = mConfiguration.getTarget();
   1701         IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget;
   1702         if (target != null && configurationTarget != target) {
   1703             if (mClient != null && configurationTarget != null) {
   1704                 changeFlags |= CFG_TARGET;
   1705                 mClient.aboutToChange(changeFlags);
   1706             }
   1707 
   1708             mConfiguration.setTarget(target, true);
   1709             selectTarget(target);
   1710             renderTargetChanged = true;
   1711         }
   1712 
   1713         // Neither locale nor render target changed: nothing to do
   1714         if (changeFlags == 0) {
   1715             return false;
   1716         }
   1717 
   1718         // Update the locale and/or the render target. This code contains a logical
   1719         // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
   1720         // such that we don't duplicate work.
   1721 
   1722         // Compute the new configuration; we want to do this both for locale changes
   1723         // and for render targets.
   1724         mConfiguration.syncFolderConfig();
   1725         changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier
   1726 
   1727         if (renderTargetChanged) {
   1728             // force a theme update to reflect the new rendering target.
   1729             // This must be done after computeCurrentConfig since it'll depend on the currentConfig
   1730             // to figure out the theme list.
   1731             updateThemes();
   1732         }
   1733 
   1734         if (mClient != null) {
   1735             mClient.changed(changeFlags);
   1736         }
   1737 
   1738         return true;
   1739     }
   1740 
   1741     // ---- Populate data structures with themes, locales, etc ----
   1742 
   1743     /**
   1744      * Updates the internal list of themes.
   1745      */
   1746     private void updateThemes() {
   1747         if (mClient == null) {
   1748             return; // can't do anything without it.
   1749         }
   1750 
   1751         ResourceRepository frameworkRes = mClient.getFrameworkResources(
   1752                 mConfiguration.getTarget());
   1753 
   1754         mDisableUpdates++;
   1755 
   1756         try {
   1757             if (mEditedFile != null) {
   1758                 String theme = mConfiguration.getTheme();
   1759                 if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) {
   1760                     mConfiguration.setTheme(null);
   1761                     mConfiguration.computePreferredTheme();
   1762                 }
   1763                 assert mConfiguration.getTheme() != null;
   1764             }
   1765 
   1766             mThemeList.clear();
   1767 
   1768             ArrayList<String> themes = new ArrayList<String>();
   1769             ResourceRepository projectRes = mClient.getProjectResources();
   1770             // in cases where the opened file is not linked to a project, this could be null.
   1771             if (projectRes != null) {
   1772                 // get the configured resources for the project
   1773                 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
   1774                     mClient.getConfiguredProjectResources();
   1775 
   1776                 if (configuredProjectRes != null) {
   1777                     // get the styles.
   1778                     Map<String, ResourceValue> styleMap = configuredProjectRes.get(
   1779                             ResourceType.STYLE);
   1780 
   1781                     if (styleMap != null) {
   1782                         // collect the themes out of all the styles, ie styles that extend,
   1783                         // directly or indirectly a platform theme.
   1784                         for (ResourceValue value : styleMap.values()) {
   1785                             if (isTheme(value, styleMap, null)) {
   1786                                 String theme = value.getName();
   1787                                 themes.add(theme);
   1788                             }
   1789                         }
   1790 
   1791                         Collections.sort(themes);
   1792 
   1793                         for (String theme : themes) {
   1794                             if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1795                                 theme = STYLE_RESOURCE_PREFIX + theme;
   1796                             }
   1797                             mThemeList.add(theme);
   1798                         }
   1799                     }
   1800                 }
   1801                 themes.clear();
   1802             }
   1803 
   1804             // get the themes, and languages from the Framework.
   1805             if (frameworkRes != null) {
   1806                 // get the configured resources for the framework
   1807                 Map<ResourceType, Map<String, ResourceValue>> frameworResources =
   1808                     frameworkRes.getConfiguredResources(mConfiguration.getFullConfig());
   1809 
   1810                 if (frameworResources != null) {
   1811                     // get the styles.
   1812                     Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
   1813 
   1814                     // collect the themes out of all the styles.
   1815                     for (ResourceValue value : styles.values()) {
   1816                         String name = value.getName();
   1817                         if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$
   1818                             themes.add(value.getName());
   1819                         }
   1820                     }
   1821 
   1822                     // sort them and add them to the combo
   1823                     Collections.sort(themes);
   1824 
   1825                     for (String theme : themes) {
   1826                         if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1827                             theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1828                         }
   1829                         mThemeList.add(theme);
   1830                     }
   1831 
   1832                     themes.clear();
   1833                 }
   1834             }
   1835 
   1836             // Migration: In the past we didn't store the style prefix in the settings;
   1837             // this meant we might lose track of whether the theme is a project style
   1838             // or a framework style. For now we need to migrate. Search through the
   1839             // theme list until we have a match
   1840             String theme = mConfiguration.getTheme();
   1841             if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
   1842                 String projectStyle = STYLE_RESOURCE_PREFIX + theme;
   1843                 String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1844                 for (String t : mThemeList) {
   1845                     if (t.equals(projectStyle)) {
   1846                         mConfiguration.setTheme(projectStyle);
   1847                         break;
   1848                     } else if (t.equals(frameworkStyle)) {
   1849                         mConfiguration.setTheme(frameworkStyle);
   1850                         break;
   1851                     }
   1852                 }
   1853                 if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1854                     // Arbitrary guess
   1855                     if (theme.startsWith("Theme.")) {
   1856                         theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1857                     } else {
   1858                         theme = STYLE_RESOURCE_PREFIX + theme;
   1859                     }
   1860                 }
   1861             }
   1862 
   1863             // TODO: Handle the case where you have a theme persisted that isn't available??
   1864             // We could look up mConfiguration.theme and make sure it appears in the list! And if
   1865             // not, picking one.
   1866             selectTheme(mConfiguration.getTheme());
   1867         } finally {
   1868             mDisableUpdates--;
   1869         }
   1870     }
   1871 
   1872     private void updateActivity() {
   1873         if (mEditedFile != null) {
   1874             String preferred = getPreferredActivity(mEditedFile);
   1875             selectActivity(preferred);
   1876         }
   1877     }
   1878 
   1879     /**
   1880      * Updates the locale combo.
   1881      * This must be called from the UI thread.
   1882      */
   1883     public void updateLocales() {
   1884         if (mClient == null) {
   1885             return; // can't do anything w/o it.
   1886         }
   1887 
   1888         mDisableUpdates++;
   1889 
   1890         try {
   1891             mLocaleList.clear();
   1892 
   1893             SortedSet<String> languages = null;
   1894 
   1895             // get the languages from the project.
   1896             ResourceRepository projectRes = mClient.getProjectResources();
   1897 
   1898             // in cases where the opened file is not linked to a project, this could be null.
   1899             if (projectRes != null) {
   1900                 // now get the languages from the project.
   1901                 languages = projectRes.getLanguages();
   1902 
   1903                 for (String language : languages) {
   1904                     LanguageQualifier langQual = new LanguageQualifier(language);
   1905 
   1906                     // find the matching regions and add them
   1907                     SortedSet<String> regions = projectRes.getRegions(language);
   1908                     for (String region : regions) {
   1909                         RegionQualifier regionQual = new RegionQualifier(region);
   1910                         mLocaleList.add(Locale.create(langQual, regionQual));
   1911                     }
   1912 
   1913                     // now the entry for the other regions the language alone
   1914                     // create a region qualifier that will never be matched by qualified resources.
   1915                     mLocaleList.add(Locale.create(langQual));
   1916                 }
   1917             }
   1918 
   1919             // create language/region qualifier that will never be matched by qualified resources.
   1920             mLocaleList.add(Locale.ANY);
   1921 
   1922             Locale locale = mConfiguration.getLocale();
   1923             setLocale(locale);
   1924         } finally {
   1925             mDisableUpdates--;
   1926         }
   1927     }
   1928 
   1929     @Nullable
   1930     private String getPreferredActivity(@NonNull IFile file) {
   1931         // Store/restore the activity context in the config state to help with
   1932         // performance if for some reason we can't write it into the XML file and to
   1933         // avoid having to open the model below
   1934         if (mConfiguration.getActivity() != null) {
   1935             return mConfiguration.getActivity();
   1936         }
   1937 
   1938         IProject project = file.getProject();
   1939 
   1940         // Look up from XML file
   1941         Document document = DomUtilities.getDocument(file);
   1942         if (document != null) {
   1943             Element element = document.getDocumentElement();
   1944             if (element != null) {
   1945                 String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT);
   1946                 if (activity != null && !activity.isEmpty()) {
   1947                     if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$
   1948                         ManifestInfo manifest = ManifestInfo.get(project);
   1949                         String pkg = manifest.getPackage();
   1950                         if (!pkg.isEmpty()) {
   1951                             if (activity.startsWith(".")) { //$NON-NLS-1$
   1952                                 activity = pkg + activity;
   1953                             } else {
   1954                                 activity = activity + '.' + pkg;
   1955                             }
   1956                         }
   1957                     }
   1958 
   1959                     mConfiguration.setActivity(activity);
   1960                     saveConstraints();
   1961                     return activity;
   1962                 }
   1963             }
   1964         }
   1965 
   1966         // No, not available there: try to infer it from the code index
   1967         String includedIn = null;
   1968         Reference includedWithin = mClient.getIncludedWithin();
   1969         if (mClient != null && includedWithin != null) {
   1970             includedIn = includedWithin.getName();
   1971         }
   1972 
   1973         ManifestInfo manifest = ManifestInfo.get(project);
   1974         String pkg = manifest.getPackage();
   1975         String layoutName = ResourceHelper.getLayoutName(mEditedFile);
   1976 
   1977         // If we are rendering a layout in included context, pick the theme
   1978         // from the outer layout instead
   1979         if (includedIn != null) {
   1980             layoutName = includedIn;
   1981         }
   1982 
   1983         String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
   1984 
   1985         if (activity == null) {
   1986             List<String> activities = ManifestInfo.getProjectActivities(project);
   1987             if (activities.size() == 1) {
   1988                 activity = activities.get(0);
   1989             }
   1990         }
   1991 
   1992         if (activity != null) {
   1993             mConfiguration.setActivity(activity);
   1994             saveConstraints();
   1995             return activity;
   1996         }
   1997 
   1998         // TODO: Do anything else, such as pick the first activity found?
   1999         // Or just leave some default label instead?
   2000         // Also, figure out what to store in the mState so I don't keep trying
   2001 
   2002         return null;
   2003     }
   2004 
   2005     /**
   2006      * Returns whether the given <var>style</var> is a theme.
   2007      * This is done by making sure the parent is a theme.
   2008      * @param value the style to check
   2009      * @param styleMap the map of styles for the current project. Key is the style name.
   2010      * @param seen the map of styles we have already processed (or null if not yet
   2011      *          initialized). Only the keys are significant (since there is no IdentityHashSet).
   2012      * @return True if the given <var>style</var> is a theme.
   2013      */
   2014     private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap,
   2015             IdentityHashMap<ResourceValue, Boolean> seen) {
   2016         if (value instanceof StyleResourceValue) {
   2017             StyleResourceValue style = (StyleResourceValue)value;
   2018 
   2019             boolean frameworkStyle = false;
   2020             String parentStyle = style.getParentStyle();
   2021             if (parentStyle == null) {
   2022                 // if there is no specified parent style we look an implied one.
   2023                 // For instance 'Theme.light' is implied child style of 'Theme',
   2024                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
   2025                 String name = style.getName();
   2026                 int index = name.lastIndexOf('.');
   2027                 if (index != -1) {
   2028                     parentStyle = name.substring(0, index);
   2029                 }
   2030             } else {
   2031                 // remove the useless @ if it's there
   2032                 if (parentStyle.startsWith("@")) {
   2033                     parentStyle = parentStyle.substring(1);
   2034                 }
   2035 
   2036                 // check for framework identifier.
   2037                 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
   2038                     frameworkStyle = true;
   2039                     parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
   2040                 }
   2041 
   2042                 // at this point we could have the format style/<name>. we want only the name
   2043                 if (parentStyle.startsWith("style/")) {
   2044                     parentStyle = parentStyle.substring("style/".length());
   2045                 }
   2046             }
   2047 
   2048             if (parentStyle != null) {
   2049                 if (frameworkStyle) {
   2050                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
   2051                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
   2052                 } else {
   2053                     // if it's a project style, we check this is a theme.
   2054                     ResourceValue parentValue = styleMap.get(parentStyle);
   2055 
   2056                     // also prevent stack overflow in case the dev mistakenly declared
   2057                     // the parent of the style as the style itself.
   2058                     if (parentValue != null && !parentValue.equals(value)) {
   2059                         if (seen == null) {
   2060                             seen = new IdentityHashMap<ResourceValue, Boolean>();
   2061                             seen.put(value, Boolean.TRUE);
   2062                         } else if (seen.containsKey(parentValue)) {
   2063                             return false;
   2064                         }
   2065                         seen.put(parentValue, Boolean.TRUE);
   2066                         return isTheme(parentValue, styleMap, seen);
   2067                     }
   2068                 }
   2069             }
   2070         }
   2071 
   2072         return false;
   2073     }
   2074 
   2075     /**
   2076      * Returns true if this configuration chooser represents the best match for
   2077      * the given file
   2078      *
   2079      * @param file the file to test
   2080      * @param config the config to test
   2081      * @return true if the given config is the best match for the given file
   2082      */
   2083     public boolean isBestMatchFor(IFile file, FolderConfiguration config) {
   2084         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
   2085                 ResourceType.LAYOUT, config);
   2086         if (match != null) {
   2087             return match.getFile().equals(mEditedFile);
   2088         }
   2089 
   2090         return false;
   2091     }
   2092 }
   2093