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