• 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 com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
21 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
22 import com.android.ide.eclipse.adt.internal.resources.configurations.DockModeQualifier;
23 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
24 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
25 import com.android.ide.eclipse.adt.internal.resources.configurations.NightModeQualifier;
26 import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier;
27 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
28 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
29 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenDimensionQualifier;
30 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenOrientationQualifier;
31 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
32 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
33 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
34 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
35 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
37 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
38 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
39 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus;
40 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
42 import com.android.layoutlib.api.IResourceValue;
43 import com.android.layoutlib.api.IStyleResourceValue;
44 import com.android.sdklib.IAndroidTarget;
45 import com.android.sdklib.resources.Density;
46 import com.android.sdklib.resources.DockMode;
47 import com.android.sdklib.resources.NightMode;
48 import com.android.sdklib.resources.ScreenOrientation;
49 
50 import org.eclipse.core.resources.IFile;
51 import org.eclipse.core.resources.IFolder;
52 import org.eclipse.core.resources.IProject;
53 import org.eclipse.core.runtime.CoreException;
54 import org.eclipse.core.runtime.IStatus;
55 import org.eclipse.core.runtime.QualifiedName;
56 import org.eclipse.draw2d.geometry.Rectangle;
57 import org.eclipse.swt.SWT;
58 import org.eclipse.swt.events.SelectionAdapter;
59 import org.eclipse.swt.events.SelectionEvent;
60 import org.eclipse.swt.events.SelectionListener;
61 import org.eclipse.swt.graphics.Image;
62 import org.eclipse.swt.layout.GridData;
63 import org.eclipse.swt.layout.GridLayout;
64 import org.eclipse.swt.widgets.Button;
65 import org.eclipse.swt.widgets.Combo;
66 import org.eclipse.swt.widgets.Composite;
67 import org.eclipse.swt.widgets.Label;
68 
69 import java.util.ArrayList;
70 import java.util.Collections;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.Set;
74 import java.util.SortedSet;
75 import java.util.Map.Entry;
76 
77 /**
78  * A composite that displays the current configuration displayed in a Graphical Layout Editor.
79  * <p/>
80  * The composite has several entry points:<br>
81  * - {@link #setFile(File)}<br>
82  *   Called after the constructor to set the file being edited. Nothing else is performed.<br>
83  *<br>
84  * - {@link #onXmlModelLoaded()}<br>
85  *   Called when the XML model is loaded, either the first time or when the Target/SDK changes.
86  *   This initializes the UI, either with the first compatible configuration found, or attempts
87  *   to restore a configuration if one is found to have been saved in the file persistent storage.
88  *   (see {@link #storeState()})<br>
89  *<br>
90  * - {@link #replaceFile(File)}<br>
91  *   Called when a file, representing the same resource but with a different config is opened<br>
92  *   by the user.<br>
93  *<br>
94  * - {@link #changeFileOnNewConfig(FolderConfiguration)}<br>
95  *   Called when config change triggers the editing of a file with a different config.
96  *<p/>
97  * Additionally, the composite can handle the following events.<br>
98  * - SDK reload. This is when the main SDK is finished loading.<br>
99  * - Target reload. This is when the target used by the project is the edited file has finished<br>
100  *   loading.<br>
101  */
102 public class ConfigurationComposite extends Composite {
103 
104     private final static String CONFIG_STATE = "state";  //$NON-NLS-1$
105     private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
106 
107     private final static int LOCALE_LANG = 0;
108     private final static int LOCALE_REGION = 1;
109 
110     private Button mClippingButton;
111     private Label mCurrentLayoutLabel;
112 
113     private Combo mDeviceCombo;
114     private Combo mDeviceConfigCombo;
115     private Combo mLocaleCombo;
116     private Combo mDockCombo;
117     private Combo mNightCombo;
118     private Combo mThemeCombo;
119     private Button mCreateButton;
120 
121     private int mPlatformThemeCount = 0;
122     /** updates are disabled if > 0 */
123     private int mDisableUpdates = 0;
124 
125     private List<LayoutDevice> mDeviceList;
126 
127     private final ArrayList<ResourceQualifier[] > mLocaleList =
128         new ArrayList<ResourceQualifier[]>();
129 
130     /**
131      * clipping value. If true, the rendering is limited to the screensize. This is the default
132      * value
133      */
134     private boolean mClipping = true;
135 
136     private final ConfigState mState = new ConfigState();
137 
138     private boolean mSdkChanged = false;
139     private boolean mFirstXmlModelChange = true;
140 
141     /** The config listener given to the constructor. Never null. */
142     private final IConfigListener mListener;
143 
144     /** The {@link FolderConfiguration} representing the state of the UI controls */
145     private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
146 
147     /** The file being edited */
148     private IFile mEditedFile;
149     /** The {@link ProjectResources} for the edited file's project */
150     private ProjectResources mResources;
151     /** The target of the project of the file being edited. */
152     private IAndroidTarget mTarget;
153     /** The {@link FolderConfiguration} being edited. */
154     private FolderConfiguration mEditedConfig;
155 
156 
157     /**
158      * Interface implemented by the part which owns a {@link ConfigurationComposite}.
159      * This notifies the owners when the configuration change.
160      * The owner must also provide methods to provide the configuration that will
161      * be displayed.
162      */
163     public interface IConfigListener {
onConfigurationChange()164         void onConfigurationChange();
onThemeChange()165         void onThemeChange();
onCreate()166         void onCreate();
onClippingChange()167         void onClippingChange();
168 
getProjectResources()169         ProjectResources getProjectResources();
getFrameworkResources()170         ProjectResources getFrameworkResources();
getConfiguredProjectResources()171         Map<String, Map<String, IResourceValue>> getConfiguredProjectResources();
getConfiguredFrameworkResources()172         Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources();
173     }
174 
175     /**
176      * State of the current config. This is used during UI reset to attempt to return the
177      * rendering to its original configuration.
178      */
179     private class ConfigState {
180         private final static String SEP = ":"; //$NON-NLS-1$
181         private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
182 
183         LayoutDevice device;
184         String configName;
185         ResourceQualifier[] locale;
186         String theme;
187         /** dock mode. Guaranteed to be non null */
188         DockMode dock = DockMode.NONE;
189         /** night mode. Guaranteed to be non null */
190         NightMode night = NightMode.NOTNIGHT;
191 
getData()192         String getData() {
193             StringBuilder sb = new StringBuilder();
194             if (device != null) {
195                 sb.append(device.getName());
196                 sb.append(SEP);
197                 sb.append(configName);
198                 sb.append(SEP);
199                 if (locale != null) {
200                     if (locale[0] != null && locale[1] != null) {
201                         // locale[0]/[1] can be null sometimes when starting Eclipse
202                         sb.append(((LanguageQualifier) locale[0]).getValue());
203                         sb.append(SEP_LOCALE);
204                         sb.append(((RegionQualifier) locale[1]).getValue());
205                     }
206                 }
207                 sb.append(SEP);
208                 sb.append(theme);
209                 sb.append(SEP);
210                 sb.append(dock.getResourceValue());
211                 sb.append(SEP);
212                 sb.append(night.getResourceValue());
213                 sb.append(SEP);
214             }
215 
216             return sb.toString();
217         }
218 
setData(String data)219         boolean setData(String data) {
220             String[] values = data.split(SEP);
221             if (values.length == 6) {
222                 for (LayoutDevice d : mDeviceList) {
223                     if (d.getName().equals(values[0])) {
224                         device = d;
225                         FolderConfiguration config = device.getConfigs().get(values[1]);
226                         if (config != null) {
227                             configName = values[1];
228 
229                             locale = new ResourceQualifier[2];
230                             String locales[] = values[2].split(SEP_LOCALE);
231                             if (locales.length >= 2) {
232                                 if (locales[0].length() > 0) {
233                                     locale[0] = new LanguageQualifier(locales[0]);
234                                 }
235                                 if (locales[1].length() > 0) {
236                                     locale[1] = new RegionQualifier(locales[1]);
237                                 }
238                             }
239 
240                             theme = values[3];
241                             dock = DockMode.getEnum(values[4]);
242                             if (dock == null) {
243                                 dock = DockMode.NONE;
244                             }
245                             night = NightMode.getEnum(values[5]);
246                             if (night == null) {
247                                 night = NightMode.NOTNIGHT;
248                             }
249 
250                             return true;
251                         }
252                     }
253                 }
254             }
255 
256             return false;
257         }
258 
259         @Override
toString()260         public String toString() {
261             StringBuilder sb = new StringBuilder();
262             if (device != null) {
263                 sb.append(device.getName());
264             } else {
265                 sb.append("null");
266             }
267             sb.append(SEP);
268             sb.append(configName);
269             sb.append(SEP);
270             if (locale != null) {
271                 sb.append(((LanguageQualifier) locale[0]).getValue());
272                 sb.append(SEP_LOCALE);
273                 sb.append(((RegionQualifier) locale[1]).getValue());
274             }
275             sb.append(SEP);
276             sb.append(theme);
277             sb.append(SEP);
278             sb.append(dock.getResourceValue());
279             sb.append(SEP);
280             sb.append(night.getResourceValue());
281             sb.append(SEP);
282 
283             return sb.toString();
284         }
285     }
286 
287     /**
288      * Interface implemented by the part which owns a {@link ConfigurationComposite}
289      * to define and handle custom toggle buttons in the button bar. Each toggle is
290      * implemented using a button, with a callback when the button is selected.
291      */
292     public static abstract class CustomToggle {
293 
294         /** The UI label of the toggle. Can be null if the image exists. */
295         private final String mUiLabel;
296 
297         /** The image to use for this toggle. Can be null if the label exists. */
298         private final Image mImage;
299 
300         /** The tooltip for the toggle. Can be null. */
301         private final String mUiTooltip;
302 
303         /**
304          * Initializes a new {@link CustomToggle}. The values set here will be used
305          * later to create the actual toggle.
306          *
307          * @param uiLabel   The UI label of the toggle. Can be null if the image exists.
308          * @param image     The image to use for this toggle. Can be null if the label exists.
309          * @param uiTooltip The tooltip for the toggle. Can be null.
310          */
CustomToggle( String uiLabel, Image image, String uiTooltip)311         public CustomToggle(
312                 String uiLabel,
313                 Image image,
314                 String uiTooltip) {
315             mUiLabel = uiLabel;
316             mImage = image;
317             mUiTooltip = uiTooltip;
318         }
319 
320         /** Called by the {@link ConfigurationComposite} when the button is selected. */
onSelected(boolean newState)321         public abstract void onSelected(boolean newState);
322 
createToggle(Composite parent)323         private void createToggle(Composite parent) {
324             final Button b = new Button(parent, SWT.TOGGLE | SWT.FLAT);
325 
326             if (mUiTooltip != null) {
327                 b.setToolTipText(mUiTooltip);
328             }
329             if (mImage != null) {
330                 b.setImage(mImage);
331             }
332             if (mUiLabel != null) {
333                 b.setText(mUiLabel);
334             }
335 
336             b.addSelectionListener(new SelectionAdapter() {
337                 @Override
338                 public void widgetSelected(SelectionEvent e) {
339                     onSelected(b.getSelection());
340                 }
341             });
342         }
343     }
344 
345     /**
346      * Creates a new {@link ConfigurationComposite} and adds it to the parent.
347      *
348      * @param listener An {@link IConfigListener} that gets and sets configuration properties.
349      *          Mandatory, cannot be null.
350      * @param customToggles An array of {@link CustomToggle} to define extra toggles button
351      *          to display at the top of the composite. Can be empty or null.
352      * @param parent The parent composite.
353      * @param style The style of this composite.
354      */
ConfigurationComposite(IConfigListener listener, CustomToggle[] customToggles, Composite parent, int style)355     public ConfigurationComposite(IConfigListener listener,
356             CustomToggle[] customToggles,
357             Composite parent, int style) {
358         super(parent, style);
359         mListener = listener;
360 
361         if (customToggles == null) {
362             customToggles = new CustomToggle[0];
363         }
364 
365         GridLayout gl;
366         GridData gd;
367         int cols = 9;  // device+config+locale+dock+day/night+separator*2+theme+createBtn
368 
369         // ---- First line: custom buttons, clipping button, editing config display.
370         Composite labelParent = new Composite(this, SWT.NONE);
371         labelParent.setLayout(gl = new GridLayout(3 + customToggles.length, false));
372         gl.marginWidth = gl.marginHeight = 0;
373         labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
374         gd.horizontalSpan = cols;
375 
376         new Label(labelParent, SWT.NONE).setText("Editing config:");
377         mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
378         mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
379         gd.widthHint = 50;
380 
381         for (CustomToggle toggle : customToggles) {
382             toggle.createToggle(labelParent);
383         }
384 
385         mClippingButton = new Button(labelParent, SWT.TOGGLE | SWT.FLAT);
386         mClippingButton.setSelection(mClipping);
387         mClippingButton.setToolTipText("Toggles screen clipping on/off");
388         mClippingButton.setImage(IconFactory.getInstance().getIcon("clipping")); //$NON-NLS-1$
389         mClippingButton.addSelectionListener(new SelectionAdapter() {
390             @Override
391             public void widgetSelected(SelectionEvent e) {
392                 onClippingChange();
393             }
394         });
395 
396         // ---- 2nd line: device/config/locale/theme Combos, create button.
397 
398         setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
399         setLayout(gl = new GridLayout(cols, false));
400         gl.marginHeight = 0;
401         gl.horizontalSpacing = 0;
402 
403         mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
404         mDeviceCombo.setLayoutData(new GridData(
405                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
406         mDeviceCombo.addSelectionListener(new SelectionAdapter() {
407             @Override
408             public void widgetSelected(SelectionEvent e) {
409                 onDeviceChange(true /* recomputeLayout*/);
410             }
411         });
412 
413         mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
414         mDeviceConfigCombo.setLayoutData(new GridData(
415                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
416         mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
417             @Override
418              public void widgetSelected(SelectionEvent e) {
419                 onDeviceConfigChange();
420             }
421         });
422 
423         mLocaleCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
424         mLocaleCombo.setLayoutData(new GridData(
425                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
426         mLocaleCombo.addSelectionListener(new SelectionListener() {
427             public void widgetDefaultSelected(SelectionEvent e) {
428                 onLocaleChange();
429             }
430             public void widgetSelected(SelectionEvent e) {
431                 onLocaleChange();
432             }
433         });
434 
435         mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
436         mDockCombo.setLayoutData(new GridData(
437                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
438         for (DockMode mode : DockMode.values()) {
439             mDockCombo.add(mode.getLongDisplayValue());
440         }
441         mDockCombo.addSelectionListener(new SelectionListener() {
442             public void widgetDefaultSelected(SelectionEvent e) {
443                 onDockChange();
444             }
445             public void widgetSelected(SelectionEvent e) {
446                 onDockChange();
447             }
448         });
449 
450         mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
451         mNightCombo.setLayoutData(new GridData(
452                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
453         for (NightMode mode : NightMode.values()) {
454             mNightCombo.add(mode.getLongDisplayValue());
455         }
456         mNightCombo.addSelectionListener(new SelectionListener() {
457             public void widgetDefaultSelected(SelectionEvent e) {
458                 onDayChange();
459             }
460             public void widgetSelected(SelectionEvent e) {
461                 onDayChange();
462             }
463         });
464 
465         // first separator
466         Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
467         separator.setLayoutData(gd = new GridData(
468                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
469         gd.heightHint = 0;
470 
471         mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
472         mThemeCombo.setLayoutData(new GridData(
473                 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
474         mThemeCombo.setEnabled(false);
475 
476         mThemeCombo.addSelectionListener(new SelectionAdapter() {
477             @Override
478             public void widgetSelected(SelectionEvent e) {
479                 onThemeChange();
480             }
481         });
482 
483         // second separator
484         separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
485         separator.setLayoutData(gd = new GridData(
486                 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
487         gd.heightHint = 0;
488 
489         mCreateButton = new Button(this, SWT.PUSH | SWT.FLAT);
490         mCreateButton.setText("Create...");
491         mCreateButton.setEnabled(false);
492         mCreateButton.addSelectionListener(new SelectionAdapter() {
493             @Override
494             public void widgetSelected(SelectionEvent e) {
495                 if (mListener != null) {
496                     mListener.onCreate();
497                 }
498             }
499         });
500     }
501 
502     // ---- Init and reset/reload methods ----
503 
504     /**
505      * Sets the reference to the file being edited.
506      * <p/>The UI is intialized in {@link #onXmlModelLoaded()} which is called as the XML model is
507      * loaded (or reloaded as the SDK/target changes).
508      *
509      * @param file the file being opened
510      *
511      * @see #onXmlModelLoaded()
512      * @see #replaceFile(FolderConfiguration)
513      * @see #changeFileOnNewConfig(FolderConfiguration)
514      */
setFile(IFile file)515     public void setFile(IFile file) {
516         mEditedFile = file;
517     }
518 
519     /**
520      * Replaces the UI with a given file configuration. This is meant to answer the user
521      * explicitly opening a different version of the same layout from the Package Explorer.
522      * <p/>This attempts to keep the current config, but may change it if it's not compatible or
523      * not the best match
524      * <p/>This will NOT trigger a redraw event (will not call
525      * {@link IConfigListener#onConfigurationChange()}.)
526      * @param file the file being opened.
527      * @param fileConfig The {@link FolderConfiguration} of the opened file.
528      * @param target the {@link IAndroidTarget} of the file's project.
529      *
530      * @see #replaceFile(FolderConfiguration)
531      */
replaceFile(IFile file)532     public void replaceFile(IFile file) {
533         // if there is no previous selection, revert to default mode.
534         if (mState.device == null) {
535             setFile(file); // onTargetChanged will be called later.
536             return;
537         }
538 
539         mEditedFile = file;
540         IProject iProject = mEditedFile.getProject();
541         mResources = ResourceManager.getInstance().getProjectResources(iProject);
542 
543         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
544         mEditedConfig = resFolder.getConfiguration();
545 
546         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
547                            // new values in the widgets.
548 
549         // only attempt to do anything if the SDK and targets are loaded.
550         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
551         if (sdkStatus == LoadStatus.LOADED) {
552             LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null);
553 
554             if (targetStatus == LoadStatus.LOADED) {
555 
556                 // update the current config selection to make sure it's
557                 // compatible with the new file
558                 adaptConfigSelection(true /*needBestMatch*/);
559 
560                 // compute the final current config
561                 computeCurrentConfig(true /*force*/);
562 
563                 // update the string showing the config value
564                 updateConfigDisplay(mEditedConfig);
565             }
566         }
567 
568         mDisableUpdates--;
569     }
570 
571     /**
572      * Updates the UI with a new file that was opened in response to a config change.
573      * @param file the file being opened.
574      *
575      * @see #openFile(FolderConfiguration, IAndroidTarget)
576      * @see #replaceFile(FolderConfiguration)
577      */
changeFileOnNewConfig(IFile file)578     public void changeFileOnNewConfig(IFile file) {
579         mEditedFile = file;
580         IProject iProject = mEditedFile.getProject();
581         mResources = ResourceManager.getInstance().getProjectResources(iProject);
582 
583         ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
584         mEditedConfig = resFolder.getConfiguration();
585 
586         // All that's needed is to update the string showing the config value
587         // (since the config combo were chosen by the user).
588         updateConfigDisplay(mEditedConfig);
589     }
590 
591     /**
592      * Responds to the event that the basic SDK information finished loading.
593      * @param target the possibly new target object associated with the file being edited (in case
594      * the SDK path was changed).
595      */
onSdkLoaded(IAndroidTarget target)596     public void onSdkLoaded(IAndroidTarget target) {
597         // a change to the SDK means that we need to check for new/removed devices.
598         mSdkChanged = true;
599 
600         // store the new target.
601         mTarget = target;
602 
603         mDisableUpdates++; // we do not want to trigger onXXXChange when setting
604                            // new values in the widgets.
605 
606         // this is going to be followed by a call to onTargetLoaded.
607         // So we can only care about the layout devices in this case.
608         initDevices();
609 
610         mDisableUpdates--;
611     }
612 
613     /**
614      * Answers to the XML model being loaded, either the first time or when the Targget/SDK changes.
615      * <p>This initializes the UI, either with the first compatible configuration found,
616      * or attempts to restore a configuration if one is found to have been saved in the file
617      * persistent storage.
618      * <p>If the SDK or target are not loaded, nothing will happend (but the method must be called
619      * back when those are loaded).
620      * <p>The method automatically handles being called the first time after editor creation, or
621      * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
622      * is properly called).
623      *
624      * @see #storeState()
625      * @see #onSdkLoaded(IAndroidTarget)
626      */
onXmlModelLoaded()627     public void onXmlModelLoaded() {
628         // only attempt to do anything if the SDK and targets are loaded.
629         LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
630         if (sdkStatus == LoadStatus.LOADED) {
631             mDisableUpdates++; // we do not want to trigger onXXXChange when setting
632 
633             // init the devices if needed (new SDK or first time going through here)
634             if (mSdkChanged || mFirstXmlModelChange) {
635                 initDevices();
636             }
637 
638             IProject iProject = mEditedFile.getProject();
639 
640             Sdk currentSdk = Sdk.getCurrent();
641             if (currentSdk != null) {
642                 mTarget = currentSdk.getTarget(iProject);
643             }
644 
645             LoadStatus targetStatus = LoadStatus.FAILED;
646             if (mTarget != null) {
647                 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null);
648             }
649 
650             if (targetStatus == LoadStatus.LOADED) {
651                 if (mResources == null) {
652                     mResources = ResourceManager.getInstance().getProjectResources(iProject);
653                 }
654                 if (mEditedConfig == null) {
655                     ResourceFolder resFolder = mResources.getResourceFolder(
656                             (IFolder) mEditedFile.getParent());
657                     mEditedConfig = resFolder.getConfiguration();
658                 }
659 
660                 // update the clipping state
661                 AndroidTargetData targetData = Sdk.getCurrent().getTargetData(mTarget);
662                 if (targetData != null) {
663                     LayoutBridge bridge = targetData.getLayoutBridge();
664                     setClippingSupport(bridge.apiLevel >= 4);
665                 }
666 
667                 // get the file stored state
668                 boolean loadedConfigData = false;
669                 try {
670                     QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE);
671                     String data = mEditedFile.getPersistentProperty(qname);
672                     if (data != null) {
673                         loadedConfigData = mState.setData(data);
674                     }
675                 } catch (CoreException e) {
676                     // pass
677                 }
678 
679                 // update the themes and locales.
680                 updateThemes();
681                 updateLocales();
682 
683                 // If the current state was loaded from the persistent storage, we update the
684                 // UI with it and then try to adapt it (which will handle incompatible
685                 // configuration).
686                 // Otherwise, just look for the first compatible configuration.
687                 if (loadedConfigData) {
688                     // first make sure we have the config to adapt
689                     selectDevice(mState.device);
690                     fillConfigCombo(mState.configName);
691 
692                     adaptConfigSelection(false /*needBestMatch*/);
693 
694                     mDockCombo.select(DockMode.getIndex(mState.dock));
695                     mNightCombo.select(NightMode.getIndex(mState.night));
696                 } else {
697                     findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
698 
699                     mDockCombo.select(0);
700                     mNightCombo.select(0);
701                 }
702 
703                 // update the string showing the config value
704                 updateConfigDisplay(mEditedConfig);
705 
706                 // compute the final current config
707                 computeCurrentConfig(true /*force*/);
708             }
709 
710             mDisableUpdates--;
711             mFirstXmlModelChange  = false;
712         }
713     }
714 
715     /**
716      * Finds a device/config that can display {@link #mEditedConfig}.
717      * <p/>Once found the device and config combos are set to the config.
718      * <p/>If there is no compatible configuration, a custom one is created.
719      * @param favorCurrentConfig if true, and no best match is found, don't change
720      * the current config. This must only be true if the current config is compatible.
721      */
findAndSetCompatibleConfig(boolean favorCurrentConfig)722     private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
723         LayoutDevice anyDeviceMatch = null; // a compatible device/config/locale
724         String anyConfigMatchName = null;
725         int anyLocaleIndex = -1;
726 
727         LayoutDevice bestDeviceMatch = null; // an actual best match
728         String bestConfigMatchName = null;
729         int bestLocaleIndex = -1;
730 
731         FolderConfiguration testConfig = new FolderConfiguration();
732 
733         mainloop: for (LayoutDevice device : mDeviceList) {
734             for (Entry<String, FolderConfiguration> entry :
735                     device.getConfigs().entrySet()) {
736                 testConfig.set(entry.getValue());
737 
738                 // look on the locales.
739                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
740                     ResourceQualifier[] locale = mLocaleList.get(i);
741 
742                     // update the test config with the locale qualifiers
743                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
744                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
745 
746                     if (mEditedConfig.isMatchFor(testConfig)) {
747                         // this is a basic match. record it in case we don't find a match
748                         // where the edited file is a best config.
749                         if (anyDeviceMatch == null) {
750                             anyDeviceMatch = device;
751                             anyConfigMatchName = entry.getKey();
752                             anyLocaleIndex = i;
753                         }
754 
755                         if (isCurrentFileBestMatchFor(testConfig)) {
756                             // this is what we want.
757                             bestDeviceMatch = device;
758                             bestConfigMatchName = entry.getKey();
759                             bestLocaleIndex = i;
760                             break mainloop;
761                         }
762                     }
763                 }
764             }
765         }
766 
767         if (bestDeviceMatch == null) {
768             if (favorCurrentConfig) {
769                 // quick check
770                 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
771                     AdtPlugin.log(IStatus.ERROR,
772                             "favorCurrentConfig can only be true if the current config is compatible");
773                 }
774 
775                 // just display the warning
776                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
777                         String.format(
778                                 "'%1$s' is not a best match for any device/locale combination.",
779                                 mEditedConfig.toDisplayString()),
780                         String.format(
781                                 "Displaying it with '%1$s'",
782                                 mCurrentConfig.toDisplayString()));
783             } else if (anyDeviceMatch != null) {
784                 // select the device anyway.
785                 selectDevice(mState.device = anyDeviceMatch);
786                 fillConfigCombo(anyConfigMatchName);
787                 mLocaleCombo.select(anyLocaleIndex);
788 
789                 // TODO: display a better warning!
790                 computeCurrentConfig(false /*force*/);
791                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
792                         String.format(
793                                 "'%1$s' is not a best match for any device/locale combination.",
794                                 mEditedConfig.toDisplayString()),
795                         String.format(
796                                 "Displaying it with '%1$s'",
797                                 mCurrentConfig.toDisplayString()));
798 
799             } else {
800                 // TODO: there is no device/config able to display the layout, create one.
801                 // For the base config values, we'll take the first device and config,
802                 // and replace whatever qualifier required by the layout file.
803             }
804         } else {
805             selectDevice(mState.device = bestDeviceMatch);
806             fillConfigCombo(bestConfigMatchName);
807             mLocaleCombo.select(bestLocaleIndex);
808         }
809     }
810 
811     /**
812      * Adapts the current device/config selection so that it's compatible with
813      * {@link #mEditedConfig}.
814      * <p/>If the current selection is compatible, nothing is changed.
815      * <p/>If it's not compatible, configs from the current devices are tested.
816      * <p/>If none are compatible, it reverts to
817      * {@link #findAndSetCompatibleConfig(FolderConfiguration)}
818      */
adaptConfigSelection(boolean needBestMatch)819     private void adaptConfigSelection(boolean needBestMatch) {
820         // check the device config (ie sans locale)
821         boolean needConfigChange = true; // if still true, we need to find another config.
822         boolean currentConfigIsCompatible = false;
823         int configIndex = mDeviceConfigCombo.getSelectionIndex();
824         if (configIndex != -1) {
825             String configName = mDeviceConfigCombo.getItem(configIndex);
826             FolderConfiguration currentConfig = mState.device.getConfigs().get(configName);
827             if (mEditedConfig.isMatchFor(currentConfig)) {
828                 currentConfigIsCompatible = true; // current config is compatible
829                 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
830                     needConfigChange = false;
831                 }
832             }
833         }
834 
835         if (needConfigChange) {
836             // if the current config/locale isn't a correct match, then
837             // look for another config/locale in the same device.
838             FolderConfiguration testConfig = new FolderConfiguration();
839 
840             // first look in the current device.
841             String matchName = null;
842             int localeIndex = -1;
843             Map<String, FolderConfiguration> configs = mState.device.getConfigs();
844             mainloop: for (Entry<String, FolderConfiguration> entry : configs.entrySet()) {
845                 testConfig.set(entry.getValue());
846 
847                 // loop on the locales.
848                 for (int i = 0 ; i < mLocaleList.size() ; i++) {
849                     ResourceQualifier[] locale = mLocaleList.get(i);
850 
851                     // update the test config with the locale qualifiers
852                     testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
853                     testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
854 
855                     if (mEditedConfig.isMatchFor(testConfig) &&
856                             isCurrentFileBestMatchFor(testConfig)) {
857                         matchName = entry.getKey();
858                         localeIndex = i;
859                         break mainloop;
860                     }
861                 }
862             }
863 
864             if (matchName != null) {
865                 selectConfig(matchName);
866                 mLocaleCombo.select(localeIndex);
867             } else {
868                 // no match in current device with any config/locale
869                 // attempt to find another device that can display this particular config.
870                 findAndSetCompatibleConfig(currentConfigIsCompatible);
871             }
872         }
873     }
874 
875     /**
876      * Finds a locale matching the config from a file.
877      * @param language the language qualifier or null if none is set.
878      * @param region the region qualifier or null if none is set.
879      */
setLocaleCombo(ResourceQualifier language, ResourceQualifier region)880     private void setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
881         // find the locale match. Since the locale list is based on the content of the
882         // project resources there must be an exact match.
883         // The only trick is that the region could be null in the fileConfig but in our
884         // list of locales, this is represented as a RegionQualifier with value of
885         // FAKE_LOCALE_VALUE.
886         final int count = mLocaleList.size();
887         for (int i = 0 ; i < count ; i++) {
888             ResourceQualifier[] locale = mLocaleList.get(i);
889 
890             // the language qualifier in the locale list is never null.
891             if (locale[LOCALE_LANG].equals(language)) {
892                 // region comparison is more complex, as the region could be null.
893                 if (region == null) {
894                     if (RegionQualifier.FAKE_REGION_VALUE.equals(
895                             ((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
896                         // match!
897                         mLocaleCombo.select(i);
898                         break;
899                     }
900                 } else if (region.equals(locale[LOCALE_REGION])) {
901                     // match!
902                     mLocaleCombo.select(i);
903                     break;
904                 }
905             }
906         }
907     }
908 
updateConfigDisplay(FolderConfiguration fileConfig)909     private void updateConfigDisplay(FolderConfiguration fileConfig) {
910         String current = fileConfig.toDisplayString();
911         mCurrentLayoutLabel.setText(current != null ? current : "(Default)");
912     }
913 
saveState(boolean force)914     private void saveState(boolean force) {
915         if (mDisableUpdates == 0) {
916             int index = mDeviceConfigCombo.getSelectionIndex();
917             if (index != -1) {
918                 mState.configName = mDeviceConfigCombo.getItem(index);
919             } else {
920                 mState.configName = null;
921             }
922 
923             // since the locales are relative to the project, only keeping the index is enough
924             index = mLocaleCombo.getSelectionIndex();
925             if (index != -1) {
926                 mState.locale = mLocaleList.get(index);
927             } else {
928                 mState.locale = null;
929             }
930 
931             index = mThemeCombo.getSelectionIndex();
932             if (index != -1) {
933                 mState.theme = mThemeCombo.getItem(index);
934             }
935 
936             index = mDockCombo.getSelectionIndex();
937             if (index != -1) {
938                 mState.dock = DockMode.getByIndex(index);
939             }
940 
941             index = mNightCombo.getSelectionIndex();
942             if (index != -1) {
943                 mState.night = NightMode.getByIndex(index);
944             }
945         }
946     }
947 
948     /**
949      * Stores the current config selection into the edited file.
950      */
storeState()951     public void storeState() {
952         try {
953             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE); //$NON-NLS-1$
954             mEditedFile.setPersistentProperty(qname, mState.getData());
955         } catch (CoreException e) {
956             // pass
957         }
958     }
959 
960     /**
961      * Updates the locale combo.
962      * This must be called from the UI thread.
963      */
updateLocales()964     public void updateLocales() {
965         if (mListener == null) {
966             return; // can't do anything w/o it.
967         }
968 
969         mDisableUpdates++;
970 
971         // Reset the combo
972         mLocaleCombo.removeAll();
973         mLocaleList.clear();
974 
975         SortedSet<String> languages = null;
976         boolean hasLocale = false;
977 
978         // get the languages from the project.
979         ProjectResources project = mListener.getProjectResources();
980 
981         // in cases where the opened file is not linked to a project, this could be null.
982         if (project != null) {
983             // now get the languages from the project.
984             languages = project.getLanguages();
985 
986             for (String language : languages) {
987                 hasLocale = true;
988 
989                 LanguageQualifier langQual = new LanguageQualifier(language);
990 
991                 // find the matching regions and add them
992                 SortedSet<String> regions = project.getRegions(language);
993                 for (String region : regions) {
994                     mLocaleCombo.add(String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$
995                     RegionQualifier regionQual = new RegionQualifier(region);
996                     mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
997                 }
998 
999                 // now the entry for the other regions the language alone
1000                 if (regions.size() > 0) {
1001                     mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$
1002                 } else {
1003                     mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$
1004                 }
1005                 // create a region qualifier that will never be matched by qualified resources.
1006                 mLocaleList.add(new ResourceQualifier[] {
1007                         langQual,
1008                         new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1009                 });
1010             }
1011         }
1012 
1013         // add a locale not present in the project resources. This will let the dev
1014         // tests his/her default values.
1015         if (hasLocale) {
1016             mLocaleCombo.add("Other");
1017         } else {
1018             mLocaleCombo.add("Any locale");
1019         }
1020 
1021         // create language/region qualifier that will never be matched by qualified resources.
1022         mLocaleList.add(new ResourceQualifier[] {
1023                 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
1024                 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1025         });
1026 
1027         if (mState.locale != null) {
1028             // FIXME: this may fails if the layout was deleted (and was the last one to have that local.
1029             // (we have other problem in this case though)
1030             setLocaleCombo(mState.locale[LOCALE_LANG],
1031                     mState.locale[LOCALE_REGION]);
1032         } else {
1033             mLocaleCombo.select(0);
1034         }
1035 
1036         mThemeCombo.getParent().layout();
1037 
1038         mDisableUpdates--;
1039     }
1040 
1041     /**
1042      * Updates the theme combo.
1043      * This must be called from the UI thread.
1044      */
updateThemes()1045     private void updateThemes() {
1046         if (mListener == null) {
1047             return; // can't do anything w/o it.
1048         }
1049 
1050         ProjectResources frameworkProject = mListener.getFrameworkResources();
1051 
1052         mDisableUpdates++;
1053 
1054         // Reset the combo
1055         mThemeCombo.removeAll();
1056         mPlatformThemeCount = 0;
1057 
1058         ArrayList<String> themes = new ArrayList<String>();
1059 
1060         // get the themes, and languages from the Framework.
1061         if (frameworkProject != null) {
1062             // get the configured resources for the framework
1063             Map<String, Map<String, IResourceValue>> frameworResources =
1064                 mListener.getConfiguredFrameworkResources();
1065 
1066             if (frameworResources != null) {
1067                 // get the styles.
1068                 Map<String, IResourceValue> styles = frameworResources.get(
1069                         ResourceType.STYLE.getName());
1070 
1071 
1072                 // collect the themes out of all the styles.
1073                 for (IResourceValue value : styles.values()) {
1074                     String name = value.getName();
1075                     if (name.startsWith("Theme.") || name.equals("Theme")) {
1076                         themes.add(value.getName());
1077                         mPlatformThemeCount++;
1078                     }
1079                 }
1080 
1081                 // sort them and add them to the combo
1082                 Collections.sort(themes);
1083 
1084                 for (String theme : themes) {
1085                     mThemeCombo.add(theme);
1086                 }
1087 
1088                 mPlatformThemeCount = themes.size();
1089                 themes.clear();
1090             }
1091         }
1092 
1093         // now get the themes and languages from the project.
1094         ProjectResources project = mListener.getProjectResources();
1095         // in cases where the opened file is not linked to a project, this could be null.
1096         if (project != null) {
1097             // get the configured resources for the project
1098             Map<String, Map<String, IResourceValue>> configuredProjectRes =
1099                 mListener.getConfiguredProjectResources();
1100 
1101             if (configuredProjectRes != null) {
1102                 // get the styles.
1103                 Map<String, IResourceValue> styleMap = configuredProjectRes.get(
1104                         ResourceType.STYLE.getName());
1105 
1106                 if (styleMap != null) {
1107                     // collect the themes out of all the styles, ie styles that extend,
1108                     // directly or indirectly a platform theme.
1109                     for (IResourceValue value : styleMap.values()) {
1110                         if (isTheme(value, styleMap)) {
1111                             themes.add(value.getName());
1112                         }
1113                     }
1114 
1115                     // sort them and add them the to the combo.
1116                     if (mPlatformThemeCount > 0 && themes.size() > 0) {
1117                         mThemeCombo.add(THEME_SEPARATOR);
1118                     }
1119 
1120                     Collections.sort(themes);
1121 
1122                     for (String theme : themes) {
1123                         mThemeCombo.add(theme);
1124                     }
1125                 }
1126             }
1127         }
1128 
1129         // try to reselect the previous theme.
1130         if (mState.theme != null) {
1131             final int count = mThemeCombo.getItemCount();
1132             for (int i = 0 ; i < count ; i++) {
1133                 if (mState.theme.equals(mThemeCombo.getItem(i))) {
1134                     mThemeCombo.select(i);
1135                     break;
1136                 }
1137             }
1138             mThemeCombo.setEnabled(true);
1139         } else if (mThemeCombo.getItemCount() > 0) {
1140             mThemeCombo.select(0);
1141             mThemeCombo.setEnabled(true);
1142         } else {
1143             mThemeCombo.setEnabled(false);
1144         }
1145 
1146         mThemeCombo.getParent().layout();
1147 
1148         mDisableUpdates--;
1149     }
1150 
1151     // ---- getters for the config selection values ----
1152 
getEditedConfig()1153     public FolderConfiguration getEditedConfig() {
1154         return mEditedConfig;
1155     }
1156 
getCurrentConfig()1157     public FolderConfiguration getCurrentConfig() {
1158         return mCurrentConfig;
1159     }
1160 
getCurrentConfig(FolderConfiguration config)1161     public void getCurrentConfig(FolderConfiguration config) {
1162         config.set(mCurrentConfig);
1163     }
1164 
1165     /**
1166      * Returns the currently selected {@link Density}. This is guaranteed to be non null.
1167      */
getDensity()1168     public Density getDensity() {
1169         if (mCurrentConfig != null) {
1170             PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier();
1171             if (qual != null) {
1172                 // just a sanity check
1173                 Density d = qual.getValue();
1174                 if (d != Density.NODPI) {
1175                     return d;
1176                 }
1177             }
1178         }
1179 
1180         // no config? return medium as the default density.
1181         return Density.MEDIUM;
1182     }
1183 
1184     /**
1185      * Returns the current device xdpi.
1186      */
getXDpi()1187     public float getXDpi() {
1188         if (mState.device != null) {
1189             float dpi = mState.device.getXDpi();
1190             if (Float.isNaN(dpi) == false) {
1191                 return dpi;
1192             }
1193         }
1194 
1195         // get the pixel density as the density.
1196         return getDensity().getDpiValue();
1197     }
1198 
1199     /**
1200      * Returns the current device ydpi.
1201      */
getYDpi()1202     public float getYDpi() {
1203         if (mState.device != null) {
1204             float dpi = mState.device.getYDpi();
1205             if (Float.isNaN(dpi) == false) {
1206                 return dpi;
1207             }
1208         }
1209 
1210         // get the pixel density as the density.
1211         return getDensity().getDpiValue();
1212     }
1213 
getScreenBounds()1214     public Rectangle getScreenBounds() {
1215         // get the orientation from the current device config
1216         ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
1217         ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
1218         if (qual != null) {
1219             orientation = qual.getValue();
1220         }
1221 
1222         // get the device screen dimension
1223         ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
1224         int s1, s2;
1225         if (qual2 != null) {
1226             s1 = qual2.getValue1();
1227             s2 = qual2.getValue2();
1228         } else {
1229             s1 = 480;
1230             s2 = 320;
1231         }
1232 
1233         switch (orientation) {
1234             default:
1235             case PORTRAIT:
1236                 return new Rectangle(0, 0, s2, s1);
1237             case LANDSCAPE:
1238                 return new Rectangle(0, 0, s1, s2);
1239             case SQUARE:
1240                 return new Rectangle(0, 0, s1, s1);
1241         }
1242     }
1243 
1244 
1245     /**
1246      * Returns the current theme, or null if the combo has no selection.
1247      */
getTheme()1248     public String getTheme() {
1249         int themeIndex = mThemeCombo.getSelectionIndex();
1250         if (themeIndex != -1) {
1251             return mThemeCombo.getItem(themeIndex);
1252         }
1253 
1254         return null;
1255     }
1256 
1257     /**
1258      * Returns whether the current theme selection is a project theme.
1259      * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
1260      * @return true for project theme, false for framework theme
1261      */
isProjectTheme()1262     public boolean isProjectTheme() {
1263         return mThemeCombo.getSelectionIndex() >= mPlatformThemeCount;
1264     }
1265 
getClipping()1266     public boolean getClipping() {
1267         return mClipping;
1268     }
1269 
setClippingSupport(boolean b)1270     private void setClippingSupport(boolean b) {
1271         mClippingButton.setEnabled(b);
1272         if (b) {
1273             mClippingButton.setToolTipText("Toggles screen clipping on/off");
1274         } else {
1275             mClipping = true;
1276             mClippingButton.setSelection(true);
1277             mClippingButton.setToolTipText("Non clipped rendering is not supported");
1278         }
1279     }
1280 
1281     /**
1282      * Loads the list of {@link LayoutDevice} and inits the UI with it.
1283      */
initDevices()1284     private void initDevices() {
1285         mDeviceList = null;
1286 
1287         Sdk sdk = Sdk.getCurrent();
1288         if (sdk != null) {
1289             LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
1290             mDeviceList = manager.getCombinedList();
1291         }
1292 
1293 
1294         // remove older devices if applicable
1295         mDeviceCombo.removeAll();
1296         mDeviceConfigCombo.removeAll();
1297 
1298         // fill with the devices
1299         if (mDeviceList != null) {
1300             for (LayoutDevice device : mDeviceList) {
1301                 mDeviceCombo.add(device.getName());
1302             }
1303             mDeviceCombo.select(0);
1304 
1305             if (mDeviceList.size() > 0) {
1306                 Map<String, FolderConfiguration> configs = mDeviceList.get(0).getConfigs();
1307                 Set<String> configNames = configs.keySet();
1308                 for (String name : configNames) {
1309                     mDeviceConfigCombo.add(name);
1310                 }
1311                 mDeviceConfigCombo.select(0);
1312                 if (configNames.size() == 1) {
1313                     mDeviceConfigCombo.setEnabled(false);
1314                 }
1315             }
1316         }
1317 
1318         // add the custom item
1319         mDeviceCombo.add("Custom...");
1320     }
1321 
1322     /**
1323      * Selects a given {@link LayoutDevice} in the device combo, if it is found.
1324      * @param device the device to select
1325      * @return true if the device was found.
1326      */
selectDevice(LayoutDevice device)1327     private boolean selectDevice(LayoutDevice device) {
1328         final int count = mDeviceList.size();
1329         for (int i = 0 ; i < count ; i++) {
1330             // since device comes from mDeviceList, we can use the == operator.
1331             if (device == mDeviceList.get(i)) {
1332                 mDeviceCombo.select(i);
1333                 return true;
1334             }
1335         }
1336 
1337         return false;
1338     }
1339 
1340     /**
1341      * Selects a config by name.
1342      * @param name the name of the config to select.
1343      */
selectConfig(String name)1344     private void selectConfig(String name) {
1345         final int count = mDeviceConfigCombo.getItemCount();
1346         for (int i = 0 ; i < count ; i++) {
1347             String item = mDeviceConfigCombo.getItem(i);
1348             if (name.equals(item)) {
1349                 mDeviceConfigCombo.select(i);
1350                 return;
1351             }
1352         }
1353     }
1354 
1355     /**
1356      * Called when the selection of the device combo changes.
1357      * @param recomputeLayout
1358      */
onDeviceChange(boolean recomputeLayout)1359     private void onDeviceChange(boolean recomputeLayout) {
1360         // because changing the content of a combo triggers a change event, respect the
1361         // mDisableUpdates flag
1362         if (mDisableUpdates > 0) {
1363             return;
1364         }
1365 
1366         String newConfigName = null;
1367 
1368         int deviceIndex = mDeviceCombo.getSelectionIndex();
1369         if (deviceIndex != -1) {
1370             // check if the user is asking for the custom item
1371             if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
1372                 onCustomDeviceConfig();
1373                 return;
1374             }
1375 
1376             // get the previous config, so that we can look for a close match
1377             if (mState.device != null) {
1378                 int index = mDeviceConfigCombo.getSelectionIndex();
1379                 if (index != -1) {
1380                     FolderConfiguration oldConfig = mState.device.getConfigs().get(
1381                             mDeviceConfigCombo.getItem(index));
1382 
1383                     LayoutDevice newDevice = mDeviceList.get(deviceIndex);
1384 
1385                     newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
1386                 }
1387             }
1388 
1389             mState.device = mDeviceList.get(deviceIndex);
1390         } else {
1391             mState.device = null;
1392         }
1393 
1394         fillConfigCombo(newConfigName);
1395 
1396         computeCurrentConfig(false /*force*/);
1397 
1398         if (recomputeLayout) {
1399             onDeviceConfigChange();
1400         }
1401     }
1402 
1403     /**
1404      * Handles a user request for the {@link ConfigManagerDialog}.
1405      */
onCustomDeviceConfig()1406     private void onCustomDeviceConfig() {
1407         ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
1408         dialog.open();
1409 
1410         // save the user devices
1411         Sdk.getCurrent().getLayoutDeviceManager().save();
1412 
1413         // Update the UI with no triggered event
1414         mDisableUpdates++;
1415 
1416         LayoutDevice oldCurrent = mState.device;
1417 
1418         // but first, update the device combo
1419         initDevices();
1420 
1421         // attempts to reselect the current device.
1422         if (selectDevice(oldCurrent)) {
1423             // current device still exists.
1424             // reselect the config
1425             selectConfig(mState.configName);
1426 
1427             // reset the UI as if it was just a replacement file, since we can keep
1428             // the current device (and possibly config).
1429             adaptConfigSelection(false /*needBestMatch*/);
1430 
1431         } else {
1432             // find a new device/config to match the current file.
1433             findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
1434         }
1435 
1436         mDisableUpdates--;
1437 
1438         // recompute the current config
1439         computeCurrentConfig(false /*force*/);
1440 
1441         // force a redraw
1442         onDeviceChange(true /*recomputeLayout*/);
1443     }
1444 
1445     /**
1446      * Attempts to find a close config among a list
1447      * @param oldConfig the reference config.
1448      * @param configs the list of config to search through
1449      * @return the name of the closest config match, or possibly null if no configs are compatible
1450      * (this can only happen if the configs don't have a single qualifier that is the same).
1451      */
getClosestMatch(FolderConfiguration oldConfig, Map<String, FolderConfiguration> configs)1452     private String getClosestMatch(FolderConfiguration oldConfig,
1453             Map<String, FolderConfiguration> configs) {
1454 
1455         // create 2 lists as we're going to go through one and put the candidates in the other.
1456         ArrayList<Entry<String, FolderConfiguration>> list1 =
1457             new ArrayList<Entry<String,FolderConfiguration>>();
1458         ArrayList<Entry<String, FolderConfiguration>> list2 =
1459             new ArrayList<Entry<String,FolderConfiguration>>();
1460 
1461         list1.addAll(configs.entrySet());
1462 
1463         final int count = FolderConfiguration.getQualifierCount();
1464         for (int i = 0 ; i < count ; i++) {
1465             // compute the new candidate list by only taking configs that have
1466             // the same i-th qualifier as the old config
1467             for (Entry<String, FolderConfiguration> entry : list1) {
1468                 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
1469 
1470                 FolderConfiguration config = entry.getValue();
1471                 ResourceQualifier newQualifier = config.getQualifier(i);
1472 
1473                 if (oldQualifier == null) {
1474                     if (newQualifier == null) {
1475                         list2.add(entry);
1476                     }
1477                 } else if (oldQualifier.equals(newQualifier)) {
1478                     list2.add(entry);
1479                 }
1480             }
1481 
1482             // at any moment if the new candidate list contains only one match, its name
1483             // is returned.
1484             if (list2.size() == 1) {
1485                 return list2.get(0).getKey();
1486             }
1487 
1488             // if the list is empty, then all the new configs failed. It is considered ok, and
1489             // we move to the next qualifier anyway. This way, if a qualifier is different for
1490             // all new configs it is simply ignored.
1491             if (list2.size() != 0) {
1492                 // move the candidates back into list1.
1493                 list1.clear();
1494                 list1.addAll(list2);
1495                 list2.clear();
1496             }
1497         }
1498 
1499         // the only way to reach this point is if there's an exact match.
1500         // (if there are more than one, then there's a duplicate config and it doesn't matter,
1501         // we take the first one).
1502         if (list1.size() > 0) {
1503             return list1.get(0).getKey();
1504         }
1505 
1506         return null;
1507     }
1508 
1509     /**
1510      * fills the config combo with new values based on {@link #mCurrentState#device}.
1511      * @param refName an optional name. if set the selection will match this name (if found)
1512      */
fillConfigCombo(String refName)1513     private void fillConfigCombo(String refName) {
1514         mDeviceConfigCombo.removeAll();
1515 
1516         if (mState.device != null) {
1517             Set<String> configNames = mState.device.getConfigs().keySet();
1518 
1519             int selectionIndex = 0;
1520             int i = 0;
1521 
1522             for (String name : configNames) {
1523                 mDeviceConfigCombo.add(name);
1524 
1525                 if (name.equals(refName)) {
1526                     selectionIndex = i;
1527                 }
1528                 i++;
1529             }
1530 
1531             mDeviceConfigCombo.select(selectionIndex);
1532             mDeviceConfigCombo.setEnabled(configNames.size() > 1);
1533         }
1534     }
1535 
1536     /**
1537      * Called when the device config selection changes.
1538      */
onDeviceConfigChange()1539     private void onDeviceConfigChange() {
1540         // because changing the content of a combo triggers a change event, respect the
1541         // mDisableUpdates flag
1542         if (mDisableUpdates > 0) {
1543             return;
1544         }
1545 
1546         if (computeCurrentConfig(false /*force*/) && mListener != null) {
1547             mListener.onConfigurationChange();
1548         }
1549     }
1550 
1551     /**
1552      * Call back for language combo selection
1553      */
onLocaleChange()1554     private void onLocaleChange() {
1555         // because mLocaleList triggers onLanguageChange at each modification, the filling
1556         // of the combo with data will trigger notifications, and we don't want that.
1557         if (mDisableUpdates > 0) {
1558             return;
1559         }
1560 
1561         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
1562             mListener.onConfigurationChange();
1563         }
1564     }
1565 
onDockChange()1566     private void onDockChange() {
1567         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
1568             mListener.onConfigurationChange();
1569         }
1570     }
1571 
onDayChange()1572     private void onDayChange() {
1573         if (computeCurrentConfig(false /*force*/) &&  mListener != null) {
1574             mListener.onConfigurationChange();
1575         }
1576     }
1577 
1578     /**
1579      * Saves the current state and the current configuration
1580      * @param force forces saving the states even if updates are disabled
1581      *
1582      * @see #saveState(boolean)
1583      */
computeCurrentConfig(boolean force)1584     private boolean computeCurrentConfig(boolean force) {
1585         saveState(force);
1586 
1587         if (mState.device != null) {
1588             // get the device config from the device/config combos.
1589             int configIndex = mDeviceConfigCombo.getSelectionIndex();
1590             String name = mDeviceConfigCombo.getItem(configIndex);
1591             FolderConfiguration config = mState.device.getConfigs().get(name);
1592 
1593             // replace the config with the one from the device
1594             mCurrentConfig.set(config);
1595 
1596             // replace the locale qualifiers with the one coming from the locale combo
1597             int localeIndex = mLocaleCombo.getSelectionIndex();
1598             if (localeIndex != -1) {
1599                 ResourceQualifier[] localeQualifiers = mLocaleList.get(localeIndex);
1600 
1601                 mCurrentConfig.setLanguageQualifier(
1602                         (LanguageQualifier)localeQualifiers[LOCALE_LANG]);
1603                 mCurrentConfig.setRegionQualifier(
1604                         (RegionQualifier)localeQualifiers[LOCALE_REGION]);
1605             }
1606 
1607             int index = mDockCombo.getSelectionIndex();
1608             if (index == -1) {
1609                 index = 0; // no selection = 0
1610             }
1611             mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index)));
1612 
1613             index = mNightCombo.getSelectionIndex();
1614             if (index == -1) {
1615                 index = 0; // no selection = 0
1616             }
1617             mCurrentConfig.setNightModeQualifier(
1618                     new NightModeQualifier(NightMode.getByIndex(index)));
1619 
1620             // update the create button.
1621             checkCreateEnable();
1622 
1623             return true;
1624         }
1625 
1626         return false;
1627     }
1628 
onThemeChange()1629     private void onThemeChange() {
1630         saveState(false /*force*/);
1631 
1632         int themeIndex = mThemeCombo.getSelectionIndex();
1633         if (themeIndex != -1) {
1634             String theme = mThemeCombo.getItem(themeIndex);
1635 
1636             if (theme.equals(THEME_SEPARATOR)) {
1637                 mThemeCombo.select(0);
1638             }
1639 
1640             if (mListener != null) {
1641                 mListener.onThemeChange();
1642             }
1643         }
1644     }
1645 
onClippingChange()1646     private void onClippingChange() {
1647         mClipping = mClippingButton.getSelection();
1648         if (mListener != null) {
1649             mListener.onClippingChange();
1650         }
1651     }
1652 
1653     /**
1654      * Returns whether the given <var>style</var> is a theme.
1655      * This is done by making sure the parent is a theme.
1656      * @param value the style to check
1657      * @param styleMap the map of styles for the current project. Key is the style name.
1658      * @return True if the given <var>style</var> is a theme.
1659      */
isTheme(IResourceValue value, Map<String, IResourceValue> styleMap)1660     private boolean isTheme(IResourceValue value, Map<String, IResourceValue> styleMap) {
1661         if (value instanceof IStyleResourceValue) {
1662             IStyleResourceValue style = (IStyleResourceValue)value;
1663 
1664             boolean frameworkStyle = false;
1665             String parentStyle = style.getParentStyle();
1666             if (parentStyle == null) {
1667                 // if there is no specified parent style we look an implied one.
1668                 // For instance 'Theme.light' is implied child style of 'Theme',
1669                 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
1670                 String name = style.getName();
1671                 int index = name.lastIndexOf('.');
1672                 if (index != -1) {
1673                     parentStyle = name.substring(0, index);
1674                 }
1675             } else {
1676                 // remove the useless @ if it's there
1677                 if (parentStyle.startsWith("@")) {
1678                     parentStyle = parentStyle.substring(1);
1679                 }
1680 
1681                 // check for framework identifier.
1682                 if (parentStyle.startsWith("android:")) {
1683                     frameworkStyle = true;
1684                     parentStyle = parentStyle.substring("android:".length());
1685                 }
1686 
1687                 // at this point we could have the format style/<name>. we want only the name
1688                 if (parentStyle.startsWith("style/")) {
1689                     parentStyle = parentStyle.substring("style/".length());
1690                 }
1691             }
1692 
1693             if (parentStyle != null) {
1694                 if (frameworkStyle) {
1695                     // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
1696                     return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
1697                 } else {
1698                     // if it's a project style, we check this is a theme.
1699                     value = styleMap.get(parentStyle);
1700                     if (value != null) {
1701                         return isTheme(value, styleMap);
1702                     }
1703                 }
1704             }
1705         }
1706 
1707         return false;
1708     }
1709 
checkCreateEnable()1710     private void checkCreateEnable() {
1711         mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
1712     }
1713 
1714     /**
1715      * Checks whether the current edited file is the best match for a given config.
1716      * <p/>
1717      * This tests against other versions of the same layout in the project.
1718      * <p/>
1719      * The given config must be compatible with the current edited file.
1720      * @param config the config to test.
1721      * @return true if the current edited file is the best match in the project for the
1722      * given config.
1723      */
isCurrentFileBestMatchFor(FolderConfiguration config)1724     private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
1725         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
1726                 ResourceFolderType.LAYOUT, config);
1727 
1728         if (match != null) {
1729             return match.getFile().equals(mEditedFile);
1730         } else {
1731             // if we stop here that means the current file is not even a match!
1732             AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
1733         }
1734 
1735         return false;
1736     }
1737 }
1738 
1739