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