2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
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;
22 import com.android.ide.common.rendering.api.ResourceValue;
23 import com.android.ide.common.rendering.api.StyleResourceValue;
24 import com.android.ide.common.resources.ResourceFile;
25 import com.android.ide.common.resources.ResourceFolder;
26 import com.android.ide.common.resources.ResourceRepository;
27 import com.android.ide.common.resources.configuration.DockModeQualifier;
28 import com.android.ide.common.resources.configuration.FolderConfiguration;
29 import com.android.ide.common.resources.configuration.LanguageQualifier;
30 import com.android.ide.common.resources.configuration.NightModeQualifier;
31 import com.android.ide.common.resources.configuration.PixelDensityQualifier;
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.VersionQualifier;
38 import com.android.ide.common.sdk.LoadStatus;
39 import com.android.ide.eclipse.adt.AdtPlugin;
40 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
41 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
42 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
43 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
44 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
45 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
46 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
47 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
48 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
49 import com.android.resources.Density;
50 import com.android.resources.DockMode;
51 import com.android.resources.NightMode;
52 import com.android.resources.ResourceFolderType;
53 import com.android.resources.ResourceType;
54 import com.android.resources.ScreenOrientation;
55 import com.android.resources.ScreenSize;
56 import com.android.sdklib.AndroidVersion;
57 import com.android.sdklib.IAndroidTarget;
58 import com.android.util.Pair;
60 import org.eclipse.core.resources.IFile;
61 import org.eclipse.core.resources.IFolder;
62 import org.eclipse.core.resources.IProject;
63 import org.eclipse.core.runtime.CoreException;
64 import org.eclipse.core.runtime.IStatus;
65 import org.eclipse.core.runtime.QualifiedName;
66 import org.eclipse.draw2d.geometry.Rectangle;
67 import org.eclipse.swt.SWT;
68 import org.eclipse.swt.events.SelectionAdapter;
69 import org.eclipse.swt.events.SelectionEvent;
70 import org.eclipse.swt.layout.GridData;
71 import org.eclipse.swt.layout.GridLayout;
72 import org.eclipse.swt.widgets.Button;
73 import org.eclipse.swt.widgets.Combo;
74 import org.eclipse.swt.widgets.Composite;
75 import org.eclipse.swt.widgets.Label;
77 import java.util.ArrayList;
78 import java.util.Collections;
79 import java.util.Comparator;
80 import java.util.HashSet;
81 import java.util.List;
82 import java.util.Locale;
85 import java.util.SortedSet;
88 * A composite that displays the current configuration displayed in a Graphical Layout Editor.
90 * The composite has several entry points:<br>
91 * - {@link #setFile(IFile)}<br>
92 * Called after the constructor to set the file being edited. Nothing else is performed.<br>
94 * - {@link #onXmlModelLoaded()}<br>
95 * Called when the XML model is loaded, either the first time or when the Target/SDK changes.
96 * This initializes the UI, either with the first compatible configuration found, or attempts
97 * to restore a configuration if one is found to have been saved in the file persistent storage.
98 * (see {@link #storeState()})<br>
100 * - {@link #replaceFile(IFile)}<br>
101 * Called when a file, representing the same resource but with a different config is opened<br>
104 * - {@link #changeFileOnNewConfig(IFile)}<br>
105 * Called when config change triggers the editing of a file with a different config.
107 * Additionally, the composite can handle the following events.<br>
108 * - SDK reload. This is when the main SDK is finished loading.<br>
109 * - Target reload. This is when the target used by the project is the edited file has finished<br>
112 public class ConfigurationComposite extends Composite {
113 private final static String SEP = ":"; //$NON-NLS-1$
114 private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
117 * Setting name for project-wide setting controlling rendering target and locale which
118 * is shared for all files
120 public final static QualifiedName NAME_RENDER_STATE =
121 new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$
124 * Settings name for file-specific configuration preferences, such as which theme or
125 * device to render the current layout with
127 public final static QualifiedName NAME_CONFIG_STATE =
128 new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
130 private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
132 private final static int LOCALE_LANG = 0;
133 private final static int LOCALE_REGION = 1;
135 private Label mCurrentLayoutLabel;
136 private Button mCreateButton;
138 private Combo mDeviceCombo;
139 private Combo mDeviceConfigCombo;
140 private Combo mLocaleCombo;
141 private Combo mDockCombo;
142 private Combo mNightCombo;
143 private Combo mThemeCombo;
144 private Combo mTargetCombo;
147 * List of booleans, matching item for item the theme names in the mThemeCombo
148 * combobox, where each boolean represents whether the corresponding theme is a
151 private List<Boolean> mIsProjectTheme = new ArrayList<Boolean>(40);
153 /** updates are disabled if > 0 */
154 private int mDisableUpdates = 0;
156 private List<LayoutDevice> mDeviceList;
157 private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
159 private final ArrayList<ResourceQualifier[] > mLocaleList =
160 new ArrayList<ResourceQualifier[]>();
162 private final ConfigState mState = new ConfigState();
164 private boolean mSdkChanged = false;
165 private boolean mFirstXmlModelChange = true;
167 /** The config listener given to the constructor. Never null. */
168 private final IConfigListener mListener;
170 /** The {@link FolderConfiguration} representing the state of the UI controls */
171 private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
173 /** The file being edited */
174 private IFile mEditedFile;
175 /** The {@link ProjectResources} for the edited file's project */
176 private ProjectResources mResources;
177 /** The target of the project of the file being edited. */
178 private IAndroidTarget mProjectTarget;
179 /** The target of the project of the file being edited. */
180 private IAndroidTarget mRenderingTarget;
181 /** The {@link FolderConfiguration} being edited. */
182 private FolderConfiguration mEditedConfig;
183 /** Serialized state to use when initializing the configuration after the SDK is loaded */
184 private String mInitialState;
187 * Interface implemented by the part which owns a {@link ConfigurationComposite}.
188 * This notifies the owners when the configuration change.
189 * The owner must also provide methods to provide the configuration that will
192 public interface IConfigListener {
194 * Called when the {@link FolderConfiguration} change. The new config can be queried
195 * with {@link ConfigurationComposite#getCurrentConfig()}.
197 void onConfigurationChange();
200 * Called after a device has changed (in addition to {@link #onConfigurationChange}
203 void onDevicePostChange();
206 * Called when the current theme changes. The theme can be queried with
207 * {@link ConfigurationComposite#getTheme()}.
209 void onThemeChange();
212 * Called when the "Create" button is clicked.
217 * Called before the rendering target changes.
218 * @param oldTarget the old rendering target
220 void onRenderingTargetPreChange(IAndroidTarget oldTarget);
223 * Called after the rendering target changes.
225 * @param target the new rendering target
227 void onRenderingTargetPostChange(IAndroidTarget target);
229 ResourceRepository getProjectResources();
230 ResourceRepository getFrameworkResources();
231 ResourceRepository getFrameworkResources(IAndroidTarget target);
232 Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
233 Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
234 String getIncludedWithin();
238 * State of the current config. This is used during UI reset to attempt to return the
239 * rendering to its original configuration.
241 private class ConfigState {
244 ResourceQualifier[] locale;
246 /** dock mode. Guaranteed to be non null */
247 DockMode dock = DockMode.NONE;
248 /** night mode. Guaranteed to be non null */
249 NightMode night = NightMode.NOTNIGHT;
250 /** the version being targeted for rendering */
251 IAndroidTarget target;
254 StringBuilder sb = new StringBuilder();
255 if (device != null) {
256 sb.append(device.getName());
258 sb.append(configName);
260 if (isLocaleSpecificLayout() && locale != null) {
261 if (locale[0] != null && locale[1] != null) {
262 // locale[0]/[1] can be null sometimes when starting Eclipse
263 sb.append(((LanguageQualifier) locale[0]).getValue());
264 sb.append(SEP_LOCALE);
265 sb.append(((RegionQualifier) locale[1]).getValue());
271 sb.append(dock.getResourceValue());
273 sb.append(night.getResourceValue());
276 // We used to store the render target here in R9. Leave a marker
277 // to ensure that we don't reuse this slot; add new extra fields after it.
281 return sb.toString();
284 boolean setData(String data) {
285 String[] values = data.split(SEP);
286 if (values.length == 6 || values.length == 7) {
287 for (LayoutDevice d : mDeviceList) {
288 if (d.getName().equals(values[0])) {
290 FolderConfiguration config = device.getFolderConfigByName(values[1]);
291 if (config != null) {
292 configName = values[1];
294 // Load locale. Note that this can get overwritten by the
295 // project-wide settings read below.
296 locale = new ResourceQualifier[2];
297 String locales[] = values[2].split(SEP_LOCALE);
298 if (locales.length >= 2) {
299 if (locales[0].length() > 0) {
300 locale[0] = new LanguageQualifier(locales[0]);
302 if (locales[1].length() > 0) {
303 locale[1] = new RegionQualifier(locales[1]);
308 dock = DockMode.getEnum(values[4]);
310 dock = DockMode.NONE;
312 night = NightMode.getEnum(values[5]);
314 night = NightMode.NOTNIGHT;
317 // element 7/values[6]: used to store render target in R9.
318 // No longer stored here. If adding more data, make
319 // sure you leave 7 alone.
321 Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
323 // We only use the "global" setting
324 if (!isLocaleSpecificLayout()) {
325 locale = pair.getFirst();
327 target = pair.getSecond();
339 public String toString() {
345 * Returns a String id to represent an {@link IAndroidTarget} which can be translated
346 * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id
347 * will never contain the {@link #SEP} character.
349 * @param target the target to return an id for
350 * @return an id for the given target; never null
352 private String targetToString(IAndroidTarget target) {
353 return target.getFullName().replace(SEP, ""); //$NON-NLS-1$
357 * Returns an {@link IAndroidTarget} that corresponds to the given id that was
358 * originally returned by {@link #targetToString}. May be null, if the platform is no
359 * longer available, or if the platform list has not yet been initialized.
361 * @param id the id that corresponds to the desired platform
362 * @return an {@link IAndroidTarget} that matches the given id, or null
364 private IAndroidTarget stringToTarget(String id) {
365 if (mTargetList != null && mTargetList.size() > 0) {
366 for (IAndroidTarget target : mTargetList) {
367 if (id.equals(targetToString(target))) {
377 * Creates a new {@link ConfigurationComposite} and adds it to the parent.
379 * The method also receives custom buttons to set into the configuration composite. The list
380 * is organized as an array of arrays. Each array represents a group of buttons thematically
383 * @param listener An {@link IConfigListener} that gets and sets configuration properties.
384 * Mandatory, cannot be null.
385 * @param parent The parent composite.
386 * @param style The style of this composite.
387 * @param initialState The initial state (serialized form) to use for the configuration
389 public ConfigurationComposite(IConfigListener listener,
390 Composite parent, int style, String initialState) {
391 super(parent, style);
392 mListener = listener;
393 mInitialState = initialState;
397 int cols = 7; // device+config+dock+day+separator*2+theme
399 // ---- First line: editing config display, locale, theme, create-button
400 Composite labelParent = new Composite(this, SWT.NONE);
401 labelParent.setLayout(gl = new GridLayout(5, false));
402 gl.marginWidth = gl.marginHeight = 0;
404 labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
405 gd.horizontalSpan = cols;
407 new Label(labelParent, SWT.NONE).setText("Editing config:");
408 mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
409 mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
412 mLocaleCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
413 mLocaleCombo.addSelectionListener(new SelectionAdapter() {
415 public void widgetSelected(SelectionEvent e) {
420 // Layout bug workaround. Without this, in -some- scenarios the Locale combo box was
421 // coming up tiny. Setting a minimumWidth hint does not work either. We need to have
422 // 2 or more items in the locale combo box when the layout is first run. These items
423 // are removed as part of the locale initialization when the SDK is loaded.
424 mLocaleCombo.add("Locale"); //$NON-NLS-1$ // Dummy place holders
425 mLocaleCombo.add("Locale"); //$NON-NLS-1$
427 mTargetCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
428 mTargetCombo.add("Android AOSP"); //$NON-NLS-1$ // Dummy place holders
429 mTargetCombo.add("Android AOSP"); //$NON-NLS-1$
430 mTargetCombo.addSelectionListener(new SelectionAdapter() {
432 public void widgetSelected(SelectionEvent e) {
433 onRenderingTargetChange();
437 mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT);
438 mCreateButton.setText("Create...");
439 mCreateButton.setEnabled(false);
440 mCreateButton.addSelectionListener(new SelectionAdapter() {
442 public void widgetSelected(SelectionEvent e) {
443 if (mListener != null) {
444 mListener.onCreate();
449 // ---- 2nd line: device/config/locale/theme Combos, create button.
451 setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
452 setLayout(gl = new GridLayout(cols, false));
454 gl.horizontalSpacing = 0;
456 mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
457 mDeviceCombo.setLayoutData(new GridData(
458 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
459 mDeviceCombo.addSelectionListener(new SelectionAdapter() {
461 public void widgetSelected(SelectionEvent e) {
462 onDeviceChange(true /* recomputeLayout*/);
466 mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
467 mDeviceConfigCombo.setLayoutData(new GridData(
468 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
469 mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
471 public void widgetSelected(SelectionEvent e) {
472 onDeviceConfigChange();
477 Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
478 separator.setLayoutData(gd = new GridData(
479 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
482 mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
483 mDockCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
484 | GridData.GRAB_HORIZONTAL));
485 for (DockMode mode : DockMode.values()) {
486 mDockCombo.add(mode.getLongDisplayValue());
488 mDockCombo.addSelectionListener(new SelectionAdapter() {
490 public void widgetSelected(SelectionEvent e) {
495 mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
496 mNightCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
497 | GridData.GRAB_HORIZONTAL));
498 for (NightMode mode : NightMode.values()) {
499 mNightCombo.add(mode.getLongDisplayValue());
501 mNightCombo.addSelectionListener(new SelectionAdapter() {
503 public void widgetSelected(SelectionEvent e) {
508 mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
509 mThemeCombo.setLayoutData(new GridData(
510 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
511 mThemeCombo.setEnabled(false);
513 mThemeCombo.addSelectionListener(new SelectionAdapter() {
515 public void widgetSelected(SelectionEvent e) {
521 // ---- Init and reset/reload methods ----
524 * Sets the reference to the file being edited.
525 * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
526 * loaded (or reloaded as the SDK/target changes).
528 * @param file the file being opened
530 * @see #onXmlModelLoaded()
531 * @see #replaceFile(IFile)
532 * @see #changeFileOnNewConfig(IFile)
534 public void setFile(IFile file) {
539 * Replaces the UI with a given file configuration. This is meant to answer the user
540 * explicitly opening a different version of the same layout from the Package Explorer.
541 * <p/>This attempts to keep the current config, but may change it if it's not compatible or
543 * <p/>This will NOT trigger a redraw event (will not call
544 * {@link IConfigListener#onConfigurationChange()}.)
545 * @param file the file being opened.
547 public void replaceFile(IFile file) {
548 // if there is no previous selection, revert to default mode.
549 if (mState.device == null) {
550 setFile(file); // onTargetChanged will be called later.
555 IProject iProject = mEditedFile.getProject();
556 mResources = ResourceManager.getInstance().getProjectResources(iProject);
558 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
559 mEditedConfig = resFolder.getConfiguration();
561 mDisableUpdates++; // we do not want to trigger onXXXChange when setting
562 // new values in the widgets.
565 // only attempt to do anything if the SDK and targets are loaded.
566 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
567 if (sdkStatus == LoadStatus.LOADED) {
568 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
571 if (targetStatus == LoadStatus.LOADED) {
573 // update the current config selection to make sure it's
574 // compatible with the new file
575 adaptConfigSelection(true /*needBestMatch*/);
577 // compute the final current config
578 computeCurrentConfig();
580 // update the string showing the config value
581 updateConfigDisplay(mEditedConfig);
590 * Updates the UI with a new file that was opened in response to a config change.
591 * @param file the file being opened.
593 * @see #replaceFile(IFile)
595 public void changeFileOnNewConfig(IFile file) {
597 IProject iProject = mEditedFile.getProject();
598 mResources = ResourceManager.getInstance().getProjectResources(iProject);
600 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
601 mEditedConfig = resFolder.getConfiguration();
603 // All that's needed is to update the string showing the config value
604 // (since the config combo were chosen by the user).
605 updateConfigDisplay(mEditedConfig);
609 * Responds to the event that the basic SDK information finished loading.
610 * @param target the possibly new target object associated with the file being edited (in case
611 * the SDK path was changed).
613 public void onSdkLoaded(IAndroidTarget target) {
614 // a change to the SDK means that we need to check for new/removed devices.
617 // store the new target.
618 mProjectTarget = target;
620 mDisableUpdates++; // we do not want to trigger onXXXChange when setting
621 // new values in the widgets.
623 // this is going to be followed by a call to onTargetLoaded.
624 // So we can only care about the layout devices in this case.
633 * Answers to the XML model being loaded, either the first time or when the Target/SDK changes.
634 * <p>This initializes the UI, either with the first compatible configuration found,
635 * or attempts to restore a configuration if one is found to have been saved in the file
636 * persistent storage.
637 * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called
638 * back when those are loaded).
639 * <p>The method automatically handles being called the first time after editor creation, or
640 * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
641 * is properly called).
644 * @see #onSdkLoaded(IAndroidTarget)
646 public AndroidTargetData onXmlModelLoaded() {
647 AndroidTargetData targetData = null;
649 // only attempt to do anything if the SDK and targets are loaded.
650 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
651 if (sdkStatus == LoadStatus.LOADED) {
652 mDisableUpdates++; // we do not want to trigger onXXXChange when setting
655 // init the devices if needed (new SDK or first time going through here)
656 if (mSdkChanged || mFirstXmlModelChange) {
661 IProject iProject = mEditedFile.getProject();
663 Sdk currentSdk = Sdk.getCurrent();
664 if (currentSdk != null) {
665 mProjectTarget = currentSdk.getTarget(iProject);
668 LoadStatus targetStatus = LoadStatus.FAILED;
669 if (mProjectTarget != null) {
670 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
674 if (targetStatus == LoadStatus.LOADED) {
675 if (mResources == null) {
676 mResources = ResourceManager.getInstance().getProjectResources(iProject);
678 if (mEditedConfig == null) {
679 ResourceFolder resFolder = mResources.getResourceFolder(
680 (IFolder) mEditedFile.getParent());
681 mEditedConfig = resFolder.getConfiguration();
684 targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
686 // get the file stored state
687 boolean loadedConfigData = false;
688 String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE);
689 if (mInitialState != null) {
690 data = mInitialState;
691 mInitialState = null;
694 loadedConfigData = mState.setData(data);
699 // If the current state was loaded from the persistent storage, we update the
700 // UI with it and then try to adapt it (which will handle incompatible
702 // Otherwise, just look for the first compatible configuration.
703 if (loadedConfigData) {
704 // first make sure we have the config to adapt
705 selectDevice(mState.device);
706 fillConfigCombo(mState.configName);
708 adaptConfigSelection(false /*needBestMatch*/);
710 mDockCombo.select(DockMode.getIndex(mState.dock));
711 mNightCombo.select(NightMode.getIndex(mState.night));
712 mTargetCombo.select(mTargetList.indexOf(mState.target));
714 targetData = Sdk.getCurrent().getTargetData(mState.target);
716 findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
718 // Default to modern layout lib
719 IAndroidTarget target = findDefaultRenderTarget();
720 if (target != null) {
721 targetData = Sdk.getCurrent().getTargetData(target);
722 mTargetCombo.select(mTargetList.indexOf(target));
726 // Update themes. This is done after updating the devices above,
727 // since we want to look at the chosen device size to decide
728 // what the default theme (for example, with Honeycomb we choose
729 // Holo as the default theme but only if the screen size is XLARGE
730 // (and of course only if the manifest does not specify another
734 // update the string showing the config value
735 updateConfigDisplay(mEditedConfig);
737 // compute the final current config
738 computeCurrentConfig();
742 mFirstXmlModelChange = false;
749 /** Return the default render target to use, or null if no strong preference */
750 private IAndroidTarget findDefaultRenderTarget() {
751 // Default to layoutlib version 5
752 Sdk current = Sdk.getCurrent();
753 if (current != null) {
754 for (IAndroidTarget target : current.getTargets()) {
755 // Only Honeycomb has layoutlib version 5; as soon as we backport
756 // adjust this algorithm to find the lowest version that contains
758 AndroidVersion version = target.getVersion();
759 int apiLevel = version.getApiLevel();
760 if (apiLevel >= 11) { // Layoutlib so far has been backported to 11
769 private static class ConfigBundle {
770 FolderConfiguration config;
776 config = new FolderConfiguration();
782 ConfigBundle(ConfigBundle bundle) {
783 config = new FolderConfiguration();
784 config.set(bundle.config);
785 localeIndex = bundle.localeIndex;
786 dockModeIndex = bundle.dockModeIndex;
787 nightModeIndex = bundle.nightModeIndex;
791 private static class ConfigMatch {
792 final FolderConfiguration testConfig;
793 final LayoutDevice device;
795 final ConfigBundle bundle;
797 public ConfigMatch(FolderConfiguration testConfig,
798 LayoutDevice device, String name, ConfigBundle bundle) {
799 this.testConfig = testConfig;
800 this.device = device;
802 this.bundle = bundle;
806 public String toString() {
807 return device.getName() + " - " + name;
812 * Finds a device/config that can display {@link #mEditedConfig}.
813 * <p/>Once found the device and config combos are set to the config.
814 * <p/>If there is no compatible configuration, a custom one is created.
815 * @param favorCurrentConfig if true, and no best match is found, don't change
816 * the current config. This must only be true if the current config is compatible.
818 private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
819 // list of compatible device/config/locale
820 List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
822 // list of actual best match (ie the file is a best match for the device/config)
823 List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
825 // get a locale that match the host locale roughly (may not be exact match on the region.)
826 int localeHostMatch = getLocaleMatch();
828 // build a list of combinations of non standard qualifiers to add to each device's
829 // qualifier set when testing for a match.
830 // These qualifiers are: locale, night-mode, car dock.
831 List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
833 // If the edited file has locales, then we have to select a matching locale from
835 // However, if it doesn't, we don't randomly take the first locale, we take one
836 // matching the current host locale (making sure it actually exist in the project)
838 if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) {
839 // add all the locales
841 max = mLocaleList.size();
843 // only add the locale host match
844 start = localeHostMatch;
845 max = localeHostMatch + 1; // test is <
848 for (int i = start ; i < max ; i++) {
849 ResourceQualifier[] l = mLocaleList.get(i);
851 ConfigBundle bundle = new ConfigBundle();
852 bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]);
853 bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]);
855 bundle.localeIndex = i;
856 configBundles.add(bundle);
859 // add the dock mode to the bundle combinations.
860 addDockModeToBundles(configBundles);
862 // add the night mode to the bundle combinations.
863 addNightModeToBundles(configBundles);
865 for (LayoutDevice device : mDeviceList) {
866 for (DeviceConfig config : device.getConfigs()) {
868 // loop on the list of config bundles to create full configurations.
869 for (ConfigBundle bundle : configBundles) {
870 // create a new config with device config
871 FolderConfiguration testConfig = new FolderConfiguration();
872 testConfig.set(config.getConfig());
874 // add on top of it, the extra qualifiers from the bundle
875 testConfig.add(bundle.config);
877 if (mEditedConfig.isMatchFor(testConfig)) {
878 // this is a basic match. record it in case we don't find a match
879 // where the edited file is a best config.
880 anyMatches.add(new ConfigMatch(testConfig, device, config.getName(),
883 if (isCurrentFileBestMatchFor(testConfig)) {
884 // this is what we want.
885 bestMatches.add(new ConfigMatch(testConfig, device, config.getName(),
893 if (bestMatches.size() == 0) {
894 if (favorCurrentConfig) {
896 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
897 AdtPlugin.log(IStatus.ERROR,
898 "favorCurrentConfig can only be true if the current config is compatible");
901 // just display the warning
902 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
904 "'%1$s' is not a best match for any device/locale combination.",
905 mEditedConfig.toDisplayString()),
907 "Displaying it with '%1$s'",
908 mCurrentConfig.toDisplayString()));
909 } else if (anyMatches.size() > 0) {
910 // select the best device anyway.
911 ConfigMatch match = selectConfigMatch(anyMatches);
912 selectDevice(mState.device = match.device);
913 fillConfigCombo(match.name);
914 mLocaleCombo.select(match.bundle.localeIndex);
915 mDockCombo.select(match.bundle.dockModeIndex);
916 mNightCombo.select(match.bundle.nightModeIndex);
918 // TODO: display a better warning!
919 computeCurrentConfig();
920 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
922 "'%1$s' is not a best match for any device/locale combination.",
923 mEditedConfig.toDisplayString()),
925 "Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.",
926 mCurrentConfig.toDisplayString()));
929 // TODO: there is no device/config able to display the layout, create one.
930 // For the base config values, we'll take the first device and config,
931 // and replace whatever qualifier required by the layout file.
934 ConfigMatch match = selectConfigMatch(bestMatches);
935 selectDevice(mState.device = match.device);
936 fillConfigCombo(match.name);
937 mLocaleCombo.select(match.bundle.localeIndex);
938 mDockCombo.select(match.bundle.dockModeIndex);
939 mNightCombo.select(match.bundle.nightModeIndex);
944 * Note: this comparator imposes orderings that are inconsistent with equals.
946 private static class TabletConfigComparator implements Comparator<ConfigMatch> {
947 public int compare(ConfigMatch o1, ConfigMatch o2) {
948 ScreenSize ss1 = o1.testConfig.getScreenSizeQualifier().getValue();
949 ScreenSize ss2 = o2.testConfig.getScreenSizeQualifier().getValue();
951 // X-LARGE is better than all others (which are considered identical)
952 // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
954 if (ss1 == ScreenSize.XLARGE) {
955 if (ss2 == ScreenSize.XLARGE) {
956 ScreenOrientation so1 =
957 o1.testConfig.getScreenOrientationQualifier().getValue();
958 ScreenOrientation so2 =
959 o2.testConfig.getScreenOrientationQualifier().getValue();
961 if (so1 == ScreenOrientation.LANDSCAPE) {
962 if (so2 == ScreenOrientation.LANDSCAPE) {
967 } else if (so2 == ScreenOrientation.LANDSCAPE) {
975 } else if (ss2 == ScreenSize.XLARGE) {
984 * Note: this comparator imposes orderings that are inconsistent with equals.
986 private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
987 public int compare(ConfigMatch o1, ConfigMatch o2) {
988 int dpi1 = Density.DEFAULT_DENSITY;
989 if (o1.testConfig.getPixelDensityQualifier() != null) {
990 dpi1 = o1.testConfig.getPixelDensityQualifier().getValue().getDpiValue();
993 int dpi2 = Density.DEFAULT_DENSITY;
994 if (o2.testConfig.getPixelDensityQualifier() != null) {
995 dpi2 = o2.testConfig.getPixelDensityQualifier().getValue().getDpiValue();
999 // portrait is better
1000 ScreenOrientation so1 =
1001 o1.testConfig.getScreenOrientationQualifier().getValue();
1002 ScreenOrientation so2 =
1003 o2.testConfig.getScreenOrientationQualifier().getValue();
1005 if (so1 == ScreenOrientation.PORTRAIT) {
1006 if (so2 == ScreenOrientation.PORTRAIT) {
1011 } else if (so2 == ScreenOrientation.PORTRAIT) {
1022 private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
1023 // API 11+ : look for a x-large device.
1024 if (mProjectTarget.getVersion().getApiLevel() >= 11) {
1025 // TODO: Revisit this once phones can run higher APIs.
1026 // Maybe check the compatible-screen tag in the manifest to figure out what kind of
1027 // device should be used for display.
1028 Collections.sort(matches, new TabletConfigComparator());
1030 // lets look for a high density device
1031 Collections.sort(matches, new PhoneConfigComparator());
1034 // the list has been sorted so that the first item is the best config
1035 return matches.get(0);
1038 private void addDockModeToBundles(List<ConfigBundle> addConfig) {
1039 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1041 // loop on each item and for each, add all variations of the dock modes
1042 for (ConfigBundle bundle : addConfig) {
1044 for (DockMode mode : DockMode.values()) {
1045 ConfigBundle b = new ConfigBundle(bundle);
1046 b.config.setDockModeQualifier(new DockModeQualifier(mode));
1047 b.dockModeIndex = index++;
1053 addConfig.addAll(list);
1056 private void addNightModeToBundles(List<ConfigBundle> addConfig) {
1057 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
1059 // loop on each item and for each, add all variations of the night modes
1060 for (ConfigBundle bundle : addConfig) {
1062 for (NightMode mode : NightMode.values()) {
1063 ConfigBundle b = new ConfigBundle(bundle);
1064 b.config.setNightModeQualifier(new NightModeQualifier(mode));
1065 b.nightModeIndex = index++;
1071 addConfig.addAll(list);
1075 * Adapts the current device/config selection so that it's compatible with
1076 * {@link #mEditedConfig}.
1077 * <p/>If the current selection is compatible, nothing is changed.
1078 * <p/>If it's not compatible, configs from the current devices are tested.
1079 * <p/>If none are compatible, it reverts to
1080 * {@link #findAndSetCompatibleConfig(FolderConfiguration)}
1082 private void adaptConfigSelection(boolean needBestMatch) {
1083 // check the device config (ie sans locale)
1084 boolean needConfigChange = true; // if still true, we need to find another config.
1085 boolean currentConfigIsCompatible = false;
1086 int configIndex = mDeviceConfigCombo.getSelectionIndex();
1087 if (configIndex != -1) {
1088 String configName = mDeviceConfigCombo.getItem(configIndex);
1089 FolderConfiguration currentConfig = mState.device.getFolderConfigByName(configName);
1090 if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) {
1091 currentConfigIsCompatible = true; // current config is compatible
1092 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
1093 needConfigChange = false;
1098 if (needConfigChange) {
1099 // if the current config/locale isn't a correct match, then
1100 // look for another config/locale in the same device.
1101 FolderConfiguration testConfig = new FolderConfiguration();
1103 // first look in the current device.
1104 String matchName = null;
1105 int localeIndex = -1;
1106 mainloop: for (DeviceConfig config : mState.device.getConfigs()) {
1107 testConfig.set(config.getConfig());
1109 // loop on the locales.
1110 for (int i = 0 ; i < mLocaleList.size() ; i++) {
1111 ResourceQualifier[] locale = mLocaleList.get(i);
1113 // update the test config with the locale qualifiers
1114 testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
1115 testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
1117 if (mEditedConfig.isMatchFor(testConfig) &&
1118 isCurrentFileBestMatchFor(testConfig)) {
1119 matchName = config.getName();
1126 if (matchName != null) {
1127 selectConfig(matchName);
1128 mLocaleCombo.select(localeIndex);
1130 // no match in current device with any config/locale
1131 // attempt to find another device that can display this particular config.
1132 findAndSetCompatibleConfig(currentConfigIsCompatible);
1138 * Finds a locale matching the config from a file.
1139 * @param language the language qualifier or null if none is set.
1140 * @param region the region qualifier or null if none is set.
1141 * @return true if there was a change in the combobox as a result of applying the locale
1143 private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
1144 boolean changed = false;
1146 // find the locale match. Since the locale list is based on the content of the
1147 // project resources there must be an exact match.
1148 // The only trick is that the region could be null in the fileConfig but in our
1149 // list of locales, this is represented as a RegionQualifier with value of
1150 // FAKE_LOCALE_VALUE.
1151 final int count = mLocaleList.size();
1152 for (int i = 0 ; i < count ; i++) {
1153 ResourceQualifier[] locale = mLocaleList.get(i);
1155 // the language qualifier in the locale list is never null.
1156 if (locale[LOCALE_LANG].equals(language)) {
1157 // region comparison is more complex, as the region could be null.
1158 if (region == null) {
1159 if (RegionQualifier.FAKE_REGION_VALUE.equals(
1160 ((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
1162 if (mLocaleCombo.getSelectionIndex() != i) {
1163 mLocaleCombo.select(i);
1168 } else if (region.equals(locale[LOCALE_REGION])) {
1170 if (mLocaleCombo.getSelectionIndex() != i) {
1171 mLocaleCombo.select(i);
1182 private void updateConfigDisplay(FolderConfiguration fileConfig) {
1183 String current = fileConfig.toDisplayString();
1184 String layoutLabel = current != null ? current : "(Default)";
1185 mCurrentLayoutLabel.setText(layoutLabel);
1186 mCurrentLayoutLabel.setToolTipText(layoutLabel);
1189 private void saveState() {
1190 if (mDisableUpdates == 0) {
1191 int index = mDeviceConfigCombo.getSelectionIndex();
1193 mState.configName = mDeviceConfigCombo.getItem(index);
1195 mState.configName = null;
1198 // since the locales are relative to the project, only keeping the index is enough
1199 index = mLocaleCombo.getSelectionIndex();
1201 mState.locale = mLocaleList.get(index);
1203 mState.locale = null;
1206 index = mThemeCombo.getSelectionIndex();
1208 mState.theme = mThemeCombo.getItem(index);
1211 index = mDockCombo.getSelectionIndex();
1213 mState.dock = DockMode.getByIndex(index);
1216 index = mNightCombo.getSelectionIndex();
1218 mState.night = NightMode.getByIndex(index);
1221 index = mTargetCombo.getSelectionIndex();
1223 mState.target = mTargetList.get(index);
1229 * Stores the current config selection into the edited file.
1231 public void storeState() {
1232 AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData());
1236 * Updates the locale combo.
1237 * This must be called from the UI thread.
1239 public void updateLocales() {
1240 if (mListener == null) {
1241 return; // can't do anything w/o it.
1248 mLocaleCombo.removeAll();
1249 mLocaleList.clear();
1251 SortedSet<String> languages = null;
1252 boolean hasLocale = false;
1254 // get the languages from the project.
1255 ResourceRepository projectRes = mListener.getProjectResources();
1257 // in cases where the opened file is not linked to a project, this could be null.
1258 if (projectRes != null) {
1259 // now get the languages from the project.
1260 languages = projectRes.getLanguages();
1262 for (String language : languages) {
1265 LanguageQualifier langQual = new LanguageQualifier(language);
1267 // find the matching regions and add them
1268 SortedSet<String> regions = projectRes.getRegions(language);
1269 for (String region : regions) {
1271 String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$
1272 RegionQualifier regionQual = new RegionQualifier(region);
1273 mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
1276 // now the entry for the other regions the language alone
1277 if (regions.size() > 0) {
1278 mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$
1280 mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$
1282 // create a region qualifier that will never be matched by qualified resources.
1283 mLocaleList.add(new ResourceQualifier[] {
1285 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1290 // add a locale not present in the project resources. This will let the dev
1291 // tests his/her default values.
1293 mLocaleCombo.add("Other");
1295 mLocaleCombo.add("Any locale");
1298 // create language/region qualifier that will never be matched by qualified resources.
1299 mLocaleList.add(new ResourceQualifier[] {
1300 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
1301 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
1304 if (mState.locale != null) {
1305 // FIXME: this may fails if the layout was deleted (and was the last one to have
1306 // that local. (we have other problem in this case though)
1307 setLocaleCombo(mState.locale[LOCALE_LANG],
1308 mState.locale[LOCALE_REGION]);
1310 mLocaleCombo.select(0);
1313 mThemeCombo.getParent().layout();
1319 private int getLocaleMatch() {
1320 Locale locale = Locale.getDefault();
1321 if (locale != null) {
1322 String currentLanguage = locale.getLanguage();
1323 String currentRegion = locale.getCountry();
1325 final int count = mLocaleList.size();
1326 for (int l = 0 ; l < count ; l++) {
1327 ResourceQualifier[] localeArray = mLocaleList.get(l);
1328 LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG];
1329 RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION];
1331 // there's always a ##/Other or ##/Any (which is the same, the region
1332 // contains FAKE_REGION_VALUE). If we don't find a perfect region match
1333 // we take the fake region. Since it's last in the list, this makes the
1335 if (langQ.getValue().equals(currentLanguage) &&
1336 (regionQ.getValue().equals(currentRegion) ||
1337 regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) {
1342 // if no locale match the current local locale, it's likely that it is
1343 // the default one which is the last one.
1351 * Updates the theme combo.
1352 * This must be called from the UI thread.
1354 private void updateThemes() {
1355 if (mListener == null) {
1356 return; // can't do anything w/o it.
1359 ResourceRepository frameworkRes = mListener.getFrameworkResources(getRenderingTarget());
1365 mThemeCombo.removeAll();
1366 mIsProjectTheme.clear();
1368 ArrayList<String> themes = new ArrayList<String>();
1369 String includedIn = mListener != null ? mListener.getIncludedWithin() : null;
1371 // First list any themes that are declared by the manifest
1372 if (mEditedFile != null) {
1373 IProject project = mEditedFile.getProject();
1374 ManifestInfo manifest = ManifestInfo.get(project);
1376 // Look up the screen size for the current configuration
1377 ScreenSize screenSize = null;
1378 if (mState.device != null) {
1379 List<DeviceConfig> configs = mState.device.getConfigs();
1380 for (DeviceConfig config : configs) {
1381 ScreenSizeQualifier qualifier =
1382 config.getConfig().getScreenSizeQualifier();
1383 screenSize = qualifier.getValue();
1387 // Look up the default/fallback theme to use for this project (which
1388 // depends on the screen size when no particular theme is specified
1390 String defaultTheme = manifest.getDefaultTheme(screenSize);
1392 Map<String, String> activityThemes = manifest.getActivityThemes();
1393 String pkg = manifest.getPackage();
1394 String preferred = null;
1395 boolean isIncluded = includedIn != null;
1396 if (mState.theme == null || isIncluded) {
1397 String layoutName = ResourceHelper.getLayoutName(mEditedFile);
1399 // If we are rendering a layout in included context, pick the theme
1400 // from the outer layout instead
1401 if (includedIn != null) {
1402 layoutName = includedIn;
1405 String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
1406 if (activity != null) {
1407 preferred = activityThemes.get(activity);
1409 if (preferred == null) {
1410 preferred = defaultTheme;
1412 String preferredTheme = ResourceHelper.styleToTheme(preferred);
1413 if (includedIn == null) {
1414 mState.theme = preferredTheme;
1416 boolean isProjectTheme = !preferred.startsWith(PREFIX_ANDROID_STYLE);
1417 mThemeCombo.add(preferredTheme);
1418 mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1420 mThemeCombo.add(THEME_SEPARATOR);
1421 mIsProjectTheme.add(Boolean.FALSE);
1424 // Create a sorted list of unique themes referenced in the manifest
1425 // (sort alphabetically, but place the preferred theme at the
1427 Set<String> themeSet = new HashSet<String>(activityThemes.values());
1428 themeSet.add(defaultTheme);
1429 List<String> themeList = new ArrayList<String>(themeSet);
1430 final String first = preferred;
1431 Collections.sort(themeList, new Comparator<String>() {
1432 public int compare(String s1, String s2) {
1435 } else if (s1 == first) {
1438 return s1.compareTo(s2);
1443 if (themeList.size() > 1 ||
1444 (themeList.size() == 1 && (preferred == null ||
1445 !preferred.equals(themeList.get(0))))) {
1446 for (String style : themeList) {
1447 String theme = ResourceHelper.styleToTheme(style);
1449 // Initialize the chosen theme to the first item
1450 // in the used theme list (that's what would be chosen
1451 // anyway) such that we stop attempting to look up
1452 // the associated activity (during initialization,
1453 // this method can be called repeatedly.)
1454 if (mState.theme == null) {
1455 mState.theme = theme;
1458 boolean isProjectTheme = !style.startsWith(PREFIX_ANDROID_STYLE);
1459 mThemeCombo.add(theme);
1460 mIsProjectTheme.add(Boolean.valueOf(isProjectTheme));
1462 mThemeCombo.add(THEME_SEPARATOR);
1463 mIsProjectTheme.add(Boolean.FALSE);
1467 // get the themes, and languages from the Framework.
1468 int platformThemeCount = 0;
1469 if (frameworkRes != null) {
1470 // get the configured resources for the framework
1471 Map<ResourceType, Map<String, ResourceValue>> frameworResources =
1472 frameworkRes.getConfiguredResources(getCurrentConfig());
1474 if (frameworResources != null) {
1476 Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
1479 // collect the themes out of all the styles.
1480 for (ResourceValue value : styles.values()) {
1481 String name = value.getName();
1482 if (name.startsWith("Theme.") || name.equals("Theme")) {
1483 themes.add(value.getName());
1487 // sort them and add them to the combo
1488 Collections.sort(themes);
1490 for (String theme : themes) {
1491 mThemeCombo.add(theme);
1492 mIsProjectTheme.add(Boolean.FALSE);
1495 platformThemeCount = themes.size();
1500 // now get the themes and languages from the project.
1501 ResourceRepository projectRes = mListener.getProjectResources();
1502 // in cases where the opened file is not linked to a project, this could be null.
1503 if (projectRes != null) {
1504 // get the configured resources for the project
1505 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
1506 mListener.getConfiguredProjectResources();
1508 if (configuredProjectRes != null) {
1510 Map<String, ResourceValue> styleMap = configuredProjectRes.get(
1511 ResourceType.STYLE);
1513 if (styleMap != null) {
1514 // collect the themes out of all the styles, ie styles that extend,
1515 // directly or indirectly a platform theme.
1516 for (ResourceValue value : styleMap.values()) {
1517 if (isTheme(value, styleMap)) {
1518 themes.add(value.getName());
1522 // sort them and add them the to the combo.
1523 if (platformThemeCount > 0 && themes.size() > 0) {
1524 mThemeCombo.add(THEME_SEPARATOR);
1525 mIsProjectTheme.add(Boolean.FALSE);
1528 Collections.sort(themes);
1530 for (String theme : themes) {
1531 mThemeCombo.add(theme);
1532 mIsProjectTheme.add(Boolean.TRUE);
1538 // try to reselect the previous theme.
1539 boolean needDefaultSelection = true;
1541 if (mState.theme != null && includedIn == null) {
1542 final int count = mThemeCombo.getItemCount();
1543 for (int i = 0 ; i < count ; i++) {
1544 if (mState.theme.equals(mThemeCombo.getItem(i))) {
1545 mThemeCombo.select(i);
1546 needDefaultSelection = false;
1547 mThemeCombo.setEnabled(true);
1553 if (needDefaultSelection) {
1554 if (mThemeCombo.getItemCount() > 0) {
1555 mThemeCombo.select(0);
1556 mThemeCombo.setEnabled(true);
1558 mThemeCombo.setEnabled(false);
1562 mThemeCombo.getParent().layout();
1567 assert mIsProjectTheme.size() == mThemeCombo.getItemCount();
1570 // ---- getters for the config selection values ----
1572 public FolderConfiguration getEditedConfig() {
1573 return mEditedConfig;
1576 public FolderConfiguration getCurrentConfig() {
1577 return mCurrentConfig;
1580 public void getCurrentConfig(FolderConfiguration config) {
1581 config.set(mCurrentConfig);
1585 * Returns the currently selected {@link Density}. This is guaranteed to be non null.
1587 public Density getDensity() {
1588 if (mCurrentConfig != null) {
1589 PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier();
1591 // just a sanity check
1592 Density d = qual.getValue();
1593 if (d != Density.NODPI) {
1599 // no config? return medium as the default density.
1600 return Density.MEDIUM;
1604 * Returns the current device xdpi.
1606 public float getXDpi() {
1607 if (mState.device != null) {
1608 float dpi = mState.device.getXDpi();
1609 if (Float.isNaN(dpi) == false) {
1614 // get the pixel density as the density.
1615 return getDensity().getDpiValue();
1619 * Returns the current device ydpi.
1621 public float getYDpi() {
1622 if (mState.device != null) {
1623 float dpi = mState.device.getYDpi();
1624 if (Float.isNaN(dpi) == false) {
1629 // get the pixel density as the density.
1630 return getDensity().getDpiValue();
1633 public Rectangle getScreenBounds() {
1634 // get the orientation from the current device config
1635 ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
1636 ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
1638 orientation = qual.getValue();
1641 // get the device screen dimension
1642 ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
1644 if (qual2 != null) {
1645 s1 = qual2.getValue1();
1646 s2 = qual2.getValue2();
1652 switch (orientation) {
1655 return new Rectangle(0, 0, s2, s1);
1657 return new Rectangle(0, 0, s1, s2);
1659 return new Rectangle(0, 0, s1, s1);
1664 * Returns the current theme, or null if the combo has no selection.
1666 * @return the theme name, or null
1668 public String getTheme() {
1669 int themeIndex = mThemeCombo.getSelectionIndex();
1670 if (themeIndex != -1) {
1671 return mThemeCombo.getItem(themeIndex);
1678 * Returns the current device string, or null if the combo has no selection.
1680 * @return the device name, or null
1682 public String getDevice() {
1683 int deviceIndex = mDeviceCombo.getSelectionIndex();
1684 if (deviceIndex != -1) {
1685 return mDeviceCombo.getItem(deviceIndex);
1692 * Returns whether the current theme selection is a project theme.
1693 * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
1694 * @return true for project theme, false for framework theme
1696 public boolean isProjectTheme() {
1697 return mIsProjectTheme.get(mThemeCombo.getSelectionIndex()).booleanValue();
1700 public IAndroidTarget getRenderingTarget() {
1701 int index = mTargetCombo.getSelectionIndex();
1703 return mTargetList.get(index);
1710 * Loads the list of {@link IAndroidTarget} and inits the UI with it.
1712 private void initTargets() {
1713 mTargetCombo.removeAll();
1714 mTargetList.clear();
1716 Sdk currentSdk = Sdk.getCurrent();
1717 if (currentSdk != null) {
1718 IAndroidTarget[] targets = currentSdk.getTargets();
1720 for (int i = 0 ; i < targets.length; i++) {
1721 // FIXME: support add-on rendering and check based on project minSdkVersion
1722 if (targets[i].isPlatform()) {
1723 mTargetCombo.add(targets[i].getFullName());
1724 mTargetList.add(targets[i]);
1726 if (mRenderingTarget != null) {
1727 // use equals because the rendering could be from a previous SDK, so
1728 // it may not be the same instance.
1729 if (mRenderingTarget.equals(targets[i])) {
1730 match = mTargetList.indexOf(targets[i]);
1732 } else if (mProjectTarget == targets[i]) {
1733 match = mTargetList.indexOf(targets[i]);
1738 mTargetCombo.setEnabled(mTargetList.size() > 1);
1740 mTargetCombo.deselectAll();
1742 // the rendering target is the same as the project.
1743 mRenderingTarget = mProjectTarget;
1745 mTargetCombo.select(match);
1747 // set the rendering target to the new object.
1748 mRenderingTarget = mTargetList.get(match);
1754 * Loads the list of {@link LayoutDevice} and inits the UI with it.
1756 private void initDevices() {
1759 Sdk sdk = Sdk.getCurrent();
1761 LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
1762 mDeviceList = manager.getCombinedList();
1766 // remove older devices if applicable
1767 mDeviceCombo.removeAll();
1768 mDeviceConfigCombo.removeAll();
1770 // fill with the devices
1771 if (mDeviceList != null) {
1772 for (LayoutDevice device : mDeviceList) {
1773 mDeviceCombo.add(device.getName());
1775 mDeviceCombo.select(0);
1777 if (mDeviceList.size() > 0) {
1778 List<DeviceConfig> configs = mDeviceList.get(0).getConfigs();
1779 for (DeviceConfig config : configs) {
1780 mDeviceConfigCombo.add(config.getName());
1782 mDeviceConfigCombo.select(0);
1783 if (configs.size() == 1) {
1784 mDeviceConfigCombo.setEnabled(false);
1789 // add the custom item
1790 mDeviceCombo.add("Custom...");
1794 * Selects a given {@link LayoutDevice} in the device combo, if it is found.
1795 * @param device the device to select
1796 * @return true if the device was found.
1798 private boolean selectDevice(LayoutDevice device) {
1799 final int count = mDeviceList.size();
1800 for (int i = 0 ; i < count ; i++) {
1801 // since device comes from mDeviceList, we can use the == operator.
1802 if (device == mDeviceList.get(i)) {
1803 mDeviceCombo.select(i);
1812 * Selects a config by name.
1813 * @param name the name of the config to select.
1815 private void selectConfig(String name) {
1816 final int count = mDeviceConfigCombo.getItemCount();
1817 for (int i = 0 ; i < count ; i++) {
1818 String item = mDeviceConfigCombo.getItem(i);
1819 if (name.equals(item)) {
1820 mDeviceConfigCombo.select(i);
1827 * Called when the selection of the device combo changes.
1828 * @param recomputeLayout
1830 private void onDeviceChange(boolean recomputeLayout) {
1831 // because changing the content of a combo triggers a change event, respect the
1832 // mDisableUpdates flag
1833 if (mDisableUpdates > 0) {
1837 String newConfigName = null;
1839 int deviceIndex = mDeviceCombo.getSelectionIndex();
1840 if (deviceIndex != -1) {
1841 // check if the user is asking for the custom item
1842 if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
1843 onCustomDeviceConfig();
1847 // get the previous config, so that we can look for a close match
1848 if (mState.device != null) {
1849 int index = mDeviceConfigCombo.getSelectionIndex();
1851 FolderConfiguration oldConfig = mState.device.getFolderConfigByName(
1852 mDeviceConfigCombo.getItem(index));
1854 LayoutDevice newDevice = mDeviceList.get(deviceIndex);
1856 newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
1860 mState.device = mDeviceList.get(deviceIndex);
1862 mState.device = null;
1865 fillConfigCombo(newConfigName);
1867 computeCurrentConfig();
1869 if (recomputeLayout) {
1870 onDeviceConfigChange();
1875 * Handles a user request for the {@link ConfigManagerDialog}.
1877 private void onCustomDeviceConfig() {
1878 ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
1881 // save the user devices
1882 Sdk.getCurrent().getLayoutDeviceManager().save();
1884 // Update the UI with no triggered event
1888 LayoutDevice oldCurrent = mState.device;
1890 // but first, update the device combo
1893 // attempts to reselect the current device.
1894 if (selectDevice(oldCurrent)) {
1895 // current device still exists.
1896 // reselect the config
1897 selectConfig(mState.configName);
1899 // reset the UI as if it was just a replacement file, since we can keep
1900 // the current device (and possibly config).
1901 adaptConfigSelection(false /*needBestMatch*/);
1904 // find a new device/config to match the current file.
1905 findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
1911 // recompute the current config
1912 computeCurrentConfig();
1915 onDeviceChange(true /*recomputeLayout*/);
1919 * Attempts to find a close config among a list
1920 * @param oldConfig the reference config.
1921 * @param configs the list of config to search through
1922 * @return the name of the closest config match, or possibly null if no configs are compatible
1923 * (this can only happen if the configs don't have a single qualifier that is the same).
1925 private String getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs) {
1927 // create 2 lists as we're going to go through one and put the candidates in the other.
1928 ArrayList<DeviceConfig> list1 = new ArrayList<DeviceConfig>();
1929 ArrayList<DeviceConfig> list2 = new ArrayList<DeviceConfig>();
1931 list1.addAll(configs);
1933 final int count = FolderConfiguration.getQualifierCount();
1934 for (int i = 0 ; i < count ; i++) {
1935 // compute the new candidate list by only taking configs that have
1936 // the same i-th qualifier as the old config
1937 for (DeviceConfig c : list1) {
1938 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
1940 FolderConfiguration folderConfig = c.getConfig();
1941 ResourceQualifier newQualifier = folderConfig.getQualifier(i);
1943 if (oldQualifier == null) {
1944 if (newQualifier == null) {
1947 } else if (oldQualifier.equals(newQualifier)) {
1952 // at any moment if the new candidate list contains only one match, its name
1954 if (list2.size() == 1) {
1955 return list2.get(0).getName();
1958 // if the list is empty, then all the new configs failed. It is considered ok, and
1959 // we move to the next qualifier anyway. This way, if a qualifier is different for
1960 // all new configs it is simply ignored.
1961 if (list2.size() != 0) {
1962 // move the candidates back into list1.
1964 list1.addAll(list2);
1969 // the only way to reach this point is if there's an exact match.
1970 // (if there are more than one, then there's a duplicate config and it doesn't matter,
1971 // we take the first one).
1972 if (list1.size() > 0) {
1973 return list1.get(0).getName();
1980 * fills the config combo with new values based on {@link #mState}.device.
1981 * @param refName an optional name. if set the selection will match this name (if found)
1983 private void fillConfigCombo(String refName) {
1984 mDeviceConfigCombo.removeAll();
1986 if (mState.device != null) {
1987 int selectionIndex = 0;
1990 for (DeviceConfig config : mState.device.getConfigs()) {
1991 mDeviceConfigCombo.add(config.getName());
1993 if (config.getName().equals(refName)) {
1999 mDeviceConfigCombo.select(selectionIndex);
2000 mDeviceConfigCombo.setEnabled(mState.device.getConfigs().size() > 1);
2005 * Called when the device config selection changes.
2007 private void onDeviceConfigChange() {
2008 // because changing the content of a combo triggers a change event, respect the
2009 // mDisableUpdates flag
2010 if (mDisableUpdates > 0) {
2014 if (computeCurrentConfig() && mListener != null) {
2015 mListener.onConfigurationChange();
2016 mListener.onDevicePostChange();
2021 * Call back for language combo selection
2023 private void onLocaleChange() {
2024 // because mLocaleList triggers onLocaleChange at each modification, the filling
2025 // of the combo with data will trigger notifications, and we don't want that.
2026 if (mDisableUpdates > 0) {
2030 if (computeCurrentConfig() && mListener != null) {
2031 mListener.onConfigurationChange();
2034 // Store locale project-wide setting
2038 private void onDockChange() {
2039 if (computeCurrentConfig() && mListener != null) {
2040 mListener.onConfigurationChange();
2044 private void onDayChange() {
2045 if (computeCurrentConfig() && mListener != null) {
2046 mListener.onConfigurationChange();
2051 * Call back for api level combo selection
2053 private void onRenderingTargetChange() {
2054 // because mApiCombo triggers onApiLevelChange at each modification, the filling
2055 // of the combo with data will trigger notifications, and we don't want that.
2056 if (mDisableUpdates > 0) {
2060 // tell the listener a new rendering target is being set. Need to do this before updating
2061 // mRenderingTarget.
2062 if (mListener != null && mRenderingTarget != null) {
2063 mListener.onRenderingTargetPreChange(mRenderingTarget);
2066 int index = mTargetCombo.getSelectionIndex();
2067 mRenderingTarget = mTargetList.get(index);
2069 boolean computeOk = computeCurrentConfig();
2071 // force a theme update to reflect the new rendering target.
2072 // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2073 // to figure out the theme list.
2076 // since the state is saved in computeCurrentConfig, we need to resave it since theme
2077 // change could have impacted it.
2080 if (mListener != null && mRenderingTarget != null) {
2081 mListener.onRenderingTargetPostChange(mRenderingTarget);
2084 if (computeOk && mListener != null) {
2085 mListener.onConfigurationChange();
2088 // Store project-wide render-target setting
2093 * Saves the current state and the current configuration
2097 private boolean computeCurrentConfig() {
2100 if (mState.device != null) {
2101 // get the device config from the device/config combos.
2102 int configIndex = mDeviceConfigCombo.getSelectionIndex();
2103 String name = mDeviceConfigCombo.getItem(configIndex);
2104 FolderConfiguration config = mState.device.getFolderConfigByName(name);
2106 // replace the config with the one from the device
2107 mCurrentConfig.set(config);
2109 // replace the locale qualifiers with the one coming from the locale combo
2110 int index = mLocaleCombo.getSelectionIndex();
2112 ResourceQualifier[] localeQualifiers = mLocaleList.get(index);
2114 mCurrentConfig.setLanguageQualifier(
2115 (LanguageQualifier)localeQualifiers[LOCALE_LANG]);
2116 mCurrentConfig.setRegionQualifier(
2117 (RegionQualifier)localeQualifiers[LOCALE_REGION]);
2120 index = mDockCombo.getSelectionIndex();
2122 index = 0; // no selection = 0
2124 mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index)));
2126 index = mNightCombo.getSelectionIndex();
2128 index = 0; // no selection = 0
2130 mCurrentConfig.setNightModeQualifier(
2131 new NightModeQualifier(NightMode.getByIndex(index)));
2133 // replace the API level by the selection of the combo
2134 index = mTargetCombo.getSelectionIndex();
2136 index = mTargetList.indexOf(mProjectTarget);
2139 IAndroidTarget target = mTargetList.get(index);
2141 if (target != null) {
2142 mCurrentConfig.setVersionQualifier(
2143 new VersionQualifier(target.getVersion().getApiLevel()));
2147 // update the create button.
2148 checkCreateEnable();
2156 private void onThemeChange() {
2159 int themeIndex = mThemeCombo.getSelectionIndex();
2160 if (themeIndex != -1) {
2161 String theme = mThemeCombo.getItem(themeIndex);
2163 if (theme.equals(THEME_SEPARATOR)) {
2164 mThemeCombo.select(0);
2167 if (mListener != null) {
2168 mListener.onThemeChange();
2174 * Returns whether the given <var>style</var> is a theme.
2175 * This is done by making sure the parent is a theme.
2176 * @param value the style to check
2177 * @param styleMap the map of styles for the current project. Key is the style name.
2178 * @return True if the given <var>style</var> is a theme.
2180 private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap) {
2181 if (value instanceof StyleResourceValue) {
2182 StyleResourceValue style = (StyleResourceValue)value;
2184 boolean frameworkStyle = false;
2185 String parentStyle = style.getParentStyle();
2186 if (parentStyle == null) {
2187 // if there is no specified parent style we look an implied one.
2188 // For instance 'Theme.light' is implied child style of 'Theme',
2189 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
2190 String name = style.getName();
2191 int index = name.lastIndexOf('.');
2193 parentStyle = name.substring(0, index);
2196 // remove the useless @ if it's there
2197 if (parentStyle.startsWith("@")) {
2198 parentStyle = parentStyle.substring(1);
2201 // check for framework identifier.
2202 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
2203 frameworkStyle = true;
2204 parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
2207 // at this point we could have the format style/<name>. we want only the name
2208 if (parentStyle.startsWith("style/")) {
2209 parentStyle = parentStyle.substring("style/".length());
2213 if (parentStyle != null) {
2214 if (frameworkStyle) {
2215 // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
2216 return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
2218 // if it's a project style, we check this is a theme.
2219 value = styleMap.get(parentStyle);
2220 if (value != null) {
2221 return isTheme(value, styleMap);
2230 private void checkCreateEnable() {
2231 mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
2235 * Checks whether the current edited file is the best match for a given config.
2237 * This tests against other versions of the same layout in the project.
2239 * The given config must be compatible with the current edited file.
2240 * @param config the config to test.
2241 * @return true if the current edited file is the best match in the project for the
2244 private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
2245 ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
2246 ResourceFolderType.LAYOUT, config);
2248 if (match != null) {
2249 return match.getFile().equals(mEditedFile);
2251 // if we stop here that means the current file is not even a match!
2252 AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
2259 * Resets the configuration chooser to reflect the given file configuration. This is
2260 * intended to be used by the "Show Included In" functionality where the user has
2261 * picked a non-default configuration (such as a particular landscape layout) and the
2262 * configuration chooser must be switched to a landscape layout. This method will
2263 * trigger a model change.
2265 * This will NOT trigger a redraw event!
2267 * FIXME: We are currently setting the configuration file to be the configuration for
2268 * the "outer" (the including) file, rather than the inner file, which is the file the
2269 * user is actually editing. We need to refine this, possibly with a way for the user
2270 * to choose which configuration they are editing. And in particular, we should be
2271 * filtering the configuration chooser to only show options in the outer configuration
2272 * that are compatible with the inner included file.
2274 * @param file the file to be configured
2276 public void resetConfigFor(IFile file) {
2278 mEditedConfig = null;
2283 * Syncs this configuration to the project wide locale and render target settings. The
2284 * locale may ignore the project-wide setting if it is a locale-specific
2287 * @return true if one or both of the toggles were changed, false if there were no
2290 public boolean syncRenderState() {
2291 if (mEditedConfig == null) {
2296 boolean localeChanged = false;
2297 boolean renderTargetChanged = false;
2299 // When a page is re-activated, force the toggles to reflect the current project
2302 Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
2304 // Only sync the locale if this layout is not already a locale-specific layout!
2305 if (!isLocaleSpecificLayout()) {
2306 ResourceQualifier[] locale = pair.getFirst();
2307 if (locale != null) {
2308 localeChanged = setLocaleCombo(locale[0], locale[1]);
2312 // Sync render target
2313 IAndroidTarget target = pair.getSecond();
2314 if (target != null) {
2315 int targetIndex = mTargetList.indexOf(target);
2316 if (targetIndex != mTargetCombo.getSelectionIndex()) {
2317 mTargetCombo.select(targetIndex);
2318 renderTargetChanged = true;
2322 if (!renderTargetChanged && !localeChanged) {
2326 // Update the locale and/or the render target. This code contains a logical
2327 // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
2328 // such that we don't duplicate work.
2330 if (renderTargetChanged) {
2331 if (mListener != null && mRenderingTarget != null) {
2332 mListener.onRenderingTargetPreChange(mRenderingTarget);
2334 int targetIndex = mTargetCombo.getSelectionIndex();
2335 mRenderingTarget = mTargetList.get(targetIndex);
2338 // Compute the new configuration; we want to do this both for locale changes
2339 // and for render targets.
2340 boolean computeOk = computeCurrentConfig();
2342 if (renderTargetChanged) {
2343 // force a theme update to reflect the new rendering target.
2344 // This must be done after computeCurrentConfig since it'll depend on the currentConfig
2345 // to figure out the theme list.
2348 if (mListener != null && mRenderingTarget != null) {
2349 mListener.onRenderingTargetPostChange(mRenderingTarget);
2353 // For both locale and render target changes
2354 if (computeOk && mListener != null) {
2355 mListener.onConfigurationChange();
2362 * Loads the render state (the locale and the render target, which are shared among
2363 * all the layouts meaning that changing it in one will change it in all) and returns
2364 * the current project-wide locale and render target to be used.
2366 * @return a pair of locale resource qualifiers and render target
2368 private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() {
2369 IProject project = mEditedFile.getProject();
2371 String data = project.getPersistentProperty(NAME_RENDER_STATE);
2373 ResourceQualifier[] locale = null;
2374 IAndroidTarget target = null;
2376 String[] values = data.split(SEP);
2377 if (values.length == 2) {
2378 locale = new ResourceQualifier[2];
2379 String locales[] = values[0].split(SEP_LOCALE);
2380 if (locales.length >= 2) {
2381 if (locales[0].length() > 0) {
2382 locale[0] = new LanguageQualifier(locales[0]);
2384 if (locales[1].length() > 0) {
2385 locale[1] = new RegionQualifier(locales[1]);
2388 target = stringToTarget(values[1]);
2391 return Pair.of(locale, target);
2394 ResourceQualifier[] any = new ResourceQualifier[] {
2395 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
2396 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
2399 return Pair.of(any, findDefaultRenderTarget());
2400 } catch (CoreException e) {
2401 AdtPlugin.log(e, null);
2407 /** Returns true if the current layout is locale-specific */
2408 private boolean isLocaleSpecificLayout() {
2409 return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null;
2413 * Saves the render state (the current locale and render target settings) into the
2414 * project wide settings storage
2416 private void saveRenderState() {
2417 IProject project = mEditedFile.getProject();
2419 int index = mLocaleCombo.getSelectionIndex();
2420 ResourceQualifier[] locale = mLocaleList.get(index);
2421 index = mTargetCombo.getSelectionIndex();
2422 IAndroidTarget target = mTargetList.get(index);
2424 // Generate a persistent string from locale+target
2425 StringBuilder sb = new StringBuilder();
2426 if (locale != null) {
2427 if (locale[0] != null && locale[1] != null) {
2428 // locale[0]/[1] can be null sometimes when starting Eclipse
2429 sb.append(((LanguageQualifier) locale[0]).getValue());
2430 sb.append(SEP_LOCALE);
2431 sb.append(((RegionQualifier) locale[1]).getValue());
2435 if (target != null) {
2436 sb.append(targetToString(target));
2440 String data = sb.toString();
2441 project.setPersistentProperty(NAME_RENDER_STATE, data);
2442 } catch (CoreException e) {
2443 AdtPlugin.log(e, null);