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