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