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.LocaleQualifier;
     47 import com.android.ide.common.resources.configuration.ResourceQualifier;
     48 import com.android.ide.common.sdk.LoadStatus;
     49 import com.android.ide.eclipse.adt.AdtPlugin;
     50 import com.android.ide.eclipse.adt.AdtUtils;
     51 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     52 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
     53 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
     54 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     55 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     56 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     57 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
     58 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
     59 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     60 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
     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 Collection<Device> mDevices = 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 Collection<Device> getDevices() {
    386         return mDevices;
    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             mDevices = manager.getDevices(DeviceManager.ALL_DEVICES);
    876         } else {
    877             mDevices = 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 (mDevices.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.qualifier.getLanguage();
   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.qualifier.getRegion();
   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             mDevices = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES);
   1373         } else {
   1374             mDevices = 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         String preferred = null;
   1590         ActivityAttributes attributes = manifest.getActivityAttributes(activity);
   1591         if (attributes != null) {
   1592             preferred = attributes.getTheme();
   1593         }
   1594         if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) {
   1595             // Yes, switch to it
   1596             selectTheme(preferred);
   1597             onThemeChange();
   1598         }
   1599 
   1600         // Persist in XML
   1601         if (mClient != null) {
   1602             mClient.setActivity(activity);
   1603         }
   1604 
   1605         saveConstraints();
   1606     }
   1607 
   1608     /**
   1609      * Call back for api level combo selection
   1610      */
   1611     void onRenderingTargetChange() {
   1612         // because mApiCombo triggers onApiLevelChange at each modification, the filling
   1613         // of the combo with data will trigger notifications, and we don't want that.
   1614         if (mDisableUpdates > 0) {
   1615             return;
   1616         }
   1617 
   1618         IAndroidTarget prevTarget = mConfiguration.getTarget();
   1619         String prevTheme = mConfiguration.getTheme();
   1620 
   1621         int changeFlags = 0;
   1622 
   1623         // tell the listener a new rendering target is being set. Need to do this before updating
   1624         // mRenderingTarget.
   1625         if (prevTarget != null) {
   1626             changeFlags |= CFG_TARGET;
   1627             mClient.aboutToChange(changeFlags);
   1628         }
   1629 
   1630         IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData();
   1631         mConfiguration.setTarget(target, true);
   1632 
   1633         // force a theme update to reflect the new rendering target.
   1634         // This must be done after computeCurrentConfig since it'll depend on the currentConfig
   1635         // to figure out the theme list.
   1636         String oldTheme = mConfiguration.getTheme();
   1637         updateThemes();
   1638         // updateThemes may change the theme (based on theme availability in the new rendering
   1639         // target) so mark theme change if necessary
   1640         if (!Objects.equal(oldTheme, mConfiguration.getTheme())) {
   1641             changeFlags |= CFG_THEME;
   1642         }
   1643 
   1644         if (target != null) {
   1645             changeFlags |= CFG_TARGET;
   1646             changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier
   1647         }
   1648 
   1649         // Store project-wide render-target setting
   1650         mConfiguration.saveRenderState();
   1651 
   1652         mConfiguration.syncFolderConfig();
   1653 
   1654         if (mClient != null) {
   1655             boolean accepted = mClient.changed(changeFlags);
   1656             if (!accepted) {
   1657                 mConfiguration.setTarget(prevTarget, true);
   1658                 mConfiguration.setTheme(prevTheme);
   1659                 mConfiguration.syncFolderConfig();
   1660                 selectTheme(prevTheme);
   1661                 selectTarget(prevTarget);
   1662             }
   1663         }
   1664     }
   1665 
   1666     /**
   1667      * Syncs this configuration to the project wide locale and render target settings. The
   1668      * locale may ignore the project-wide setting if it is a locale-specific
   1669      * configuration.
   1670      *
   1671      * @return true if one or both of the toggles were changed, false if there were no
   1672      *         changes
   1673      */
   1674     public boolean syncRenderState() {
   1675         if (mConfiguration.getEditedConfig() == null) {
   1676             // Startup; ignore
   1677             return false;
   1678         }
   1679 
   1680         boolean renderTargetChanged = false;
   1681 
   1682         // When a page is re-activated, force the toggles to reflect the current project
   1683         // state
   1684 
   1685         Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this);
   1686 
   1687         int changeFlags = 0;
   1688         // Only sync the locale if this layout is not already a locale-specific layout!
   1689         if (pair != null && !mConfiguration.isLocaleSpecificLayout()) {
   1690             Locale locale = pair.getFirst();
   1691             if (locale != null) {
   1692                 boolean localeChanged = setLocale(locale);
   1693                 if (localeChanged) {
   1694                     changeFlags |= CFG_LOCALE;
   1695                 }
   1696             } else {
   1697                 locale = Locale.ANY;
   1698             }
   1699             mConfiguration.setLocale(locale, true);
   1700         }
   1701 
   1702         // Sync render target
   1703         IAndroidTarget configurationTarget = mConfiguration.getTarget();
   1704         IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget;
   1705         if (target != null && configurationTarget != target) {
   1706             if (mClient != null && configurationTarget != null) {
   1707                 changeFlags |= CFG_TARGET;
   1708                 mClient.aboutToChange(changeFlags);
   1709             }
   1710 
   1711             mConfiguration.setTarget(target, true);
   1712             selectTarget(target);
   1713             renderTargetChanged = true;
   1714         }
   1715 
   1716         // Neither locale nor render target changed: nothing to do
   1717         if (changeFlags == 0) {
   1718             return false;
   1719         }
   1720 
   1721         // Update the locale and/or the render target. This code contains a logical
   1722         // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
   1723         // such that we don't duplicate work.
   1724 
   1725         // Compute the new configuration; we want to do this both for locale changes
   1726         // and for render targets.
   1727         mConfiguration.syncFolderConfig();
   1728         changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier
   1729 
   1730         if (renderTargetChanged) {
   1731             // force a theme update to reflect the new rendering target.
   1732             // This must be done after computeCurrentConfig since it'll depend on the currentConfig
   1733             // to figure out the theme list.
   1734             updateThemes();
   1735         }
   1736 
   1737         if (mClient != null) {
   1738             mClient.changed(changeFlags);
   1739         }
   1740 
   1741         return true;
   1742     }
   1743 
   1744     // ---- Populate data structures with themes, locales, etc ----
   1745 
   1746     /**
   1747      * Updates the internal list of themes.
   1748      */
   1749     private void updateThemes() {
   1750         if (mClient == null) {
   1751             return; // can't do anything without it.
   1752         }
   1753 
   1754         ResourceRepository frameworkRes = mClient.getFrameworkResources(
   1755                 mConfiguration.getTarget());
   1756 
   1757         mDisableUpdates++;
   1758 
   1759         try {
   1760             if (mEditedFile != null) {
   1761                 String theme = mConfiguration.getTheme();
   1762                 if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) {
   1763                     mConfiguration.setTheme(null);
   1764                     mConfiguration.computePreferredTheme();
   1765                 }
   1766                 assert mConfiguration.getTheme() != null;
   1767             }
   1768 
   1769             mThemeList.clear();
   1770 
   1771             ArrayList<String> themes = new ArrayList<String>();
   1772             ResourceRepository projectRes = mClient.getProjectResources();
   1773             // in cases where the opened file is not linked to a project, this could be null.
   1774             if (projectRes != null) {
   1775                 // get the configured resources for the project
   1776                 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
   1777                     mClient.getConfiguredProjectResources();
   1778 
   1779                 if (configuredProjectRes != null) {
   1780                     // get the styles.
   1781                     Map<String, ResourceValue> styleMap = configuredProjectRes.get(
   1782                             ResourceType.STYLE);
   1783 
   1784                     if (styleMap != null) {
   1785                         // collect the themes out of all the styles, ie styles that extend,
   1786                         // directly or indirectly a platform theme.
   1787                         for (ResourceValue value : styleMap.values()) {
   1788                             if (isTheme(value, styleMap, null)) {
   1789                                 String theme = value.getName();
   1790                                 themes.add(theme);
   1791                             }
   1792                         }
   1793 
   1794                         Collections.sort(themes);
   1795 
   1796                         for (String theme : themes) {
   1797                             if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1798                                 theme = STYLE_RESOURCE_PREFIX + theme;
   1799                             }
   1800                             mThemeList.add(theme);
   1801                         }
   1802                     }
   1803                 }
   1804                 themes.clear();
   1805             }
   1806 
   1807             // get the themes, and languages from the Framework.
   1808             if (frameworkRes != null) {
   1809                 // get the configured resources for the framework
   1810                 Map<ResourceType, Map<String, ResourceValue>> frameworResources =
   1811                     frameworkRes.getConfiguredResources(mConfiguration.getFullConfig());
   1812 
   1813                 if (frameworResources != null) {
   1814                     // get the styles.
   1815                     Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
   1816 
   1817                     // collect the themes out of all the styles.
   1818                     for (ResourceValue value : styles.values()) {
   1819                         String name = value.getName();
   1820                         if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$
   1821                             themes.add(value.getName());
   1822                         }
   1823                     }
   1824 
   1825                     // sort them and add them to the combo
   1826                     Collections.sort(themes);
   1827 
   1828                     for (String theme : themes) {
   1829                         if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1830                             theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1831                         }
   1832                         mThemeList.add(theme);
   1833                     }
   1834 
   1835                     themes.clear();
   1836                 }
   1837             }
   1838 
   1839             // Migration: In the past we didn't store the style prefix in the settings;
   1840             // this meant we might lose track of whether the theme is a project style
   1841             // or a framework style. For now we need to migrate. Search through the
   1842             // theme list until we have a match
   1843             String theme = mConfiguration.getTheme();
   1844             if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
   1845                 String projectStyle = STYLE_RESOURCE_PREFIX + theme;
   1846                 String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1847                 for (String t : mThemeList) {
   1848                     if (t.equals(projectStyle)) {
   1849                         mConfiguration.setTheme(projectStyle);
   1850                         break;
   1851                     } else if (t.equals(frameworkStyle)) {
   1852                         mConfiguration.setTheme(frameworkStyle);
   1853                         break;
   1854                     }
   1855                 }
   1856                 if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
   1857                     // Arbitrary guess
   1858                     if (theme.startsWith("Theme.")) {
   1859                         theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
   1860                     } else {
   1861                         theme = STYLE_RESOURCE_PREFIX + theme;
   1862                     }
   1863                 }
   1864             }
   1865 
   1866             // TODO: Handle the case where you have a theme persisted that isn't available??
   1867             // We could look up mConfiguration.theme and make sure it appears in the list! And if
   1868             // not, picking one.
   1869             selectTheme(mConfiguration.getTheme());
   1870         } finally {
   1871             mDisableUpdates--;
   1872         }
   1873     }
   1874 
   1875     private void updateActivity() {
   1876         if (mEditedFile != null) {
   1877             String preferred = getPreferredActivity(mEditedFile);
   1878             selectActivity(preferred);
   1879         }
   1880     }
   1881 
   1882     /**
   1883      * Updates the locale combo.
   1884      * This must be called from the UI thread.
   1885      */
   1886     public void updateLocales() {
   1887         if (mClient == null) {
   1888             return; // can't do anything w/o it.
   1889         }
   1890 
   1891         mDisableUpdates++;
   1892 
   1893         try {
   1894             mLocaleList.clear();
   1895 
   1896             SortedSet<String> languages = null;
   1897 
   1898             // get the languages from the project.
   1899             ResourceRepository projectRes = mClient.getProjectResources();
   1900 
   1901             // in cases where the opened file is not linked to a project, this could be null.
   1902             if (projectRes != null) {
   1903                 // now get the languages from the project.
   1904                 languages = projectRes.getLanguages();
   1905 
   1906                 for (String language : languages) {
   1907                     // find the matching regions and add them
   1908                     SortedSet<String> regions = projectRes.getRegions(language);
   1909                     for (String region : regions) {
   1910                         LocaleQualifier locale = LocaleQualifier.getQualifier(language + "-r" + region);
   1911                         if (locale != null) {
   1912                             mLocaleList.add(Locale.create(locale));
   1913                         }
   1914                     }
   1915 
   1916                     // now the entry for the other regions the language alone
   1917                     // create a region qualifier that will never be matched by qualified resources.
   1918                     LocaleQualifier locale = new LocaleQualifier(language);
   1919                     mLocaleList.add(Locale.create(locale));
   1920                 }
   1921             }
   1922 
   1923             // create language/region qualifier that will never be matched by qualified resources.
   1924             mLocaleList.add(Locale.ANY);
   1925 
   1926             Locale locale = mConfiguration.getLocale();
   1927             setLocale(locale);
   1928         } finally {
   1929             mDisableUpdates--;
   1930         }
   1931     }
   1932 
   1933     @Nullable
   1934     private String getPreferredActivity(@NonNull IFile file) {
   1935         // Store/restore the activity context in the config state to help with
   1936         // performance if for some reason we can't write it into the XML file and to
   1937         // avoid having to open the model below
   1938         if (mConfiguration.getActivity() != null) {
   1939             return mConfiguration.getActivity();
   1940         }
   1941 
   1942         IProject project = file.getProject();
   1943 
   1944         // Look up from XML file
   1945         Document document = DomUtilities.getDocument(file);
   1946         if (document != null) {
   1947             Element element = document.getDocumentElement();
   1948             if (element != null) {
   1949                 String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT);
   1950                 if (activity != null && !activity.isEmpty()) {
   1951                     if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$
   1952                         ManifestInfo manifest = ManifestInfo.get(project);
   1953                         String pkg = manifest.getPackage();
   1954                         if (!pkg.isEmpty()) {
   1955                             if (activity.startsWith(".")) { //$NON-NLS-1$
   1956                                 activity = pkg + activity;
   1957                             } else {
   1958                                 activity = activity + '.' + pkg;
   1959                             }
   1960                         }
   1961                     }
   1962 
   1963                     mConfiguration.setActivity(activity);
   1964                     saveConstraints();
   1965                     return activity;
   1966                 }
   1967             }
   1968         }
   1969 
   1970         // No, not available there: try to infer it from the code index
   1971         String includedIn = null;
   1972         Reference includedWithin = mClient.getIncludedWithin();
   1973         if (mClient != null && includedWithin != null) {
   1974             includedIn = includedWithin.getName();
   1975         }
   1976 
   1977         ManifestInfo manifest = ManifestInfo.get(project);
   1978         String pkg = manifest.getPackage();
   1979         String layoutName = ResourceHelper.getLayoutName(mEditedFile);
   1980 
   1981         // If we are rendering a layout in included context, pick the theme
   1982         // from the outer layout instead
   1983         if (includedIn != null) {
   1984             layoutName = includedIn;
   1985         }
   1986 
   1987         String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
   1988 
   1989         if (activity == null) {
   1990             List<String> activities = ManifestInfo.getProjectActivities(project);
   1991             if (activities.size() == 1) {
   1992                 activity = activities.get(0);
   1993             }
   1994         }
   1995 
   1996         if (activity != null) {
   1997             mConfiguration.setActivity(activity);
   1998             saveConstraints();
   1999             return activity;
   2000         }
   2001 
   2002         // TODO: Do anything else, such as pick the first activity found?
   2003         // Or just leave some default label instead?
   2004         // Also, figure out what to store in the mState so I don't keep trying
   2005 
   2006         return null;
   2007     }
   2008 
   2009     /**
   2010      * Returns whether the given <var>style</var> is a theme.
   2011      * This is done by making sure the parent is a theme.
   2012      * @param value the style to check
   2013      * @param styleMap the map of styles for the current project. Key is the style name.
   2014      * @param seen the map of styles we have already processed (or null if not yet
   2015      *          initialized). Only the keys are significant (since there is no IdentityHashSet).
   2016      * @return True if the given <var>style</var> is a theme.
   2017      */
   2018     private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap,
   2019             IdentityHashMap<ResourceValue, Boolean> seen) {
   2020         if (value instanceof StyleResourceValue) {
   2021             StyleResourceValue style = (StyleResourceValue)value;
   2022 
   2023             boolean frameworkStyle = false;
   2024             String parentStyle = style.getParentStyle();
   2025             if (parentStyle == null) {
   2026                 // if there is no specified parent style we look an implied one.
   2027                 // For instance 'Theme.light' is implied child style of 'Theme',
   2028                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
   2029                 String name = style.getName();
   2030                 int index = name.lastIndexOf('.');
   2031                 if (index != -1) {
   2032                     parentStyle = name.substring(0, index);
   2033                 }
   2034             } else {
   2035                 // remove the useless @ if it's there
   2036                 if (parentStyle.startsWith("@")) {
   2037                     parentStyle = parentStyle.substring(1);
   2038                 }
   2039 
   2040                 // check for framework identifier.
   2041                 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
   2042                     frameworkStyle = true;
   2043                     parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
   2044                 }
   2045 
   2046                 // at this point we could have the format style/<name>. we want only the name
   2047                 if (parentStyle.startsWith("style/")) {
   2048                     parentStyle = parentStyle.substring("style/".length());
   2049                 }
   2050             }
   2051 
   2052             if (parentStyle != null) {
   2053                 if (frameworkStyle) {
   2054                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
   2055                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
   2056                 } else {
   2057                     // if it's a project style, we check this is a theme.
   2058                     ResourceValue parentValue = styleMap.get(parentStyle);
   2059 
   2060                     // also prevent stack overflow in case the dev mistakenly declared
   2061                     // the parent of the style as the style itself.
   2062                     if (parentValue != null && !parentValue.equals(value)) {
   2063                         if (seen == null) {
   2064                             seen = new IdentityHashMap<ResourceValue, Boolean>();
   2065                             seen.put(value, Boolean.TRUE);
   2066                         } else if (seen.containsKey(parentValue)) {
   2067                             return false;
   2068                         }
   2069                         seen.put(parentValue, Boolean.TRUE);
   2070                         return isTheme(parentValue, styleMap, seen);
   2071                     }
   2072                 }
   2073             }
   2074         }
   2075 
   2076         return false;
   2077     }
   2078 
   2079     /**
   2080      * Returns true if this configuration chooser represents the best match for
   2081      * the given file
   2082      *
   2083      * @param file the file to test
   2084      * @param config the config to test
   2085      * @return true if the given config is the best match for the given file
   2086      */
   2087     public boolean isBestMatchFor(IFile file, FolderConfiguration config) {
   2088         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
   2089                 ResourceType.LAYOUT, config);
   2090         if (match != null) {
   2091             return match.getFile().equals(mEditedFile);
   2092         }
   2093 
   2094         return false;
   2095     }
   2096 }
   2097