• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ConfigurationChooser( @onNull ConfigurationClient client, Composite parent, @Nullable String initialState)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
getEditedFile()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
getProject()319     public IProject getProject() {
320         if (mEditedFile != null) {
321             return mEditedFile.getProject();
322         } else {
323             return null;
324         }
325     }
326 
getClient()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
getResources()338     public ProjectResources getResources() {
339         return mResources;
340     }
341 
342     /**
343      * Returns the full, complete {@link FolderConfiguration}
344      *
345      * @return the full configuration
346      */
getFullConfiguration()347     public FolderConfiguration getFullConfiguration() {
348         return mConfiguration.getFullConfig();
349     }
350 
351     /**
352      * Returns the project target
353      *
354      * @return the project target
355      */
getProjectTarget()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      */
getConfiguration()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
getLocaleList()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
getDeviceList()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
getTargetList()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
getTarget()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
getDeviceName()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
getThemeName()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 */
gotoNextState()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
widgetDisposed(DisposeEvent e)458     public void widgetDisposed(DisposeEvent e) {
459         dispose();
460     }
461 
462     @Override
dispose()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      */
setFile(IFile file)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      */
replaceFile(IFile file)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      */
changeFileOnNewConfig(IFile file)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      */
resetConfigFor(IFile file)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      */
setConfiguration(@onNull Configuration configuration)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      */
onSdkLoaded(IAndroidTarget target)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      */
onXmlModelLoaded()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      */
ensureVisible()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      */
onAlternateLayoutCreated()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      */
initDevices()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      */
initTargets()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 */
ensureInitialized()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 
updateDevices()916     private void updateDevices() {
917         if (mDeviceList.size() == 0) {
918             initDevices();
919         }
920     }
921 
updateTargets()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      */
resizeToolBar()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 
getOrientationIcon(ScreenOrientation orientation, boolean flip)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 
getOrientationImage(ScreenOrientation orientation, boolean flip)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
getOrientation(State state)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      */
saveConstraints()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 
selectDeviceState(@ullable State state)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 
selectTarget(IAndroidTarget target)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      */
selectDevice(@ullable Device device)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 
selectActivity(@ullable String fqcn)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 
selectTheme(@ullable String theme)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 
selectLocale(@ullable Locale locale)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
getFlagImage(@ullable Locale locale)1129     Image getFlagImage(@Nullable Locale locale) {
1130         if (locale != null) {
1131             return locale.getFlagImage();
1132         }
1133 
1134         return LocaleManager.getGlobeIcon();
1135     }
1136 
selectConfiguration(FolderConfiguration fileConfig)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      */
setLocale(@ullable Locale locale)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      */
getActivityLabel(String fqcn, boolean brief)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      */
getThemeLabel(String theme, boolean brief)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      */
getRenderingTargetLabel(IAndroidTarget target, boolean brief)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      */
getDeviceLabel(@ullable Device device, boolean brief)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
getLocaleLabel( @ullable ConfigurationChooser chooser, @Nullable Locale locale, boolean brief)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
onDevicesChanged()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      */
onDeviceChange()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      */
syncToVariations( final int flags, final @NonNull IFile updatedFile, final @NonNull Configuration base, final boolean includeSelf, boolean async)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 
doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf, Configuration base)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      */
onDeviceConfigChange()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      */
onLocaleChange()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 
onThemeChange()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 
notifyFolderConfigChanged()1564     void notifyFolderConfigChanged() {
1565         if (mDisableUpdates > 0 || mClient == null) {
1566             return;
1567         }
1568 
1569         if (mClient.changed(CFG_FOLDER)) {
1570             saveConstraints();
1571         }
1572     }
1573 
onSelectActivity()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      */
onRenderingTargetChange()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      */
syncRenderState()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      */
updateThemes()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 
updateActivity()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      */
updateLocales()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
getPreferredActivity(@onNull IFile file)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      */
isTheme(ResourceValue value, Map<String, ResourceValue> styleMap, IdentityHashMap<ResourceValue, Boolean> seen)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      */
isBestMatchFor(IFile file, FolderConfiguration config)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