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