• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.ide.common.layout.LayoutConstants.ANDROID_NS_NAME_PREFIX;
20 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_STYLE;
21 
22 import com.android.ide.common.api.Rect;
23 import com.android.ide.common.rendering.api.ResourceValue;
24 import com.android.ide.common.rendering.api.StyleResourceValue;
25 import com.android.ide.common.resources.ResourceFile;
26 import com.android.ide.common.resources.ResourceFolder;
27 import com.android.ide.common.resources.ResourceRepository;
28 import com.android.ide.common.resources.configuration.DensityQualifier;
29 import com.android.ide.common.resources.configuration.FolderConfiguration;
30 import com.android.ide.common.resources.configuration.LanguageQualifier;
31 import com.android.ide.common.resources.configuration.NightModeQualifier;
32 import com.android.ide.common.resources.configuration.RegionQualifier;
33 import com.android.ide.common.resources.configuration.ResourceQualifier;
34 import com.android.ide.common.resources.configuration.ScreenDimensionQualifier;
35 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
36 import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
37 import com.android.ide.common.resources.configuration.UiModeQualifier;
38 import com.android.ide.common.resources.configuration.VersionQualifier;
39 import com.android.ide.common.sdk.LoadStatus;
40 import com.android.ide.eclipse.adt.AdtPlugin;
41 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
42 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
43 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
44 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
45 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
46 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
47 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
48 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
49 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
50 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
51 import com.android.resources.Density;
52 import com.android.resources.NightMode;
53 import com.android.resources.ResourceFolderType;
54 import com.android.resources.ResourceType;
55 import com.android.resources.ScreenOrientation;
56 import com.android.resources.ScreenSize;
57 import com.android.resources.UiMode;
58 import com.android.sdklib.AndroidVersion;
59 import com.android.sdklib.IAndroidTarget;
60 import com.android.sdklib.repository.PkgProps;
61 import com.android.sdklib.util.SparseIntArray;
62 import com.android.util.Pair;
63 
64 import org.eclipse.core.resources.IFile;
65 import org.eclipse.core.resources.IFolder;
66 import org.eclipse.core.resources.IProject;
67 import org.eclipse.core.runtime.CoreException;
68 import org.eclipse.core.runtime.IStatus;
69 import org.eclipse.core.runtime.QualifiedName;
70 import org.eclipse.swt.SWT;
71 import org.eclipse.swt.events.SelectionAdapter;
72 import org.eclipse.swt.events.SelectionEvent;
73 import org.eclipse.swt.layout.GridData;
74 import org.eclipse.swt.layout.GridLayout;
75 import org.eclipse.swt.widgets.Button;
76 import org.eclipse.swt.widgets.Combo;
77 import org.eclipse.swt.widgets.Composite;
78 import org.eclipse.swt.widgets.Label;
79 import org.eclipse.ui.IEditorPart;
80 import org.eclipse.ui.IWorkbench;
81 import org.eclipse.ui.IWorkbenchPage;
82 import org.eclipse.ui.IWorkbenchWindow;
83 import org.eclipse.ui.PlatformUI;
84 
85 import java.util.ArrayList;
86 import java.util.Collections;
87 import java.util.Comparator;
88 import java.util.HashSet;
89 import java.util.List;
90 import java.util.Locale;
91 import java.util.Map;
92 import java.util.Set;
93 import java.util.SortedSet;
94 
95 /**
96  * A composite that displays the current configuration displayed in a Graphical Layout Editor.
97  * <p/>
98  * The composite has several entry points:<br>
99  * - {@link #setFile(IFile)}<br>
100  *   Called after the constructor to set the file being edited. Nothing else is performed.<br>
101  *<br>
102  * - {@link #onXmlModelLoaded()}<br>
103  *   Called when the XML model is loaded, either the first time or when the Target/SDK changes.
104  *   This initializes the UI, either with the first compatible configuration found, or attempts
105  *   to restore a configuration if one is found to have been saved in the file persistent storage.
106  *   (see {@link #storeState()})<br>
107  *<br>
108  * - {@link #replaceFile(IFile)}<br>
109  *   Called when a file, representing the same resource but with a different config is opened<br>
110  *   by the user.<br>
111  *<br>
112  * - {@link #changeFileOnNewConfig(IFile)}<br>
113  *   Called when config change triggers the editing of a file with a different config.
114  *<p/>
115  * Additionally, the composite can handle the following events.<br>
116  * - SDK reload. This is when the main SDK is finished loading.<br>
117  * - Target reload. This is when the target used by the project is the edited file has finished<br>
118  *   loading.<br>
119  */
120 public class ConfigurationComposite extends Composite {
121     private final static String SEP = ":"; //$NON-NLS-1$
122     private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
123 
124     /**
125      * Setting name for project-wide setting controlling rendering target and locale which
126      * is shared for all files
127      */
128     public final static QualifiedName NAME_RENDER_STATE =
129         new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$
130 
131     /**
132      * Settings name for file-specific configuration preferences, such as which theme or
133      * device to render the current layout with
134      */
135     public final static QualifiedName NAME_CONFIG_STATE =
136         new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
137 
138     private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
139 
140     private final static int LOCALE_LANG = 0;
141     private final static int LOCALE_REGION = 1;
142 
143     private Label mCurrentLayoutLabel;
144     private Button mCreateButton;
145 
146     private Combo mDeviceCombo;
147     private Combo mDeviceConfigCombo;
148     private Combo mLocaleCombo;
149     private Combo mUiModeCombo;
150     private Combo mNightCombo;
151     private Combo mThemeCombo;
152     private Combo mTargetCombo;
153 
154     /**
155      * List of booleans, matching item for item the theme names in the mThemeCombo
156      * combobox, where each boolean represents whether the corresponding theme is a
157      * project theme
158      */
159     private List<Boolean> mIsProjectTheme = new ArrayList<Boolean>(40);
160 
161     /** updates are disabled if > 0 */
162     private int mDisableUpdates = 0;
163 
164     private List<LayoutDevice> mDeviceList;
165     private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
166 
167     private final ArrayList<ResourceQualifier[] > mLocaleList =
168         new ArrayList<ResourceQualifier[]>();
169 
170     private final ConfigState mState = new ConfigState();
171 
172     private boolean mSdkChanged = false;
173     private boolean mFirstXmlModelChange = true;
174 
175     /** The config listener given to the constructor. Never null. */
176     private final IConfigListener mListener;
177 
178     /** The {@link FolderConfiguration} representing the state of the UI controls */
179     private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
180 
181     /** The file being edited */
182     private IFile mEditedFile;
183     /** The {@link ProjectResources} for the edited file's project */
184     private ProjectResources mResources;
185     /** The target of the project of the file being edited. */
186     private IAndroidTarget mProjectTarget;
187     /** The target of the project of the file being edited. */
188     private IAndroidTarget mRenderingTarget;
189     /** The {@link FolderConfiguration} being edited. */
190     private FolderConfiguration mEditedConfig;
191     /** Serialized state to use when initializing the configuration after the SDK is loaded */
192     private String mInitialState;
193 
194     /**
195      * Interface implemented by the part which owns a {@link ConfigurationComposite}.
196      * This notifies the owners when the configuration change.
197      * The owner must also provide methods to provide the configuration that will
198      * be displayed.
199      */
200     public interface IConfigListener {
201         /**
202          * Called when the {@link FolderConfiguration} change. The new config can be queried
203          * with {@link ConfigurationComposite#getCurrentConfig()}.
204          */
onConfigurationChange()205         void onConfigurationChange();
206 
207         /**
208          * Called after a device has changed (in addition to {@link #onConfigurationChange}
209          * getting called)
210          */
onDevicePostChange()211         void onDevicePostChange();
212 
213         /**
214          * Called when the current theme changes. The theme can be queried with
215          * {@link ConfigurationComposite#getTheme()}.
216          */
onThemeChange()217         void onThemeChange();
218 
219         /**
220          * Called when the "Create" button is clicked.
221          */
onCreate()222         void onCreate();
223 
224         /**
225          * Called before the rendering target changes.
226          * @param oldTarget the old rendering target
227          */
onRenderingTargetPreChange(IAndroidTarget oldTarget)228         void onRenderingTargetPreChange(IAndroidTarget oldTarget);
229 
230         /**
231          * Called after the rendering target changes.
232          *
233          * @param target the new rendering target
234          */
onRenderingTargetPostChange(IAndroidTarget target)235         void onRenderingTargetPostChange(IAndroidTarget target);
236 
getProjectResources()237         ResourceRepository getProjectResources();
getFrameworkResources()238         ResourceRepository getFrameworkResources();
getFrameworkResources(IAndroidTarget target)239         ResourceRepository getFrameworkResources(IAndroidTarget target);
getConfiguredProjectResources()240         Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
getConfiguredFrameworkResources()241         Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
getIncludedWithin()242         String getIncludedWithin();
243     }
244 
245     /**
246      * State of the current config. This is used during UI reset to attempt to return the
247      * rendering to its original configuration.
248      */
249     private class ConfigState {
250         LayoutDevice device;
251         String configName;
252         ResourceQualifier[] locale;
253         String theme;
254         /** UI mode. Guaranteed to be non null */
255         UiMode uiMode = UiMode.NORMAL;
256         /** night mode. Guaranteed to be non null */
257         NightMode night = NightMode.NOTNIGHT;
258         /** the version being targeted for rendering */
259         IAndroidTarget target;
260 
getData()261         String getData() {
262             StringBuilder sb = new StringBuilder();
263             if (device != null) {
264                 sb.append(device.getName());
265                 sb.append(SEP);
266                 sb.append(configName);
267                 sb.append(SEP);
268                 if (isLocaleSpecificLayout() && locale != null) {
269                     if (locale[0] != null && locale[1] != null) {
270                         // locale[0]/[1] can be null sometimes when starting Eclipse
271                         sb.append(((LanguageQualifier) locale[0]).getValue());
272                         sb.append(SEP_LOCALE);
273                         sb.append(((RegionQualifier) locale[1]).getValue());
274                     }
275                 }
276                 sb.append(SEP);
277                 sb.append(theme);
278                 sb.append(SEP);
279                 sb.append(uiMode.getResourceValue());
280                 sb.append(SEP);
281                 sb.append(night.getResourceValue());
282                 sb.append(SEP);
283 
284                 // We used to store the render target here in R9. Leave a marker
285                 // to ensure that we don't reuse this slot; add new extra fields after it.
286                 sb.append(SEP);
287             }
288 
289             return sb.toString();
290         }
291 
setData(String data)292         boolean setData(String data) {
293             String[] values = data.split(SEP);
294             if (values.length == 6 || values.length == 7) {
295                 for (LayoutDevice d : mDeviceList) {
296                     if (d.getName().equals(values[0])) {
297                         device = d;
298                         FolderConfiguration config = device.getFolderConfigByName(values[1]);
299                         if (config != null) {
300                             configName = values[1];
301 
302                             // Load locale. Note that this can get overwritten by the
303                             // project-wide settings read below.
304                             locale = new ResourceQualifier[2];
305                             String locales[] = values[2].split(SEP_LOCALE);
306                             if (locales.length >= 2) {
307                                 if (locales[0].length() > 0) {
308                                     locale[0] = new LanguageQualifier(locales[0]);
309                                 }
310                                 if (locales[1].length() > 0) {
311                                     locale[1] = new RegionQualifier(locales[1]);
312                                 }
313                             }
314 
315                             theme = values[3];
316                             uiMode = UiMode.getEnum(values[4]);
317                             if (uiMode == null) {
318                                 uiMode = UiMode.NORMAL;
319                             }
320                             night = NightMode.getEnum(values[5]);
321                             if (night == null) {
322                                 night = NightMode.NOTNIGHT;
323                             }
324 
325                             // element 7/values[6]: used to store render target in R9.
326                             // No longer stored here. If adding more data, make
327                             // sure you leave 7 alone.
328 
329                             Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
330 
331                             // We only use the "global" setting
332                             if (!isLocaleSpecificLayout()) {
333                                 locale = pair.getFirst();
334                             }
335                             target = pair.getSecond();
336 
337                             return true;
338                         }
339                     }
340                 }
341             }
342 
343             return false;
344         }
345 
346         @Override
toString()347         public String toString() {
348             return getData();
349         }
350     }
351 
352     /**
353      * Returns a String id to represent an {@link IAndroidTarget} which can be translated
354      * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id
355      * will never contain the {@link #SEP} character.
356      *
357      * @param target the target to return an id for
358      * @return an id for the given target; never null
359      */
targetToString(IAndroidTarget target)360     private String targetToString(IAndroidTarget target) {
361         return target.getFullName().replace(SEP, "");  //$NON-NLS-1$
362     }
363 
364     /**
365      * Returns an {@link IAndroidTarget} that corresponds to the given id that was
366      * originally returned by {@link #targetToString}. May be null, if the platform is no
367      * longer available, or if the platform list has not yet been initialized.
368      *
369      * @param id the id that corresponds to the desired platform
370      * @return an {@link IAndroidTarget} that matches the given id, or null
371      */
stringToTarget(String id)372     private IAndroidTarget stringToTarget(String id) {
373         if (mTargetList != null && mTargetList.size() > 0) {
374             for (IAndroidTarget target : mTargetList) {
375                 if (id.equals(targetToString(target))) {
376                     return target;
377                 }
378             }
379         }
380 
381         return null;
382     }
383 
384     /**
385      * Creates a new {@link ConfigurationComposite} and adds it to the parent.
386      *
387      * The method also receives custom buttons to set into the configuration composite. The list
388      * is organized as an array of arrays. Each array represents a group of buttons thematically
389      * grouped together.
390      *
391      * @param listener An {@link IConfigListener} that gets and sets configuration properties.
392      *          Mandatory, cannot be null.
393      * @param parent The parent composite.
394      * @param style The style of this composite.
395      * @param initialState The initial state (serialized form) to use for the configuration
396      */
ConfigurationComposite(IConfigListener listener, Composite parent, int style, String initialState)397     public ConfigurationComposite(IConfigListener listener,
398             Composite parent, int style, String initialState) {
399         super(parent, style);
400         mListener = listener;
401         mInitialState = initialState;
402 
403         GridLayout gl;
404         GridData gd;
405         int cols = 7;  // device+config+dock+day+separator*2+theme
406 
407         // ---- First line: editing config display, locale, theme, create-button
408         Composite labelParent = new Composite(this, SWT.NONE);
409         labelParent.setLayout(gl = new GridLayout(5, false));
410         gl.marginWidth = gl.marginHeight = 0;
411         gl.marginTop = 3;
412         labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
413         gd.horizontalSpan = cols;
414 
415         new Label(labelParent, SWT.NONE).setText("Editing config:");
416         mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
417         mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
418         gd.widthHint = 50;
419 
420         mLocaleCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
421         mLocaleCombo.addSelectionListener(new SelectionAdapter() {
422             @Override
423             public void widgetSelected(SelectionEvent e) {
424                 onLocaleChange();
425             }
426         });
427 
428         // Layout bug workaround. Without this, in -some- scenarios the Locale combo box was
429         // coming up tiny. Setting a minimumWidth hint does not work either. We need to have
430         // 2 or more items in the locale combo box when the layout is first run. These items
431         // are removed as part of the locale initialization when the SDK is loaded.
432         mLocaleCombo.add("Locale"); //$NON-NLS-1$  // Dummy place holders
433         mLocaleCombo.add("Locale"); //$NON-NLS-1$
434 
435         mTargetCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
436         mTargetCombo.add("Android AOSP"); //$NON-NLS-1$  // Dummy place holders
437         mTargetCombo.add("Android AOSP"); //$NON-NLS-1$
438         mTargetCombo.addSelectionListener(new SelectionAdapter() {
439             @Override
440             public void widgetSelected(SelectionEvent e) {
441                 onRenderingTargetChange();
442             }
443         });
444 
445         mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT);
446         mCreateButton.setText("Create...");
447         mCreateButton.setEnabled(false);
448         mCreateButton.addSelectionListener(new SelectionAdapter() {
449             @Override
450             public void widgetSelected(SelectionEvent e) {
451                 if (mListener != null) {
452                     mListener.onCreate();
453                 }
454             }
455         });
456 
457         // ---- 2nd line: device/config/locale/theme Combos, create button.
458 
459         setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
460         setLayout(gl = new GridLayout(cols, false));
461         gl.marginHeight = 0;
462         gl.horizontalSpacing = 0;
463 
464         mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
465         mDeviceCombo.setLayoutData(new GridData(
466                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
467         mDeviceCombo.addSelectionListener(new SelectionAdapter() {
468             @Override
469             public void widgetSelected(SelectionEvent e) {
470                 onDeviceChange(true /* recomputeLayout*/);
471             }
472         });
473 
474         mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
475         mDeviceConfigCombo.setLayoutData(new GridData(
476                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
477         mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
478             @Override
479              public void widgetSelected(SelectionEvent e) {
480                 onDeviceConfigChange();
481             }
482         });
483 
484         // first separator
485         Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
486         separator.setLayoutData(gd = new GridData(
487                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
488         gd.heightHint = 0;
489 
490         mUiModeCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
491         mUiModeCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
492                 | GridData.GRAB_HORIZONTAL));
493         for (UiMode mode : UiMode.values()) {
494             mUiModeCombo.add(mode.getLongDisplayValue());
495         }
496         mUiModeCombo.addSelectionListener(new SelectionAdapter() {
497             @Override
498             public void widgetSelected(SelectionEvent e) {
499                 onDockChange();
500             }
501         });
502 
503         mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
504         mNightCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
505                 | GridData.GRAB_HORIZONTAL));
506         for (NightMode mode : NightMode.values()) {
507             mNightCombo.add(mode.getLongDisplayValue());
508         }
509         mNightCombo.addSelectionListener(new SelectionAdapter() {
510             @Override
511             public void widgetSelected(SelectionEvent e) {
512                 onDayChange();
513             }
514         });
515 
516         mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
517         mThemeCombo.setLayoutData(new GridData(
518                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
519         mThemeCombo.setEnabled(false);
520 
521         mThemeCombo.addSelectionListener(new SelectionAdapter() {
522             @Override
523             public void widgetSelected(SelectionEvent e) {
524                 onThemeChange();
525             }
526         });
527     }
528 
529     // ---- Init and reset/reload methods ----
530 
531     /**
532      * Sets the reference to the file being edited.
533      * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
534      * loaded (or reloaded as the SDK/target changes).
535      *
536      * @param file the file being opened
537      *
538      * @see #onXmlModelLoaded()
539      * @see #replaceFile(IFile)
540      * @see #changeFileOnNewConfig(IFile)
541      */
setFile(IFile file)542     public void setFile(IFile file) {
543         mEditedFile = file;
544     }
545 
546     /**
547      * Replaces the UI with a given file configuration. This is meant to answer the user
548      * explicitly opening a different version of the same layout from the Package Explorer.
549      * <p/>This attempts to keep the current config, but may change it if it's not compatible or
550      * not the best match
551      * <p/>This will NOT trigger a redraw event (will not call
552      * {@link IConfigListener#onConfigurationChange()}.)
553      * @param file the file being opened.
554      */
replaceFile(IFile file)555     public void replaceFile(IFile file) {
556         // if there is no previous selection, revert to default mode.
557         if (mState.device == null) {
558             setFile(file); // onTargetChanged will be called later.
559             return;
560         }
561 
562         mEditedFile = file;
563         IProject iProject = mEditedFile.getProject();
564         mResources = ResourceManager.getInstance().getProjectResources(iProject);
565 
566         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
567         mEditedConfig = resFolder.getConfiguration();
568 
569         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
570                            // new values in the widgets.
571 
572         try {
573             // only attempt to do anything if the SDK and targets are loaded.
574             LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
575             if (sdkStatus == LoadStatus.LOADED) {
576                 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
577                         null /*project*/);
578 
579                 if (targetStatus == LoadStatus.LOADED) {
580 
581                     // update the current config selection to make sure it's
582                     // compatible with the new file
583                     adaptConfigSelection(true /*needBestMatch*/);
584 
585                     // compute the final current config
586                     computeCurrentConfig();
587 
588                     // update the string showing the config value
589                     updateConfigDisplay(mEditedConfig);
590                 }
591             }
592         } finally {
593             mDisableUpdates--;
594         }
595     }
596 
597     /**
598      * Updates the UI with a new file that was opened in response to a config change.
599      * @param file the file being opened.
600      *
601      * @see #replaceFile(IFile)
602      */
changeFileOnNewConfig(IFile file)603     public void changeFileOnNewConfig(IFile file) {
604         mEditedFile = file;
605         IProject iProject = mEditedFile.getProject();
606         mResources = ResourceManager.getInstance().getProjectResources(iProject);
607 
608         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
609         mEditedConfig = resFolder.getConfiguration();
610 
611         // All that's needed is to update the string showing the config value
612         // (since the config combo were chosen by the user).
613         updateConfigDisplay(mEditedConfig);
614     }
615 
616     /**
617      * Responds to the event that the basic SDK information finished loading.
618      * @param target the possibly new target object associated with the file being edited (in case
619      * the SDK path was changed).
620      */
onSdkLoaded(IAndroidTarget target)621     public void onSdkLoaded(IAndroidTarget target) {
622         // a change to the SDK means that we need to check for new/removed devices.
623         mSdkChanged = true;
624 
625         // store the new target.
626         mProjectTarget = target;
627 
628         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
629                            // new values in the widgets.
630         try {
631             // this is going to be followed by a call to onTargetLoaded.
632             // So we can only care about the layout devices in this case.
633             initDevices();
634             initTargets();
635         } finally {
636             mDisableUpdates--;
637         }
638     }
639 
640     /**
641      * Answers to the XML model being loaded, either the first time or when the Target/SDK changes.
642      * <p>This initializes the UI, either with the first compatible configuration found,
643      * or attempts to restore a configuration if one is found to have been saved in the file
644      * persistent storage.
645      * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called
646      * back when those are loaded).
647      * <p>The method automatically handles being called the first time after editor creation, or
648      * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
649      * is properly called).
650      *
651      * @see #storeState()
652      * @see #onSdkLoaded(IAndroidTarget)
653      */
onXmlModelLoaded()654     public AndroidTargetData onXmlModelLoaded() {
655         AndroidTargetData targetData = null;
656 
657         // only attempt to do anything if the SDK and targets are loaded.
658         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
659         if (sdkStatus == LoadStatus.LOADED) {
660             mDisableUpdates++; // we do not want to trigger onXXXChange when setting
661 
662             try {
663                 // init the devices if needed (new SDK or first time going through here)
664                 if (mSdkChanged || mFirstXmlModelChange) {
665                     initDevices();
666                     initTargets();
667                 }
668 
669                 IProject iProject = mEditedFile.getProject();
670 
671                 Sdk currentSdk = Sdk.getCurrent();
672                 if (currentSdk != null) {
673                     mProjectTarget = currentSdk.getTarget(iProject);
674                 }
675 
676                 LoadStatus targetStatus = LoadStatus.FAILED;
677                 if (mProjectTarget != null) {
678                     targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
679                     initTargets();
680                 }
681 
682                 if (targetStatus == LoadStatus.LOADED) {
683                     if (mResources == null) {
684                         mResources = ResourceManager.getInstance().getProjectResources(iProject);
685                     }
686                     if (mEditedConfig == null) {
687                         ResourceFolder resFolder = mResources.getResourceFolder(
688                                 (IFolder) mEditedFile.getParent());
689                         mEditedConfig = resFolder.getConfiguration();
690                     }
691 
692                     targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
693 
694                     // get the file stored state
695                     boolean loadedConfigData = false;
696                     String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE);
697                     if (mInitialState != null) {
698                         data = mInitialState;
699                         mInitialState = null;
700                     }
701                     if (data != null) {
702                         loadedConfigData = mState.setData(data);
703                     }
704 
705                     updateLocales();
706 
707                     // If the current state was loaded from the persistent storage, we update the
708                     // UI with it and then try to adapt it (which will handle incompatible
709                     // configuration).
710                     // Otherwise, just look for the first compatible configuration.
711                     if (loadedConfigData) {
712                         // first make sure we have the config to adapt
713                         selectDevice(mState.device);
714                         fillConfigCombo(mState.configName);
715 
716                         adaptConfigSelection(false /*needBestMatch*/);
717 
718                         mUiModeCombo.select(UiMode.getIndex(mState.uiMode));
719                         mNightCombo.select(NightMode.getIndex(mState.night));
720                         mTargetCombo.select(mTargetList.indexOf(mState.target));
721 
722                         targetData = Sdk.getCurrent().getTargetData(mState.target);
723                     } else {
724                         findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
725 
726                         // Default to modern layout lib
727                         IAndroidTarget target = findDefaultRenderTarget();
728                         if (target != null) {
729                             targetData = Sdk.getCurrent().getTargetData(target);
730                             mTargetCombo.select(mTargetList.indexOf(target));
731                         }
732                     }
733 
734                     // Update themes. This is done after updating the devices above,
735                     // since we want to look at the chosen device size to decide
736                     // what the default theme (for example, with Honeycomb we choose
737                     // Holo as the default theme but only if the screen size is XLARGE
738                     // (and of course only if the manifest does not specify another
739                     // default theme).
740                     updateThemes();
741 
742                     // update the string showing the config value
743                     updateConfigDisplay(mEditedConfig);
744 
745                     // compute the final current config
746                     computeCurrentConfig();
747                 }
748             } finally {
749                 mDisableUpdates--;
750                 mFirstXmlModelChange = false;
751             }
752         }
753 
754         return targetData;
755     }
756 
757     /** Return the default render target to use, or null if no strong preference */
findDefaultRenderTarget()758     private IAndroidTarget findDefaultRenderTarget() {
759         // Default to layoutlib version 5
760         Sdk current = Sdk.getCurrent();
761         if (current != null) {
762             IAndroidTarget projectTarget = current.getTarget(mEditedFile.getProject());
763             int minProjectApi = Integer.MAX_VALUE;
764             if (projectTarget != null) {
765                 if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) {
766                     // Renderable non-platform targets are all going to be adequate (they
767                     // will have at least version 5 of layoutlib) so use the project
768                     // target as the render target.
769                     return projectTarget;
770                 }
771 
772                 if (projectTarget.getVersion().isPreview()
773                         && projectTarget.hasRenderingLibrary()) {
774                     // If the project target is a preview version, then just use it
775                     return projectTarget;
776                 }
777 
778                 minProjectApi = projectTarget.getVersion().getApiLevel();
779             }
780 
781             // We want to pick a render target that contains at least version 5 (and
782             // preferably version 6) of the layout library. To do this, we go through the
783             // targets and pick the -smallest- API level that is both simultaneously at
784             // least as big as the project API level, and supports layoutlib level 5+.
785             IAndroidTarget best = null;
786             int bestApiLevel = Integer.MAX_VALUE;
787 
788             for (IAndroidTarget target : current.getTargets()) {
789                 // Non-platform targets are not chosen as the default render target
790                 if (!target.isPlatform()) {
791                     continue;
792                 }
793 
794                 int apiLevel = target.getVersion().getApiLevel();
795 
796                 // Ignore targets that have a lower API level than the minimum project
797                 // API level:
798                 if (apiLevel < minProjectApi) {
799                     continue;
800                 }
801 
802                 // Look up the layout lib API level. This property is new so it will only
803                 // be defined for version 6 or higher, which means non-null is adequate
804                 // to see if this target is eligible:
805                 String property = target.getProperty(PkgProps.LAYOUTLIB_API);
806                 // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate:
807                 if (property != null || apiLevel >= 11) {
808                     if (apiLevel < bestApiLevel) {
809                         bestApiLevel = apiLevel;
810                         best = target;
811                     }
812                 }
813             }
814 
815             return best;
816         }
817 
818         return null;
819     }
820 
821     private static class ConfigBundle {
822         FolderConfiguration config;
823         int localeIndex;
824         int dockModeIndex;
825         int nightModeIndex;
826 
ConfigBundle()827         ConfigBundle() {
828             config = new FolderConfiguration();
829             localeIndex = 0;
830             dockModeIndex = 0;
831             nightModeIndex = 0;
832         }
833 
ConfigBundle(ConfigBundle bundle)834         ConfigBundle(ConfigBundle bundle) {
835             config = new FolderConfiguration();
836             config.set(bundle.config);
837             localeIndex = bundle.localeIndex;
838             dockModeIndex = bundle.dockModeIndex;
839             nightModeIndex = bundle.nightModeIndex;
840         }
841     }
842 
843     private static class ConfigMatch {
844         final FolderConfiguration testConfig;
845         final LayoutDevice device;
846         final String name;
847         final ConfigBundle bundle;
848 
ConfigMatch(FolderConfiguration testConfig, LayoutDevice device, String name, ConfigBundle bundle)849         public ConfigMatch(FolderConfiguration testConfig,
850                 LayoutDevice device, String name, ConfigBundle bundle) {
851             this.testConfig = testConfig;
852             this.device = device;
853             this.name = name;
854             this.bundle = bundle;
855         }
856 
857         @Override
toString()858         public String toString() {
859             return device.getName() + " - " + name;
860         }
861     }
862 
863     /**
864      * Finds a device/config that can display {@link #mEditedConfig}.
865      * <p/>Once found the device and config combos are set to the config.
866      * <p/>If there is no compatible configuration, a custom one is created.
867      * @param favorCurrentConfig if true, and no best match is found, don't change
868      * the current config. This must only be true if the current config is compatible.
869      */
findAndSetCompatibleConfig(boolean favorCurrentConfig)870     private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
871         // list of compatible device/config/locale
872         List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
873 
874         // list of actual best match (ie the file is a best match for the device/config)
875         List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
876 
877         // get a locale that match the host locale roughly (may not be exact match on the region.)
878         int localeHostMatch = getLocaleMatch();
879 
880         // build a list of combinations of non standard qualifiers to add to each device's
881         // qualifier set when testing for a match.
882         // These qualifiers are: locale, night-mode, car dock.
883         List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
884 
885         // If the edited file has locales, then we have to select a matching locale from
886         // the list.
887         // However, if it doesn't, we don't randomly take the first locale, we take one
888         // matching the current host locale (making sure it actually exist in the project)
889         int start, max;
890         if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) {
891             // add all the locales
892             start = 0;
893             max = mLocaleList.size();
894         } else {
895             // only add the locale host match
896             start = localeHostMatch;
897             max = localeHostMatch + 1; // test is <
898         }
899 
900         for (int i = start ; i < max ; i++) {
901             ResourceQualifier[] l = mLocaleList.get(i);
902 
903             ConfigBundle bundle = new ConfigBundle();
904             bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]);
905             bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]);
906 
907             bundle.localeIndex = i;
908             configBundles.add(bundle);
909         }
910 
911         // add the dock mode to the bundle combinations.
912         addDockModeToBundles(configBundles);
913 
914         // add the night mode to the bundle combinations.
915         addNightModeToBundles(configBundles);
916 
917         addRenderTargetToBundles(configBundles);
918 
919         for (LayoutDevice device : mDeviceList) {
920             for (DeviceConfig config : device.getConfigs()) {
921 
922                 // loop on the list of config bundles to create full configurations.
923                 for (ConfigBundle bundle : configBundles) {
924                     // create a new config with device config
925                     FolderConfiguration testConfig = new FolderConfiguration();
926                     testConfig.set(config.getConfig());
927 
928                     // add on top of it, the extra qualifiers from the bundle
929                     testConfig.add(bundle.config);
930 
931                     if (mEditedConfig.isMatchFor(testConfig)) {
932                         // this is a basic match. record it in case we don't find a match
933                         // where the edited file is a best config.
934                         anyMatches.add(new ConfigMatch(testConfig, device, config.getName(),
935                                 bundle));
936 
937                         if (isCurrentFileBestMatchFor(testConfig)) {
938                             // this is what we want.
939                             bestMatches.add(new ConfigMatch(testConfig, device, config.getName(),
940                                     bundle));
941                         }
942                     }
943                 }
944             }
945         }
946 
947         if (bestMatches.size() == 0) {
948             if (favorCurrentConfig) {
949                 // quick check
950                 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
951                     AdtPlugin.log(IStatus.ERROR,
952                             "favorCurrentConfig can only be true if the current config is compatible");
953                 }
954 
955                 // just display the warning
956                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
957                         String.format(
958                                 "'%1$s' is not a best match for any device/locale combination.",
959                                 mEditedConfig.toDisplayString()),
960                         String.format(
961                                 "Displaying it with '%1$s'",
962                                 mCurrentConfig.toDisplayString()));
963             } else if (anyMatches.size() > 0) {
964                 // select the best device anyway.
965                 ConfigMatch match = selectConfigMatch(anyMatches);
966                 selectDevice(mState.device = match.device);
967                 fillConfigCombo(match.name);
968                 mLocaleCombo.select(match.bundle.localeIndex);
969                 mUiModeCombo.select(match.bundle.dockModeIndex);
970                 mNightCombo.select(match.bundle.nightModeIndex);
971 
972                 // TODO: display a better warning!
973                 computeCurrentConfig();
974                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
975                         String.format(
976                                 "'%1$s' is not a best match for any device/locale combination.",
977                                 mEditedConfig.toDisplayString()),
978                         String.format(
979                                 "Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.",
980                                 mCurrentConfig.toDisplayString()));
981 
982             } else {
983                 // TODO: there is no device/config able to display the layout, create one.
984                 // For the base config values, we'll take the first device and config,
985                 // and replace whatever qualifier required by the layout file.
986             }
987         } else {
988             ConfigMatch match = selectConfigMatch(bestMatches);
989             selectDevice(mState.device = match.device);
990             fillConfigCombo(match.name);
991             mLocaleCombo.select(match.bundle.localeIndex);
992             mUiModeCombo.select(match.bundle.dockModeIndex);
993             mNightCombo.select(match.bundle.nightModeIndex);
994         }
995     }
996 
997     /**
998      * Note: this comparator imposes orderings that are inconsistent with equals.
999      */
1000     private static class TabletConfigComparator implements Comparator<ConfigMatch> {
compare(ConfigMatch o1, ConfigMatch o2)1001         public int compare(ConfigMatch o1, ConfigMatch o2) {
1002             ScreenSize ss1 = o1.testConfig.getScreenSizeQualifier().getValue();
1003             ScreenSize ss2 = o2.testConfig.getScreenSizeQualifier().getValue();
1004 
1005             // X-LARGE is better than all others (which are considered identical)
1006             // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
1007 
1008             if (ss1 == ScreenSize.XLARGE) {
1009                 if (ss2 == ScreenSize.XLARGE) {
1010                     ScreenOrientation so1 =
1011                         o1.testConfig.getScreenOrientationQualifier().getValue();
1012                     ScreenOrientation so2 =
1013                         o2.testConfig.getScreenOrientationQualifier().getValue();
1014 
1015                     if (so1 == ScreenOrientation.LANDSCAPE) {
1016                         if (so2 == ScreenOrientation.LANDSCAPE) {
1017                             return 0;
1018                         } else {
1019                             return -1;
1020                         }
1021                     } else if (so2 == ScreenOrientation.LANDSCAPE) {
1022                         return 1;
1023                     } else {
1024                         return 0;
1025                     }
1026                 } else {
1027                     return -1;
1028                 }
1029             } else if (ss2 == ScreenSize.XLARGE) {
1030                 return 1;
1031             } else {
1032                 return 0;
1033             }
1034         }
1035     }
1036 
1037     /**
1038      * Note: this comparator imposes orderings that are inconsistent with equals.
1039      */
1040     private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
1041 
1042         private SparseIntArray mDensitySort = new SparseIntArray(4);
1043 
PhoneConfigComparator()1044         public PhoneConfigComparator() {
1045             // put the sort order for the density.
1046             mDensitySort.put(Density.HIGH.getDpiValue(),   1);
1047             mDensitySort.put(Density.MEDIUM.getDpiValue(), 2);
1048             mDensitySort.put(Density.XHIGH.getDpiValue(),  3);
1049             mDensitySort.put(Density.LOW.getDpiValue(),    4);
1050         }
1051 
compare(ConfigMatch o1, ConfigMatch o2)1052         public int compare(ConfigMatch o1, ConfigMatch o2) {
1053             int dpi1 = Density.DEFAULT_DENSITY;
1054             if (o1.testConfig.getDensityQualifier() != null) {
1055                 dpi1 = o1.testConfig.getDensityQualifier().getValue().getDpiValue();
1056                 dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/);
1057             }
1058 
1059             int dpi2 = Density.DEFAULT_DENSITY;
1060             if (o2.testConfig.getDensityQualifier() != null) {
1061                 dpi2 = o2.testConfig.getDensityQualifier().getValue().getDpiValue();
1062                 dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/);
1063             }
1064 
1065             if (dpi1 == dpi2) {
1066                 // portrait is better
1067                 ScreenOrientation so1 =
1068                     o1.testConfig.getScreenOrientationQualifier().getValue();
1069                 ScreenOrientation so2 =
1070                     o2.testConfig.getScreenOrientationQualifier().getValue();
1071 
1072                 if (so1 == ScreenOrientation.PORTRAIT) {
1073                     if (so2 == ScreenOrientation.PORTRAIT) {
1074                         return 0;
1075                     } else {
1076                         return -1;
1077                     }
1078                 } else if (so2 == ScreenOrientation.PORTRAIT) {
1079                     return 1;
1080                 } else {
1081                     return 0;
1082                 }
1083             }
1084 
1085             return dpi1 - dpi2;
1086         }
1087     }
1088 
selectConfigMatch(List<ConfigMatch> matches)1089     private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
1090         // API 11-13: look for a x-large device
1091         int apiLevel = mProjectTarget.getVersion().getApiLevel();
1092         if (apiLevel >= 11 && apiLevel < 14) {
1093             // TODO: Maybe check the compatible-screen tag in the manifest to figure out
1094             // what kind of device should be used for display.
1095             Collections.sort(matches, new TabletConfigComparator());
1096         } else {
1097             // lets look for a high density device
1098             Collections.sort(matches, new PhoneConfigComparator());
1099         }
1100 
1101         // Look at the currently active editor to see if it's a layout editor, and if so,
1102         // look up its configuration and if the configuration is in our match list,
1103         // use it. This means we "preserve" the current configuration when you open
1104         // new layouts.
1105         IWorkbench workbench = PlatformUI.getWorkbench();
1106         IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow();
1107         IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
1108         IEditorPart activeEditor = page.getActiveEditor();
1109         if (activeEditor instanceof LayoutEditor
1110                 && mEditedFile != null
1111                 // (Only do this when the two files are in the same project)
1112                 && ((LayoutEditor) activeEditor).getProject() == mEditedFile.getProject()) {
1113             LayoutEditor editor = (LayoutEditor) activeEditor;
1114             FolderConfiguration configuration = editor.getGraphicalEditor().getConfiguration();
1115             if (configuration != null) {
1116                 for (ConfigMatch match : matches) {
1117                     if (configuration.equals(match.testConfig)) {
1118                         return match;
1119                     }
1120                 }
1121             }
1122         }
1123 
1124         // the list has been sorted so that the first item is the best config
1125         return matches.get(0);
1126     }
1127 
addRenderTargetToBundles(List<ConfigBundle> configBundles)1128     private void addRenderTargetToBundles(List<ConfigBundle> configBundles) {
1129         Pair<ResourceQualifier[], IAndroidTarget> state = loadRenderState();
1130         if (state != null) {
1131             IAndroidTarget target = state.getSecond();
1132             if (target != null) {
1133                 int apiLevel = target.getVersion().getApiLevel();
1134                 for (ConfigBundle bundle : configBundles) {
1135                     bundle.config.setVersionQualifier(
1136                             new VersionQualifier(apiLevel));
1137                 }
1138             }
1139         }
1140     }
1141 
addDockModeToBundles(List<ConfigBundle> addConfig)1142     private void addDockModeToBundles(List<ConfigBundle> addConfig) {
1143         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1144 
1145         // loop on each item and for each, add all variations of the dock modes
1146         for (ConfigBundle bundle : addConfig) {
1147             int index = 0;
1148             for (UiMode mode : UiMode.values()) {
1149                 ConfigBundle b = new ConfigBundle(bundle);
1150                 b.config.setUiModeQualifier(new UiModeQualifier(mode));
1151                 b.dockModeIndex = index++;
1152                 list.add(b);
1153             }
1154         }
1155 
1156         addConfig.clear();
1157         addConfig.addAll(list);
1158     }
1159 
addNightModeToBundles(List<ConfigBundle> addConfig)1160     private void addNightModeToBundles(List<ConfigBundle> addConfig) {
1161         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1162 
1163         // loop on each item and for each, add all variations of the night modes
1164         for (ConfigBundle bundle : addConfig) {
1165             int index = 0;
1166             for (NightMode mode : NightMode.values()) {
1167                 ConfigBundle b = new ConfigBundle(bundle);
1168                 b.config.setNightModeQualifier(new NightModeQualifier(mode));
1169                 b.nightModeIndex = index++;
1170                 list.add(b);
1171             }
1172         }
1173 
1174         addConfig.clear();
1175         addConfig.addAll(list);
1176     }
1177 
1178     /**
1179      * Adapts the current device/config selection so that it's compatible with
1180      * {@link #mEditedConfig}.
1181      * <p/>If the current selection is compatible, nothing is changed.
1182      * <p/>If it's not compatible, configs from the current devices are tested.
1183      * <p/>If none are compatible, it reverts to
1184      * {@link #findAndSetCompatibleConfig(FolderConfiguration)}
1185      */
adaptConfigSelection(boolean needBestMatch)1186     private void adaptConfigSelection(boolean needBestMatch) {
1187         // check the device config (ie sans locale)
1188         boolean needConfigChange = true; // if still true, we need to find another config.
1189         boolean currentConfigIsCompatible = false;
1190         int configIndex = mDeviceConfigCombo.getSelectionIndex();
1191         if (configIndex != -1) {
1192             String configName = mDeviceConfigCombo.getItem(configIndex);
1193             FolderConfiguration currentConfig = mState.device.getFolderConfigByName(configName);
1194             if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) {
1195                 currentConfigIsCompatible = true; // current config is compatible
1196                 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
1197                     needConfigChange = false;
1198                 }
1199             }
1200         }
1201 
1202         if (needConfigChange) {
1203             // if the current config/locale isn't a correct match, then
1204             // look for another config/locale in the same device.
1205             FolderConfiguration testConfig = new FolderConfiguration();
1206 
1207             // first look in the current device.
1208             String matchName = null;
1209             int localeIndex = -1;
1210             mainloop: for (DeviceConfig config : mState.device.getConfigs()) {
1211                 testConfig.set(config.getConfig());
1212 
1213                 // loop on the locales.
1214                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
1215                     ResourceQualifier[] locale = mLocaleList.get(i);
1216 
1217                     // update the test config with the locale qualifiers
1218                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
1219                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
1220 
1221                     if (mEditedConfig.isMatchFor(testConfig) &&
1222                             isCurrentFileBestMatchFor(testConfig)) {
1223                         matchName = config.getName();
1224                         localeIndex = i;
1225                         break mainloop;
1226                     }
1227                 }
1228             }
1229 
1230             if (matchName != null) {
1231                 selectConfig(matchName);
1232                 mLocaleCombo.select(localeIndex);
1233             } else {
1234                 // no match in current device with any config/locale
1235                 // attempt to find another device that can display this particular config.
1236                 findAndSetCompatibleConfig(currentConfigIsCompatible);
1237             }
1238         }
1239     }
1240 
1241     /**
1242      * Finds a locale matching the config from a file.
1243      * @param language the language qualifier or null if none is set.
1244      * @param region the region qualifier or null if none is set.
1245      * @return true if there was a change in the combobox as a result of applying the locale
1246      */
setLocaleCombo(ResourceQualifier language, ResourceQualifier region)1247     private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
1248         boolean changed = false;
1249 
1250         // find the locale match. Since the locale list is based on the content of the
1251         // project resources there must be an exact match.
1252         // The only trick is that the region could be null in the fileConfig but in our
1253         // list of locales, this is represented as a RegionQualifier with value of
1254         // FAKE_LOCALE_VALUE.
1255         final int count = mLocaleList.size();
1256         for (int i = 0 ; i < count ; i++) {
1257             ResourceQualifier[] locale = mLocaleList.get(i);
1258 
1259             // the language qualifier in the locale list is never null.
1260             if (locale[LOCALE_LANG].equals(language)) {
1261                 // region comparison is more complex, as the region could be null.
1262                 if (region == null) {
1263                     if (RegionQualifier.FAKE_REGION_VALUE.equals(
1264                             ((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
1265                         // match!
1266                         if (mLocaleCombo.getSelectionIndex() != i) {
1267                             mLocaleCombo.select(i);
1268                             changed = true;
1269                         }
1270                         break;
1271                     }
1272                 } else if (region.equals(locale[LOCALE_REGION])) {
1273                     // match!
1274                     if (mLocaleCombo.getSelectionIndex() != i) {
1275                         mLocaleCombo.select(i);
1276                         changed = true;
1277                     }
1278                     break;
1279                 }
1280             }
1281         }
1282 
1283         return changed;
1284     }
1285 
updateConfigDisplay(FolderConfiguration fileConfig)1286     private void updateConfigDisplay(FolderConfiguration fileConfig) {
1287         String current = fileConfig.toDisplayString();
1288         String layoutLabel = current != null ? current : "(Default)";
1289         mCurrentLayoutLabel.setText(layoutLabel);
1290         mCurrentLayoutLabel.setToolTipText(layoutLabel);
1291     }
1292 
saveState()1293     private void saveState() {
1294         if (mDisableUpdates == 0) {
1295             int index = mDeviceConfigCombo.getSelectionIndex();
1296             if (index != -1) {
1297                 mState.configName = mDeviceConfigCombo.getItem(index);
1298             } else {
1299                 mState.configName = null;
1300             }
1301 
1302             // since the locales are relative to the project, only keeping the index is enough
1303             index = mLocaleCombo.getSelectionIndex();
1304             if (index != -1) {
1305                 mState.locale = mLocaleList.get(index);
1306             } else {
1307                 mState.locale = null;
1308             }
1309 
1310             index = mThemeCombo.getSelectionIndex();
1311             if (index != -1) {
1312                 mState.theme = mThemeCombo.getItem(index);
1313             }
1314 
1315             index = mUiModeCombo.getSelectionIndex();
1316             if (index != -1) {
1317                 mState.uiMode = UiMode.getByIndex(index);
1318             }
1319 
1320             index = mNightCombo.getSelectionIndex();
1321             if (index != -1) {
1322                 mState.night = NightMode.getByIndex(index);
1323             }
1324 
1325             index = mTargetCombo.getSelectionIndex();
1326             if (index != -1) {
1327                 mState.target = mTargetList.get(index);
1328             }
1329         }
1330     }
1331 
1332     /**
1333      * Stores the current config selection into the edited file.
1334      */
storeState()1335     public void storeState() {
1336         AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData());
1337     }
1338 
1339     /**
1340      * Updates the locale combo.
1341      * This must be called from the UI thread.
1342      */
updateLocales()1343     public void updateLocales() {
1344         if (mListener == null) {
1345             return; // can't do anything w/o it.
1346         }
1347 
1348         mDisableUpdates++;
1349 
1350         try {
1351             // Reset the combo
1352             mLocaleCombo.removeAll();
1353             mLocaleList.clear();
1354 
1355             SortedSet<String> languages = null;
1356             boolean hasLocale = false;
1357 
1358             // get the languages from the project.
1359             ResourceRepository projectRes = mListener.getProjectResources();
1360 
1361             // in cases where the opened file is not linked to a project, this could be null.
1362             if (projectRes != null) {
1363                 // now get the languages from the project.
1364                 languages = projectRes.getLanguages();
1365 
1366                 for (String language : languages) {
1367                     hasLocale = true;
1368 
1369                     LanguageQualifier langQual = new LanguageQualifier(language);
1370 
1371                     // find the matching regions and add them
1372                     SortedSet<String> regions = projectRes.getRegions(language);
1373                     for (String region : regions) {
1374                         mLocaleCombo.add(
1375                                 String.format("%1$s / %2$s", language, region));
1376                         RegionQualifier regionQual = new RegionQualifier(region);
1377                         mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
1378                     }
1379 
1380                     // now the entry for the other regions the language alone
1381                     if (regions.size() > 0) {
1382                         mLocaleCombo.add(String.format("%1$s / Other", language));
1383                     } else {
1384                         mLocaleCombo.add(String.format("%1$s / Any", language));
1385                     }
1386                     // create a region qualifier that will never be matched by qualified resources.
1387                     mLocaleList.add(new ResourceQualifier[] {
1388                             langQual,
1389                             new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1390                     });
1391                 }
1392             }
1393 
1394             // add a locale not present in the project resources. This will let the dev
1395             // tests his/her default values.
1396             if (hasLocale) {
1397                 mLocaleCombo.add("Other");
1398             } else {
1399                 mLocaleCombo.add("Any locale");
1400             }
1401 
1402             // create language/region qualifier that will never be matched by qualified resources.
1403             mLocaleList.add(new ResourceQualifier[] {
1404                     new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
1405                     new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1406             });
1407 
1408             if (mState.locale != null) {
1409                 // FIXME: this may fails if the layout was deleted (and was the last one to have
1410                 // that local. (we have other problem in this case though)
1411                 setLocaleCombo(mState.locale[LOCALE_LANG],
1412                         mState.locale[LOCALE_REGION]);
1413             } else {
1414                 mLocaleCombo.select(0);
1415             }
1416 
1417             mThemeCombo.getParent().layout();
1418         } finally {
1419             mDisableUpdates--;
1420         }
1421     }
1422 
getLocaleMatch()1423     private int getLocaleMatch() {
1424         Locale locale = Locale.getDefault();
1425         if (locale != null) {
1426             String currentLanguage = locale.getLanguage();
1427             String currentRegion = locale.getCountry();
1428 
1429             final int count = mLocaleList.size();
1430             for (int l = 0 ; l < count ; l++) {
1431                 ResourceQualifier[] localeArray = mLocaleList.get(l);
1432                 LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG];
1433                 RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION];
1434 
1435                 // there's always a ##/Other or ##/Any (which is the same, the region
1436                 // contains FAKE_REGION_VALUE). If we don't find a perfect region match
1437                 // we take the fake region. Since it's last in the list, this makes the
1438                 // test easy.
1439                 if (langQ.getValue().equals(currentLanguage) &&
1440                         (regionQ.getValue().equals(currentRegion) ||
1441                          regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) {
1442                     return l;
1443                 }
1444             }
1445 
1446             // if no locale match the current local locale, it's likely that it is
1447             // the default one which is the last one.
1448             return count - 1;
1449         }
1450 
1451         return -1;
1452     }
1453 
1454     /**
1455      * Updates the theme combo.
1456      * This must be called from the UI thread.
1457      */
updateThemes()1458     private void updateThemes() {
1459         if (mListener == null) {
1460             return; // can't do anything w/o it.
1461         }
1462 
1463         ResourceRepository frameworkRes = mListener.getFrameworkResources(getRenderingTarget());
1464 
1465         mDisableUpdates++;
1466 
1467         try {
1468             // Reset the combo
1469             mThemeCombo.removeAll();
1470             mIsProjectTheme.clear();
1471 
1472             ArrayList<String> themes = new ArrayList<String>();
1473             String includedIn = mListener.getIncludedWithin();
1474 
1475             // First list any themes that are declared by the manifest
1476             if (mEditedFile != null) {
1477                 IProject project = mEditedFile.getProject();
1478                 ManifestInfo manifest = ManifestInfo.get(project);
1479 
1480                 // Look up the screen size for the current configuration
1481                 ScreenSize screenSize = null;
1482                 if (mState.device != null) {
1483                     List<DeviceConfig> configs = mState.device.getConfigs();
1484                     for (DeviceConfig config : configs) {
1485                         ScreenSizeQualifier qualifier =
1486                             config.getConfig().getScreenSizeQualifier();
1487                         screenSize = qualifier.getValue();
1488                         break;
1489                     }
1490                 }
1491                 // Look up the default/fallback theme to use for this project (which
1492                 // depends on the screen size when no particular theme is specified
1493                 // in the manifest)
1494                 String defaultTheme = manifest.getDefaultTheme(mState.target, screenSize);
1495 
1496                 Map<String, String> activityThemes = manifest.getActivityThemes();
1497                 String pkg = manifest.getPackage();
1498                 String preferred = null;
1499                 boolean isIncluded = includedIn != null;
1500                 if (mState.theme == null || isIncluded) {
1501                     String layoutName = ResourceHelper.getLayoutName(mEditedFile);
1502 
1503                     // If we are rendering a layout in included context, pick the theme
1504                     // from the outer layout instead
1505                     if (includedIn != null) {
1506                         layoutName = includedIn;
1507                     }
1508 
1509                     String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
1510                     if (activity != null) {
1511                         preferred = activityThemes.get(activity);
1512                     }
1513                     if (preferred == null) {
1514                         preferred = defaultTheme;
1515                     }
1516                     String preferredTheme = ResourceHelper.styleToTheme(preferred);
1517                     if (includedIn == null) {
1518                         mState.theme = preferredTheme;
1519                     }
1520                     boolean isProjectTheme = !preferred.startsWith(PREFIX_ANDROID_STYLE);
1521                     mThemeCombo.add(preferredTheme);
1522                     mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1523 
1524                     mThemeCombo.add(THEME_SEPARATOR);
1525                     mIsProjectTheme.add(Boolean.FALSE);
1526                 }
1527 
1528                 // Create a sorted list of unique themes referenced in the manifest
1529                 // (sort alphabetically, but place the preferred theme at the
1530                 // top of the list)
1531                 Set<String> themeSet = new HashSet<String>(activityThemes.values());
1532                 themeSet.add(defaultTheme);
1533                 List<String> themeList = new ArrayList<String>(themeSet);
1534                 final String first = preferred;
1535                 Collections.sort(themeList, new Comparator<String>() {
1536                     public int compare(String s1, String s2) {
1537                         if (s1 == first) {
1538                             return -1;
1539                         } else if (s1 == first) {
1540                             return 1;
1541                         } else {
1542                             return s1.compareTo(s2);
1543                         }
1544                     }
1545                 });
1546 
1547                 if (themeList.size() > 1 ||
1548                         (themeList.size() == 1 && (preferred == null ||
1549                                 !preferred.equals(themeList.get(0))))) {
1550                     for (String style : themeList) {
1551                         String theme = ResourceHelper.styleToTheme(style);
1552 
1553                         // Initialize the chosen theme to the first item
1554                         // in the used theme list (that's what would be chosen
1555                         // anyway) such that we stop attempting to look up
1556                         // the associated activity (during initialization,
1557                         // this method can be called repeatedly.)
1558                         if (mState.theme == null) {
1559                             mState.theme = theme;
1560                         }
1561 
1562                         boolean isProjectTheme = !style.startsWith(PREFIX_ANDROID_STYLE);
1563                         mThemeCombo.add(theme);
1564                         mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1565                     }
1566                     mThemeCombo.add(THEME_SEPARATOR);
1567                     mIsProjectTheme.add(Boolean.FALSE);
1568                 }
1569             }
1570 
1571             // now get the themes and languages from the project.
1572             int projectThemeCount = 0;
1573             ResourceRepository projectRes = mListener.getProjectResources();
1574             // in cases where the opened file is not linked to a project, this could be null.
1575             if (projectRes != null) {
1576                 // get the configured resources for the project
1577                 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
1578                     mListener.getConfiguredProjectResources();
1579 
1580                 if (configuredProjectRes != null) {
1581                     // get the styles.
1582                     Map<String, ResourceValue> styleMap = configuredProjectRes.get(
1583                             ResourceType.STYLE);
1584 
1585                     if (styleMap != null) {
1586                         // collect the themes out of all the styles, ie styles that extend,
1587                         // directly or indirectly a platform theme.
1588                         for (ResourceValue value : styleMap.values()) {
1589                             if (isTheme(value, styleMap)) {
1590                                 themes.add(value.getName());
1591                             }
1592                         }
1593 
1594                         Collections.sort(themes);
1595 
1596                         for (String theme : themes) {
1597                             mThemeCombo.add(theme);
1598                             mIsProjectTheme.add(Boolean.TRUE);
1599                         }
1600                     }
1601                 }
1602                 projectThemeCount = themes.size();
1603                 themes.clear();
1604             }
1605 
1606             // get the themes, and languages from the Framework.
1607             if (frameworkRes != null) {
1608                 // get the configured resources for the framework
1609                 Map<ResourceType, Map<String, ResourceValue>> frameworResources =
1610                     frameworkRes.getConfiguredResources(getCurrentConfig());
1611 
1612                 if (frameworResources != null) {
1613                     // get the styles.
1614                     Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
1615 
1616 
1617                     // collect the themes out of all the styles.
1618                     for (ResourceValue value : styles.values()) {
1619                         String name = value.getName();
1620                         if (name.startsWith("Theme.") || name.equals("Theme")) {
1621                             themes.add(value.getName());
1622                         }
1623                     }
1624 
1625                     // sort them and add them to the combo
1626                     Collections.sort(themes);
1627 
1628                     if (projectThemeCount > 0 && themes.size() > 0) {
1629                         mThemeCombo.add(THEME_SEPARATOR);
1630                         mIsProjectTheme.add(Boolean.FALSE);
1631                     }
1632 
1633                     for (String theme : themes) {
1634                         mThemeCombo.add(theme);
1635                         mIsProjectTheme.add(Boolean.FALSE);
1636                     }
1637 
1638                     themes.clear();
1639                 }
1640             }
1641 
1642             // try to reselect the previous theme.
1643             boolean needDefaultSelection = true;
1644 
1645             if (mState.theme != null && includedIn == null) {
1646                 final int count = mThemeCombo.getItemCount();
1647                 for (int i = 0 ; i < count ; i++) {
1648                     if (mState.theme.equals(mThemeCombo.getItem(i))) {
1649                         mThemeCombo.select(i);
1650                         needDefaultSelection = false;
1651                         mThemeCombo.setEnabled(true);
1652                         break;
1653                     }
1654                 }
1655             }
1656 
1657             if (needDefaultSelection) {
1658                 if (mThemeCombo.getItemCount() > 0) {
1659                     mThemeCombo.select(0);
1660                     mThemeCombo.setEnabled(true);
1661                 } else {
1662                     mThemeCombo.setEnabled(false);
1663                 }
1664             }
1665 
1666             mThemeCombo.getParent().layout();
1667         } finally {
1668             mDisableUpdates--;
1669         }
1670 
1671         assert mIsProjectTheme.size() == mThemeCombo.getItemCount();
1672     }
1673 
1674     // ---- getters for the config selection values ----
1675 
getEditedConfig()1676     public FolderConfiguration getEditedConfig() {
1677         return mEditedConfig;
1678     }
1679 
getCurrentConfig()1680     public FolderConfiguration getCurrentConfig() {
1681         return mCurrentConfig;
1682     }
1683 
getCurrentConfig(FolderConfiguration config)1684     public void getCurrentConfig(FolderConfiguration config) {
1685         config.set(mCurrentConfig);
1686     }
1687 
1688     /**
1689      * Returns the currently selected {@link Density}. This is guaranteed to be non null.
1690      */
getDensity()1691     public Density getDensity() {
1692         if (mCurrentConfig != null) {
1693             DensityQualifier qual = mCurrentConfig.getDensityQualifier();
1694             if (qual != null) {
1695                 // just a sanity check
1696                 Density d = qual.getValue();
1697                 if (d != Density.NODPI) {
1698                     return d;
1699                 }
1700             }
1701         }
1702 
1703         // no config? return medium as the default density.
1704         return Density.MEDIUM;
1705     }
1706 
1707     /**
1708      * Returns the current device xdpi.
1709      */
getXDpi()1710     public float getXDpi() {
1711         if (mState.device != null) {
1712             float dpi = mState.device.getXDpi();
1713             if (Float.isNaN(dpi) == false) {
1714                 return dpi;
1715             }
1716         }
1717 
1718         // get the pixel density as the density.
1719         return getDensity().getDpiValue();
1720     }
1721 
1722     /**
1723      * Returns the current device ydpi.
1724      */
getYDpi()1725     public float getYDpi() {
1726         if (mState.device != null) {
1727             float dpi = mState.device.getYDpi();
1728             if (Float.isNaN(dpi) == false) {
1729                 return dpi;
1730             }
1731         }
1732 
1733         // get the pixel density as the density.
1734         return getDensity().getDpiValue();
1735     }
1736 
getScreenBounds()1737     public Rect getScreenBounds() {
1738         // get the orientation from the current device config
1739         ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
1740         ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
1741         if (qual != null) {
1742             orientation = qual.getValue();
1743         }
1744 
1745         // get the device screen dimension
1746         ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
1747         int s1, s2;
1748         if (qual2 != null) {
1749             s1 = qual2.getValue1();
1750             s2 = qual2.getValue2();
1751         } else {
1752             s1 = 480;
1753             s2 = 320;
1754         }
1755 
1756         switch (orientation) {
1757             default:
1758             case PORTRAIT:
1759                 return new Rect(0, 0, s2, s1);
1760             case LANDSCAPE:
1761                 return new Rect(0, 0, s1, s2);
1762             case SQUARE:
1763                 return new Rect(0, 0, s1, s1);
1764         }
1765     }
1766 
1767     /**
1768      * Returns the current theme, or null if the combo has no selection.
1769      *
1770      * @return the theme name, or null
1771      */
getTheme()1772     public String getTheme() {
1773         int themeIndex = mThemeCombo.getSelectionIndex();
1774         if (themeIndex != -1) {
1775             return mThemeCombo.getItem(themeIndex);
1776         }
1777 
1778         return null;
1779     }
1780 
1781     /**
1782      * Returns the current device string, or null if the combo has no selection.
1783      *
1784      * @return the device name, or null
1785      */
getDevice()1786     public String getDevice() {
1787         int deviceIndex = mDeviceCombo.getSelectionIndex();
1788         if (deviceIndex != -1) {
1789             return mDeviceCombo.getItem(deviceIndex);
1790         }
1791 
1792         return null;
1793     }
1794 
1795     /**
1796      * Returns whether the current theme selection is a project theme.
1797      * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
1798      * @return true for project theme, false for framework theme
1799      */
isProjectTheme()1800     public boolean isProjectTheme() {
1801         return mIsProjectTheme.get(mThemeCombo.getSelectionIndex()).booleanValue();
1802     }
1803 
getRenderingTarget()1804     public IAndroidTarget getRenderingTarget() {
1805         int index = mTargetCombo.getSelectionIndex();
1806         if (index >= 0) {
1807             return mTargetList.get(index);
1808         }
1809 
1810         return null;
1811     }
1812 
1813     /**
1814      * Loads the list of {@link IAndroidTarget} and inits the UI with it.
1815      */
initTargets()1816     private void initTargets() {
1817         mTargetCombo.removeAll();
1818         mTargetList.clear();
1819 
1820         Sdk currentSdk = Sdk.getCurrent();
1821         if (currentSdk != null) {
1822             IAndroidTarget[] targets = currentSdk.getTargets();
1823             int match = -1;
1824             for (int i = 0 ; i < targets.length; i++) {
1825                 // FIXME: add check based on project minSdkVersion
1826                 if (targets[i].hasRenderingLibrary()) {
1827                     mTargetCombo.add(targets[i].getShortClasspathName());
1828                     mTargetList.add(targets[i]);
1829 
1830                     if (mRenderingTarget != null) {
1831                         // use equals because the rendering could be from a previous SDK, so
1832                         // it may not be the same instance.
1833                         if (mRenderingTarget.equals(targets[i])) {
1834                             match = mTargetList.indexOf(targets[i]);
1835                         }
1836                     } else if (mProjectTarget == targets[i]) {
1837                         match = mTargetList.indexOf(targets[i]);
1838                     }
1839                 }
1840             }
1841 
1842             mTargetCombo.setEnabled(mTargetList.size() > 1);
1843             if (match == -1) {
1844                 mTargetCombo.deselectAll();
1845 
1846                 // the rendering target is the same as the project.
1847                 mRenderingTarget = mProjectTarget;
1848             } else {
1849                 mTargetCombo.select(match);
1850 
1851                 // set the rendering target to the new object.
1852                 mRenderingTarget = mTargetList.get(match);
1853             }
1854         }
1855     }
1856 
1857     /**
1858      * Loads the list of {@link LayoutDevice} and inits the UI with it.
1859      */
initDevices()1860     private void initDevices() {
1861         mDeviceList = null;
1862 
1863         Sdk sdk = Sdk.getCurrent();
1864         if (sdk != null) {
1865             LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
1866             mDeviceList = manager.getCombinedList();
1867         }
1868 
1869 
1870         // remove older devices if applicable
1871         mDeviceCombo.removeAll();
1872         mDeviceConfigCombo.removeAll();
1873 
1874         // fill with the devices
1875         if (mDeviceList != null) {
1876             for (LayoutDevice device : mDeviceList) {
1877                 mDeviceCombo.add(device.getName());
1878             }
1879             mDeviceCombo.select(0);
1880 
1881             if (mDeviceList.size() > 0) {
1882                 List<DeviceConfig> configs = mDeviceList.get(0).getConfigs();
1883                 for (DeviceConfig config : configs) {
1884                     mDeviceConfigCombo.add(config.getName());
1885                 }
1886                 mDeviceConfigCombo.select(0);
1887                 if (configs.size() == 1) {
1888                     mDeviceConfigCombo.setEnabled(false);
1889                 }
1890             }
1891         }
1892 
1893         // add the custom item
1894         mDeviceCombo.add("Custom...");
1895     }
1896 
1897     /**
1898      * Selects a given {@link LayoutDevice} in the device combo, if it is found.
1899      * @param device the device to select
1900      * @return true if the device was found.
1901      */
selectDevice(LayoutDevice device)1902     private boolean selectDevice(LayoutDevice device) {
1903         final int count = mDeviceList.size();
1904         for (int i = 0 ; i < count ; i++) {
1905             // since device comes from mDeviceList, we can use the == operator.
1906             if (device == mDeviceList.get(i)) {
1907                 mDeviceCombo.select(i);
1908                 return true;
1909             }
1910         }
1911 
1912         return false;
1913     }
1914 
1915     /**
1916      * Selects a config by name.
1917      * @param name the name of the config to select.
1918      */
selectConfig(String name)1919     private void selectConfig(String name) {
1920         final int count = mDeviceConfigCombo.getItemCount();
1921         for (int i = 0 ; i < count ; i++) {
1922             String item = mDeviceConfigCombo.getItem(i);
1923             if (name.equals(item)) {
1924                 mDeviceConfigCombo.select(i);
1925                 return;
1926             }
1927         }
1928     }
1929 
1930     /**
1931      * Called when the selection of the device combo changes.
1932      * @param recomputeLayout
1933      */
onDeviceChange(boolean recomputeLayout)1934     private void onDeviceChange(boolean recomputeLayout) {
1935         // because changing the content of a combo triggers a change event, respect the
1936         // mDisableUpdates flag
1937         if (mDisableUpdates > 0) {
1938             return;
1939         }
1940 
1941         String newConfigName = null;
1942 
1943         int deviceIndex = mDeviceCombo.getSelectionIndex();
1944         if (deviceIndex != -1) {
1945             // check if the user is asking for the custom item
1946             if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
1947                 onCustomDeviceConfig();
1948                 return;
1949             }
1950 
1951             // get the previous config, so that we can look for a close match
1952             if (mState.device != null) {
1953                 int index = mDeviceConfigCombo.getSelectionIndex();
1954                 if (index != -1) {
1955                     FolderConfiguration oldConfig = mState.device.getFolderConfigByName(
1956                             mDeviceConfigCombo.getItem(index));
1957 
1958                     LayoutDevice newDevice = mDeviceList.get(deviceIndex);
1959 
1960                     newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
1961                 }
1962             }
1963 
1964             mState.device = mDeviceList.get(deviceIndex);
1965         } else {
1966             mState.device = null;
1967         }
1968 
1969         fillConfigCombo(newConfigName);
1970 
1971         computeCurrentConfig();
1972 
1973         if (recomputeLayout) {
1974             onDeviceConfigChange();
1975         }
1976     }
1977 
1978     /**
1979      * Handles a user request for the {@link ConfigManagerDialog}.
1980      */
onCustomDeviceConfig()1981     private void onCustomDeviceConfig() {
1982         ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
1983         dialog.open();
1984 
1985         // save the user devices
1986         Sdk.getCurrent().getLayoutDeviceManager().save();
1987 
1988         // Update the UI with no triggered event
1989         mDisableUpdates++;
1990 
1991         try {
1992             LayoutDevice oldCurrent = mState.device;
1993 
1994             // but first, update the device combo
1995             initDevices();
1996 
1997             // attempts to reselect the current device.
1998             if (selectDevice(oldCurrent)) {
1999                 // current device still exists.
2000                 // reselect the config
2001                 selectConfig(mState.configName);
2002 
2003                 // reset the UI as if it was just a replacement file, since we can keep
2004                 // the current device (and possibly config).
2005                 adaptConfigSelection(false /*needBestMatch*/);
2006 
2007             } else {
2008                 // find a new device/config to match the current file.
2009                 findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
2010             }
2011         } finally {
2012             mDisableUpdates--;
2013         }
2014 
2015         // recompute the current config
2016         computeCurrentConfig();
2017 
2018         // force a redraw
2019         onDeviceChange(true /*recomputeLayout*/);
2020     }
2021 
2022     /**
2023      * Attempts to find a close config among a list
2024      * @param oldConfig the reference config.
2025      * @param configs the list of config to search through
2026      * @return the name of the closest config match, or possibly null if no configs are compatible
2027      * (this can only happen if the configs don't have a single qualifier that is the same).
2028      */
getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs)2029     private String getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs) {
2030 
2031         // create 2 lists as we're going to go through one and put the candidates in the other.
2032         ArrayList<DeviceConfig> list1 = new ArrayList<DeviceConfig>();
2033         ArrayList<DeviceConfig> list2 = new ArrayList<DeviceConfig>();
2034 
2035         list1.addAll(configs);
2036 
2037         final int count = FolderConfiguration.getQualifierCount();
2038         for (int i = 0 ; i < count ; i++) {
2039             // compute the new candidate list by only taking configs that have
2040             // the same i-th qualifier as the old config
2041             for (DeviceConfig c : list1) {
2042                 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
2043 
2044                 FolderConfiguration folderConfig = c.getConfig();
2045                 ResourceQualifier newQualifier = folderConfig.getQualifier(i);
2046 
2047                 if (oldQualifier == null) {
2048                     if (newQualifier == null) {
2049                         list2.add(c);
2050                     }
2051                 } else if (oldQualifier.equals(newQualifier)) {
2052                     list2.add(c);
2053                 }
2054             }
2055 
2056             // at any moment if the new candidate list contains only one match, its name
2057             // is returned.
2058             if (list2.size() == 1) {
2059                 return list2.get(0).getName();
2060             }
2061 
2062             // if the list is empty, then all the new configs failed. It is considered ok, and
2063             // we move to the next qualifier anyway. This way, if a qualifier is different for
2064             // all new configs it is simply ignored.
2065             if (list2.size() != 0) {
2066                 // move the candidates back into list1.
2067                 list1.clear();
2068                 list1.addAll(list2);
2069                 list2.clear();
2070             }
2071         }
2072 
2073         // the only way to reach this point is if there's an exact match.
2074         // (if there are more than one, then there's a duplicate config and it doesn't matter,
2075         // we take the first one).
2076         if (list1.size() > 0) {
2077             return list1.get(0).getName();
2078         }
2079 
2080         return null;
2081     }
2082 
2083     /**
2084      * fills the config combo with new values based on {@link #mState}.device.
2085      * @param refName an optional name. if set the selection will match this name (if found)
2086      */
fillConfigCombo(String refName)2087     private void fillConfigCombo(String refName) {
2088         mDeviceConfigCombo.removeAll();
2089 
2090         if (mState.device != null) {
2091             int selectionIndex = 0;
2092             int i = 0;
2093 
2094             for (DeviceConfig config : mState.device.getConfigs()) {
2095                 mDeviceConfigCombo.add(config.getName());
2096 
2097                 if (config.getName().equals(refName)) {
2098                     selectionIndex = i;
2099                 }
2100                 i++;
2101             }
2102 
2103             mDeviceConfigCombo.select(selectionIndex);
2104             mDeviceConfigCombo.setEnabled(mState.device.getConfigs().size() > 1);
2105         }
2106     }
2107 
2108     /**
2109      * Called when the device config selection changes.
2110      */
onDeviceConfigChange()2111     private void onDeviceConfigChange() {
2112         // because changing the content of a combo triggers a change event, respect the
2113         // mDisableUpdates flag
2114         if (mDisableUpdates > 0) {
2115             return;
2116         }
2117 
2118         if (computeCurrentConfig() && mListener != null) {
2119             mListener.onConfigurationChange();
2120             mListener.onDevicePostChange();
2121         }
2122     }
2123 
2124     /**
2125      * Call back for language combo selection
2126      */
onLocaleChange()2127     private void onLocaleChange() {
2128         // because mLocaleList triggers onLocaleChange at each modification, the filling
2129         // of the combo with data will trigger notifications, and we don't want that.
2130         if (mDisableUpdates > 0) {
2131             return;
2132         }
2133 
2134         if (computeCurrentConfig() &&  mListener != null) {
2135             mListener.onConfigurationChange();
2136         }
2137 
2138         // Store locale project-wide setting
2139         saveRenderState();
2140     }
2141 
onDockChange()2142     private void onDockChange() {
2143         if (computeCurrentConfig() &&  mListener != null) {
2144             mListener.onConfigurationChange();
2145         }
2146     }
2147 
onDayChange()2148     private void onDayChange() {
2149         if (computeCurrentConfig() &&  mListener != null) {
2150             mListener.onConfigurationChange();
2151         }
2152     }
2153 
2154     /**
2155      * Call back for api level combo selection
2156      */
onRenderingTargetChange()2157     private void onRenderingTargetChange() {
2158         // because mApiCombo triggers onApiLevelChange at each modification, the filling
2159         // of the combo with data will trigger notifications, and we don't want that.
2160         if (mDisableUpdates > 0) {
2161             return;
2162         }
2163 
2164         // tell the listener a new rendering target is being set. Need to do this before updating
2165         // mRenderingTarget.
2166         if (mListener != null && mRenderingTarget != null) {
2167             mListener.onRenderingTargetPreChange(mRenderingTarget);
2168         }
2169 
2170         int index = mTargetCombo.getSelectionIndex();
2171         mRenderingTarget = mTargetList.get(index);
2172 
2173         boolean computeOk = computeCurrentConfig();
2174 
2175         // force a theme update to reflect the new rendering target.
2176         // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2177         // to figure out the theme list.
2178         updateThemes();
2179 
2180         // since the state is saved in computeCurrentConfig, we need to resave it since theme
2181         // change could have impacted it.
2182         saveState();
2183 
2184         if (mListener != null && mRenderingTarget != null) {
2185             mListener.onRenderingTargetPostChange(mRenderingTarget);
2186         }
2187 
2188         // Store project-wide render-target setting
2189         saveRenderState();
2190 
2191         if (computeOk &&  mListener != null) {
2192             mListener.onConfigurationChange();
2193         }
2194     }
2195 
2196     /**
2197      * Saves the current state and the current configuration
2198      *
2199      * @see #saveState()
2200      */
computeCurrentConfig()2201     private boolean computeCurrentConfig() {
2202         saveState();
2203 
2204         if (mState.device != null) {
2205             // get the device config from the device/config combos.
2206             int configIndex = mDeviceConfigCombo.getSelectionIndex();
2207             String name = mDeviceConfigCombo.getItem(configIndex);
2208             FolderConfiguration config = mState.device.getFolderConfigByName(name);
2209 
2210             // replace the config with the one from the device
2211             mCurrentConfig.set(config);
2212 
2213             // replace the locale qualifiers with the one coming from the locale combo
2214             int index = mLocaleCombo.getSelectionIndex();
2215             if (index != -1) {
2216                 ResourceQualifier[] localeQualifiers = mLocaleList.get(index);
2217 
2218                 mCurrentConfig.setLanguageQualifier(
2219                         (LanguageQualifier)localeQualifiers[LOCALE_LANG]);
2220                 mCurrentConfig.setRegionQualifier(
2221                         (RegionQualifier)localeQualifiers[LOCALE_REGION]);
2222             }
2223 
2224             index = mUiModeCombo.getSelectionIndex();
2225             if (index == -1) {
2226                 index = 0; // no selection = 0
2227             }
2228             mCurrentConfig.setUiModeQualifier(new UiModeQualifier(UiMode.getByIndex(index)));
2229 
2230             index = mNightCombo.getSelectionIndex();
2231             if (index == -1) {
2232                 index = 0; // no selection = 0
2233             }
2234             mCurrentConfig.setNightModeQualifier(
2235                     new NightModeQualifier(NightMode.getByIndex(index)));
2236 
2237             // replace the API level by the selection of the combo
2238             index = mTargetCombo.getSelectionIndex();
2239             if (index == -1) {
2240                 index = mTargetList.indexOf(mProjectTarget);
2241             }
2242             if (index != -1) {
2243                 IAndroidTarget target = mTargetList.get(index);
2244 
2245                 if (target != null) {
2246                     mCurrentConfig.setVersionQualifier(
2247                             new VersionQualifier(target.getVersion().getApiLevel()));
2248                 }
2249             }
2250 
2251             // update the create button.
2252             checkCreateEnable();
2253 
2254             return true;
2255         }
2256 
2257         return false;
2258     }
2259 
onThemeChange()2260     private void onThemeChange() {
2261         saveState();
2262 
2263         int themeIndex = mThemeCombo.getSelectionIndex();
2264         if (themeIndex != -1) {
2265             String theme = mThemeCombo.getItem(themeIndex);
2266 
2267             if (theme.equals(THEME_SEPARATOR)) {
2268                 mThemeCombo.select(0);
2269             }
2270 
2271             if (mListener != null) {
2272                 mListener.onThemeChange();
2273             }
2274         }
2275     }
2276 
2277     /**
2278      * Returns whether the given <var>style</var> is a theme.
2279      * This is done by making sure the parent is a theme.
2280      * @param value the style to check
2281      * @param styleMap the map of styles for the current project. Key is the style name.
2282      * @return True if the given <var>style</var> is a theme.
2283      */
isTheme(ResourceValue value, Map<String, ResourceValue> styleMap)2284     private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap) {
2285         if (value instanceof StyleResourceValue) {
2286             StyleResourceValue style = (StyleResourceValue)value;
2287 
2288             boolean frameworkStyle = false;
2289             String parentStyle = style.getParentStyle();
2290             if (parentStyle == null) {
2291                 // if there is no specified parent style we look an implied one.
2292                 // For instance 'Theme.light' is implied child style of 'Theme',
2293                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
2294                 String name = style.getName();
2295                 int index = name.lastIndexOf('.');
2296                 if (index != -1) {
2297                     parentStyle = name.substring(0, index);
2298                 }
2299             } else {
2300                 // remove the useless @ if it's there
2301                 if (parentStyle.startsWith("@")) {
2302                     parentStyle = parentStyle.substring(1);
2303                 }
2304 
2305                 // check for framework identifier.
2306                 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
2307                     frameworkStyle = true;
2308                     parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
2309                 }
2310 
2311                 // at this point we could have the format style/<name>. we want only the name
2312                 if (parentStyle.startsWith("style/")) {
2313                     parentStyle = parentStyle.substring("style/".length());
2314                 }
2315             }
2316 
2317             if (parentStyle != null) {
2318                 if (frameworkStyle) {
2319                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
2320                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
2321                 } else {
2322                     // if it's a project style, we check this is a theme.
2323                     ResourceValue parentValue = styleMap.get(parentStyle);
2324 
2325                     // also prevent stackoverflow in case the dev mistakenly declared
2326                     // the parent of the style as the style itfself.
2327                     if (parentValue != null && parentValue.equals(value) == false) {
2328                         return isTheme(parentValue, styleMap);
2329                     }
2330                 }
2331             }
2332         }
2333 
2334         return false;
2335     }
2336 
checkCreateEnable()2337     private void checkCreateEnable() {
2338         mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
2339     }
2340 
2341     /**
2342      * Checks whether the current edited file is the best match for a given config.
2343      * <p/>
2344      * This tests against other versions of the same layout in the project.
2345      * <p/>
2346      * The given config must be compatible with the current edited file.
2347      * @param config the config to test.
2348      * @return true if the current edited file is the best match in the project for the
2349      * given config.
2350      */
isCurrentFileBestMatchFor(FolderConfiguration config)2351     private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
2352         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
2353                 ResourceFolderType.LAYOUT, config);
2354 
2355         if (match != null) {
2356             return match.getFile().equals(mEditedFile);
2357         } else {
2358             // if we stop here that means the current file is not even a match!
2359             AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
2360         }
2361 
2362         return false;
2363     }
2364 
2365     /**
2366      * Resets the configuration chooser to reflect the given file configuration. This is
2367      * intended to be used by the "Show Included In" functionality where the user has
2368      * picked a non-default configuration (such as a particular landscape layout) and the
2369      * configuration chooser must be switched to a landscape layout. This method will
2370      * trigger a model change.
2371      * <p>
2372      * This will NOT trigger a redraw event!
2373      * <p>
2374      * FIXME: We are currently setting the configuration file to be the configuration for
2375      * the "outer" (the including) file, rather than the inner file, which is the file the
2376      * user is actually editing. We need to refine this, possibly with a way for the user
2377      * to choose which configuration they are editing. And in particular, we should be
2378      * filtering the configuration chooser to only show options in the outer configuration
2379      * that are compatible with the inner included file.
2380      *
2381      * @param file the file to be configured
2382      */
resetConfigFor(IFile file)2383     public void resetConfigFor(IFile file) {
2384         setFile(file);
2385         mEditedConfig = null;
2386         onXmlModelLoaded();
2387     }
2388 
2389     /**
2390      * Syncs this configuration to the project wide locale and render target settings. The
2391      * locale may ignore the project-wide setting if it is a locale-specific
2392      * configuration.
2393      *
2394      * @return true if one or both of the toggles were changed, false if there were no
2395      *         changes
2396      */
syncRenderState()2397     public boolean syncRenderState() {
2398         if (mEditedConfig == null) {
2399             // Startup; ignore
2400             return false;
2401         }
2402 
2403         boolean localeChanged = false;
2404         boolean renderTargetChanged = false;
2405 
2406         // When a page is re-activated, force the toggles to reflect the current project
2407         // state
2408 
2409         Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
2410 
2411         // Only sync the locale if this layout is not already a locale-specific layout!
2412         if (!isLocaleSpecificLayout()) {
2413             ResourceQualifier[] locale = pair.getFirst();
2414             if (locale != null) {
2415                 localeChanged = setLocaleCombo(locale[0], locale[1]);
2416             }
2417         }
2418 
2419         // Sync render target
2420         IAndroidTarget target = pair.getSecond();
2421         if (target != null) {
2422             int targetIndex = mTargetList.indexOf(target);
2423             if (targetIndex != mTargetCombo.getSelectionIndex()) {
2424                 mTargetCombo.select(targetIndex);
2425                 renderTargetChanged = true;
2426             }
2427         }
2428 
2429         if (!renderTargetChanged && !localeChanged) {
2430             return false;
2431         }
2432 
2433         // Update the locale and/or the render target. This code contains a logical
2434         // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
2435         // such that we don't duplicate work.
2436 
2437         if (renderTargetChanged) {
2438             if (mListener != null && mRenderingTarget != null) {
2439                 mListener.onRenderingTargetPreChange(mRenderingTarget);
2440             }
2441             int targetIndex = mTargetCombo.getSelectionIndex();
2442             mRenderingTarget = mTargetList.get(targetIndex);
2443         }
2444 
2445         // Compute the new configuration; we want to do this both for locale changes
2446         // and for render targets.
2447         boolean computeOk = computeCurrentConfig();
2448 
2449         if (renderTargetChanged) {
2450             // force a theme update to reflect the new rendering target.
2451             // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2452             // to figure out the theme list.
2453             updateThemes();
2454 
2455             if (mListener != null && mRenderingTarget != null) {
2456                 mListener.onRenderingTargetPostChange(mRenderingTarget);
2457             }
2458         }
2459 
2460         // For both locale and render target changes
2461         if (computeOk &&  mListener != null) {
2462             mListener.onConfigurationChange();
2463         }
2464 
2465         return true;
2466     }
2467 
2468     /**
2469      * Loads the render state (the locale and the render target, which are shared among
2470      * all the layouts meaning that changing it in one will change it in all) and returns
2471      * the current project-wide locale and render target to be used.
2472      *
2473      * @return a pair of locale resource qualifiers and render target
2474      */
loadRenderState()2475     private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() {
2476         IProject project = mEditedFile.getProject();
2477         try {
2478             String data = project.getPersistentProperty(NAME_RENDER_STATE);
2479             if (data != null) {
2480                 ResourceQualifier[] locale = null;
2481                 IAndroidTarget target = null;
2482 
2483                 String[] values = data.split(SEP);
2484                 if (values.length == 2) {
2485                     locale = new ResourceQualifier[2];
2486                     String locales[] = values[0].split(SEP_LOCALE);
2487                     if (locales.length >= 2) {
2488                         if (locales[0].length() > 0) {
2489                             locale[0] = new LanguageQualifier(locales[0]);
2490                         }
2491                         if (locales[1].length() > 0) {
2492                             locale[1] = new RegionQualifier(locales[1]);
2493                         }
2494                     }
2495                     target = stringToTarget(values[1]);
2496 
2497                     // See if we should "correct" the rendering target to a better version.
2498                     // If you're using a pre-release version of the render target, and a
2499                     // final release is available and installed, we should switch to that
2500                     // one instead.
2501                     if (target != null) {
2502                         AndroidVersion version = target.getVersion();
2503                         if (version.getCodename() != null && mTargetList != null) {
2504                             int targetApiLevel = version.getApiLevel() + 1;
2505                             for (IAndroidTarget t : mTargetList) {
2506                                 if (t.getVersion().getApiLevel() == targetApiLevel
2507                                         && t.isPlatform()) {
2508                                     target = t;
2509                                     break;
2510                                 }
2511                             }
2512                         }
2513                     }
2514                 }
2515 
2516                 return Pair.of(locale, target);
2517             }
2518 
2519             ResourceQualifier[] any = new ResourceQualifier[] {
2520                     new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
2521                     new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
2522             };
2523 
2524             return Pair.of(any, findDefaultRenderTarget());
2525         } catch (CoreException e) {
2526             AdtPlugin.log(e, null);
2527         }
2528 
2529         return null;
2530     }
2531 
2532     /** Returns true if the current layout is locale-specific */
isLocaleSpecificLayout()2533     private boolean isLocaleSpecificLayout() {
2534         return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null;
2535     }
2536 
2537     /**
2538      * Saves the render state (the current locale and render target settings) into the
2539      * project wide settings storage
2540      */
saveRenderState()2541     private void saveRenderState() {
2542         IProject project = mEditedFile.getProject();
2543         try {
2544             int index = mLocaleCombo.getSelectionIndex();
2545             ResourceQualifier[] locale = mLocaleList.get(index);
2546             index = mTargetCombo.getSelectionIndex();
2547             IAndroidTarget target = mTargetList.get(index);
2548 
2549             // Generate a persistent string from locale+target
2550             StringBuilder sb = new StringBuilder();
2551             if (locale != null) {
2552                 if (locale[0] != null && locale[1] != null) {
2553                     // locale[0]/[1] can be null sometimes when starting Eclipse
2554                     sb.append(((LanguageQualifier) locale[0]).getValue());
2555                     sb.append(SEP_LOCALE);
2556                     sb.append(((RegionQualifier) locale[1]).getValue());
2557                 }
2558             }
2559             sb.append(SEP);
2560             if (target != null) {
2561                 sb.append(targetToString(target));
2562                 sb.append(SEP);
2563             }
2564 
2565             String data = sb.toString();
2566             project.setPersistentProperty(NAME_RENDER_STATE, data);
2567         } catch (CoreException e) {
2568             AdtPlugin.log(e, null);
2569         }
2570     }
2571 }
2572