/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.resources.configurations.DockModeQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.NightModeQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenDimensionQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenOrientationQualifier;
import com.android.ide.eclipse.adt.internal.resources.configurations.VersionQualifier;
import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
import com.android.resources.Density;
import com.android.resources.DockMode;
import com.android.resources.NightMode;
import com.android.resources.ResourceType;
import com.android.resources.ScreenOrientation;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.util.Pair;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
/**
* A composite that displays the current configuration displayed in a Graphical Layout Editor.
* <p/>
* The composite has several entry points:<br>
* - {@link #setFile(IFile)}<br>
* Called after the constructor to set the file being edited. Nothing else is performed.<br>
*<br>
* - {@link #onXmlModelLoaded()}<br>
* Called when the XML model is loaded, either the first time or when the Target/SDK changes.
* This initializes the UI, either with the first compatible configuration found, or attempts
* to restore a configuration if one is found to have been saved in the file persistent storage.
* (see {@link #storeState()})<br>
*<br>
* - {@link #replaceFile(IFile)}<br>
* Called when a file, representing the same resource but with a different config is opened<br>
* by the user.<br>
*<br>
* - {@link #changeFileOnNewConfig(IFile)}<br>
* Called when config change triggers the editing of a file with a different config.
*<p/>
* Additionally, the composite can handle the following events.<br>
* - SDK reload. This is when the main SDK is finished loading.<br>
* - Target reload. This is when the target used by the project is the edited file has finished<br>
* loading.<br>
*/
public class ConfigurationComposite extends Composite {
private final static String SEP = ":"; //$NON-NLS-1$
private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
/**
* Setting name for project-wide setting controlling rendering target and locale which
* is shared for all files
*/
public final static QualifiedName NAME_RENDER_STATE =
new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$
/**
* Settings name for file-specific configuration preferences, such as which theme or
* device to render the current layout with
*/
public final static QualifiedName NAME_CONFIG_STATE =
new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
private final static int LOCALE_LANG = 0;
private final static int LOCALE_REGION = 1;
private Label mCurrentLayoutLabel;
private Button mCreateButton;
private Combo mDeviceCombo;
private Combo mDeviceConfigCombo;
private Combo mLocaleCombo;
private Combo mDockCombo;
private Combo mNightCombo;
private Combo mThemeCombo;
private Combo mTargetCombo;
private int mPlatformThemeCount = 0;
/** updates are disabled if > 0 */
private int mDisableUpdates = 0;
private List<LayoutDevice> mDeviceList;
private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
private final ArrayList<ResourceQualifier[] > mLocaleList =
new ArrayList<ResourceQualifier[]>();
private final ConfigState mState = new ConfigState();
private boolean mSdkChanged = false;
private boolean mFirstXmlModelChange = true;
/** The config listener given to the constructor. Never null. */
private final IConfigListener mListener;
/** The {@link FolderConfiguration} representing the state of the UI controls */
private final FolderConfiguration mCurrentConfig = new FolderConfiguration();
/** The file being edited */
private IFile mEditedFile;
/** The {@link ProjectResources} for the edited file's project */
private ProjectResources mResources;
/** The target of the project of the file being edited. */
private IAndroidTarget mProjectTarget;
/** The target of the project of the file being edited. */
private IAndroidTarget mRenderingTarget;
/** The {@link FolderConfiguration} being edited. */
private FolderConfiguration mEditedConfig;
/** Serialized state to use when initializing the configuration after the SDK is loaded */
private String mInitialState;
/**
* Interface implemented by the part which owns a {@link ConfigurationComposite}.
* This notifies the owners when the configuration change.
* The owner must also provide methods to provide the configuration that will
* be displayed.
*/
public interface IConfigListener {
/**
* Called when the {@link FolderConfiguration} change. The new config can be queried
* with {@link ConfigurationComposite#getCurrentConfig()}.
*/
void onConfigurationChange();
/**
* Called after a device has changed (in addition to {@link #onConfigurationChange}
* getting called)
*/
void onDevicePostChange();
/**
* Called when the current theme changes. The theme can be queried with
* {@link ConfigurationComposite#getTheme()}.
*/
void onThemeChange();
/**
* Called when the "Create" button is clicked.
*/
void onCreate();
/**
* Called before the rendering target changes.
* @param oldTarget the old rendering target
*/
void onRenderingTargetPreChange(IAndroidTarget oldTarget);
/**
* Called after the rendering target changes.
*
* @param target the new rendering target
*/
void onRenderingTargetPostChange(IAndroidTarget target);
ProjectResources getProjectResources();
ProjectResources getFrameworkResources();
ProjectResources getFrameworkResources(IAndroidTarget target);
Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
}
/**
* State of the current config. This is used during UI reset to attempt to return the
* rendering to its original configuration.
*/
private class ConfigState {
LayoutDevice device;
String configName;
ResourceQualifier[] locale;
String theme;
/** dock mode. Guaranteed to be non null */
DockMode dock = DockMode.NONE;
/** night mode. Guaranteed to be non null */
NightMode night = NightMode.NOTNIGHT;
/** the version being targeted for rendering */
IAndroidTarget target;
String getData() {
StringBuilder sb = new StringBuilder();
if (device != null) {
sb.append(device.getName());
sb.append(SEP);
sb.append(configName);
sb.append(SEP);
if (isLocaleSpecificLayout() && locale != null) {
if (locale[0] != null && locale[1] != null) {
// locale[0]/[1] can be null sometimes when starting Eclipse
sb.append(((LanguageQualifier) locale[0]).getValue());
sb.append(SEP_LOCALE);
sb.append(((RegionQualifier) locale[1]).getValue());
}
}
sb.append(SEP);
sb.append(theme);
sb.append(SEP);
sb.append(dock.getResourceValue());
sb.append(SEP);
sb.append(night.getResourceValue());
sb.append(SEP);
// We used to store the render target here in R9. Leave a marker
// to ensure that we don't reuse this slot; add new extra fields after it.
sb.append(SEP);
}
return sb.toString();
}
boolean setData(String data) {
String[] values = data.split(SEP);
if (values.length == 6 || values.length == 7) {
for (LayoutDevice d : mDeviceList) {
if (d.getName().equals(values[0])) {
device = d;
FolderConfiguration config = device.getFolderConfigByName(values[1]);
if (config != null) {
configName = values[1];
// Load locale. Note that this can get overwritten by the
// project-wide settings read below.
locale = new ResourceQualifier[2];
String locales[] = values[2].split(SEP_LOCALE);
if (locales.length >= 2) {
if (locales[0].length() > 0) {
locale[0] = new LanguageQualifier(locales[0]);
}
if (locales[1].length() > 0) {
locale[1] = new RegionQualifier(locales[1]);
}
}
theme = values[3];
dock = DockMode.getEnum(values[4]);
if (dock == null) {
dock = DockMode.NONE;
}
night = NightMode.getEnum(values[5]);
if (night == null) {
night = NightMode.NOTNIGHT;
}
// element 7/values[6]: used to store render target in R9.
// No longer stored here. If adding more data, make
// sure you leave 7 alone.
Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
// We only use the "global" setting
if (!isLocaleSpecificLayout()) {
locale = pair.getFirst();
}
target = pair.getSecond();
return true;
}
}
}
}
return false;
}
@Override
public String toString() {
return getData();
}
}
/**
* Returns a String id to represent an {@link IAndroidTarget} which can be translated
* back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id
* will never contain the {@link #SEP} character.
*
* @param target the target to return an id for
* @return an id for the given target; never null
*/
private String targetToString(IAndroidTarget target) {
return target.getFullName().replace(SEP, ""); //$NON-NLS-1$
}
/**
* Returns an {@link IAndroidTarget} that corresponds to the given id that was
* originally returned by {@link #targetToString}. May be null, if the platform is no
* longer available, or if the platform list has not yet been initialized.
*
* @param id the id that corresponds to the desired platform
* @return an {@link IAndroidTarget} that matches the given id, or null
*/
private IAndroidTarget stringToTarget(String id) {
if (mTargetList != null && mTargetList.size() > 0) {
for (IAndroidTarget target : mTargetList) {
if (id.equals(targetToString(target))) {
return target;
}
}
}
return null;
}
/**
* Creates a new {@link ConfigurationComposite} and adds it to the parent.
*
* The method also receives custom buttons to set into the configuration composite. The list
* is organized as an array of arrays. Each array represents a group of buttons thematically
* grouped together.
*
* @param listener An {@link IConfigListener} that gets and sets configuration properties.
* Mandatory, cannot be null.
* @param parent The parent composite.
* @param style The style of this composite.
* @param initialState The initial state (serialized form) to use for the configuration
*/
public ConfigurationComposite(IConfigListener listener,
Composite parent, int style, String initialState) {
super(parent, style);
mListener = listener;
mInitialState = initialState;
GridLayout gl;
GridData gd;
int cols = 7; // device+config+dock+day+separator*2+theme
// ---- First line: editing config display, locale, theme, create-button
Composite labelParent = new Composite(this, SWT.NONE);
labelParent.setLayout(gl = new GridLayout(5, false));
gl.marginWidth = gl.marginHeight = 0;
gl.marginTop = 3;
labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.horizontalSpan = cols;
new Label(labelParent, SWT.NONE).setText("Editing config:");
mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.widthHint = 50;
mLocaleCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
mLocaleCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onLocaleChange();
}
});
// Layout bug workaround. Without this, in -some- scenarios the Locale combo box was
// coming up tiny. Setting a minimumWidth hint does not work either. We need to have
// 2 or more items in the locale combo box when the layout is first run. These items
// are removed as part of the locale initialization when the SDK is loaded.
mLocaleCombo.add("Locale"); //$NON-NLS-1$ // Dummy place holders
mLocaleCombo.add("Locale"); //$NON-NLS-1$
mTargetCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY);
mTargetCombo.add("Android AOSP"); //$NON-NLS-1$ // Dummy place holders
mTargetCombo.add("Android AOSP"); //$NON-NLS-1$
mTargetCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onRenderingTargetChange();
}
});
mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT);
mCreateButton.setText("Create...");
mCreateButton.setEnabled(false);
mCreateButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (mListener != null) {
mListener.onCreate();
}
}
});
// ---- 2nd line: device/config/locale/theme Combos, create button.
setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
setLayout(gl = new GridLayout(cols, false));
gl.marginHeight = 0;
gl.horizontalSpacing = 0;
mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
mDeviceCombo.setLayoutData(new GridData(
GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
mDeviceCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onDeviceChange(true /* recomputeLayout*/);
}
});
mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
mDeviceConfigCombo.setLayoutData(new GridData(
GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onDeviceConfigChange();
}
});
// first separator
Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
separator.setLayoutData(gd = new GridData(
GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
gd.heightHint = 0;
mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
mDockCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
| GridData.GRAB_HORIZONTAL));
for (DockMode mode : DockMode.values()) {
mDockCombo.add(mode.getLongDisplayValue());
}
mDockCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onDockChange();
}
});
mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
mNightCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL
| GridData.GRAB_HORIZONTAL));
for (NightMode mode : NightMode.values()) {
mNightCombo.add(mode.getLongDisplayValue());
}
mNightCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onDayChange();
}
});
mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
mThemeCombo.setLayoutData(new GridData(
GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
mThemeCombo.setEnabled(false);
mThemeCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onThemeChange();
}
});
}
// ---- Init and reset/reload methods ----
/**
* Sets the reference to the file being edited.
* <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
* loaded (or reloaded as the SDK/target changes).
*
* @param file the file being opened
*
* @see #onXmlModelLoaded()
* @see #replaceFile(IFile)
* @see #changeFileOnNewConfig(IFile)
*/
public void setFile(IFile file) {
mEditedFile = file;
}
/**
* Replaces the UI with a given file configuration. This is meant to answer the user
* explicitly opening a different version of the same layout from the Package Explorer.
* <p/>This attempts to keep the current config, but may change it if it's not compatible or
* not the best match
* <p/>This will NOT trigger a redraw event (will not call
* {@link IConfigListener#onConfigurationChange()}.)
* @param file the file being opened.
*/
public void replaceFile(IFile file) {
// if there is no previous selection, revert to default mode.
if (mState.device == null) {
setFile(file); // onTargetChanged will be called later.
return;
}
mEditedFile = file;
IProject iProject = mEditedFile.getProject();
mResources = ResourceManager.getInstance().getProjectResources(iProject);
ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
mEditedConfig = resFolder.getConfiguration();
mDisableUpdates++; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
try {
// only attempt to do anything if the SDK and targets are loaded.
LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
if (sdkStatus == LoadStatus.LOADED) {
LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
null /*project*/);
if (targetStatus == LoadStatus.LOADED) {
// update the current config selection to make sure it's
// compatible with the new file
adaptConfigSelection(true /*needBestMatch*/);
// compute the final current config
computeCurrentConfig();
// update the string showing the config value
updateConfigDisplay(mEditedConfig);
}
}
} finally {
mDisableUpdates--;
}
}
/**
* Updates the UI with a new file that was opened in response to a config change.
* @param file the file being opened.
*
* @see #replaceFile(IFile)
*/
public void changeFileOnNewConfig(IFile file) {
mEditedFile = file;
IProject iProject = mEditedFile.getProject();
mResources = ResourceManager.getInstance().getProjectResources(iProject);
ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
mEditedConfig = resFolder.getConfiguration();
// All that's needed is to update the string showing the config value
// (since the config combo were chosen by the user).
updateConfigDisplay(mEditedConfig);
}
/**
* Responds to the event that the basic SDK information finished loading.
* @param target the possibly new target object associated with the file being edited (in case
* the SDK path was changed).
*/
public void onSdkLoaded(IAndroidTarget target) {
// a change to the SDK means that we need to check for new/removed devices.
mSdkChanged = true;
// store the new target.
mProjectTarget = target;
mDisableUpdates++; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
try {
// this is going to be followed by a call to onTargetLoaded.
// So we can only care about the layout devices in this case.
initDevices();
initTargets();
} finally {
mDisableUpdates--;
}
}
/**
* Answers to the XML model being loaded, either the first time or when the Target/SDK changes.
* <p>This initializes the UI, either with the first compatible configuration found,
* or attempts to restore a configuration if one is found to have been saved in the file
* persistent storage.
* <p>If the SDK or target are not loaded, nothing will happened (but the method must be called
* back when those are loaded).
* <p>The method automatically handles being called the first time after editor creation, or
* being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
* is properly called).
*
* @see #storeState()
* @see #onSdkLoaded(IAndroidTarget)
*/
public AndroidTargetData onXmlModelLoaded() {
AndroidTargetData targetData = null;
// only attempt to do anything if the SDK and targets are loaded.
LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
if (sdkStatus == LoadStatus.LOADED) {
mDisableUpdates++; // we do not want to trigger onXXXChange when setting
try {
// init the devices if needed (new SDK or first time going through here)
if (mSdkChanged || mFirstXmlModelChange) {
initDevices();
initTargets();
}
IProject iProject = mEditedFile.getProject();
Sdk currentSdk = Sdk.getCurrent();
if (currentSdk != null) {
mProjectTarget = currentSdk.getTarget(iProject);
}
LoadStatus targetStatus = LoadStatus.FAILED;
if (mProjectTarget != null) {
targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
initTargets();
}
if (targetStatus == LoadStatus.LOADED) {
if (mResources == null) {
mResources = ResourceManager.getInstance().getProjectResources(iProject);
}
if (mEditedConfig == null) {
ResourceFolder resFolder = mResources.getResourceFolder(
(IFolder) mEditedFile.getParent());
mEditedConfig = resFolder.getConfiguration();
}
targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
// get the file stored state
boolean loadedConfigData = false;
String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE);
if (mInitialState != null) {
data = mInitialState;
mInitialState = null;
}
if (data != null) {
loadedConfigData = mState.setData(data);
}
// update the themes and locales.
updateThemes();
updateLocales();
// If the current state was loaded from the persistent storage, we update the
// UI with it and then try to adapt it (which will handle incompatible
// configuration).
// Otherwise, just look for the first compatible configuration.
if (loadedConfigData) {
// first make sure we have the config to adapt
selectDevice(mState.device);
fillConfigCombo(mState.configName);
adaptConfigSelection(false /*needBestMatch*/);
mDockCombo.select(DockMode.getIndex(mState.dock));
mNightCombo.select(NightMode.getIndex(mState.night));
mTargetCombo.select(mTargetList.indexOf(mState.target));
targetData = Sdk.getCurrent().getTargetData(mState.target);
} else {
findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
// Default to modern layout lib
IAndroidTarget target = findDefaultRenderTarget();
if (target != null) {
targetData = Sdk.getCurrent().getTargetData(target);
mTargetCombo.select(mTargetList.indexOf(target));
}
}
// update the string showing the config value
updateConfigDisplay(mEditedConfig);
// compute the final current config
computeCurrentConfig();
}
} finally {
mDisableUpdates--;
mFirstXmlModelChange = false;
}
}
return targetData;
}
/** Return the default render target to use, or null if no strong preference */
private IAndroidTarget findDefaultRenderTarget() {
// Default to layoutlib version 5
Sdk current = Sdk.getCurrent();
if (current != null) {
for (IAndroidTarget target : current.getTargets()) {
// Only Honeycomb has layoutlib version 5; as soon as we backport
// adjust this algorithm to find the lowest version that contains
// layoutlib 5
AndroidVersion version = target.getVersion();
int apiLevel = version.getApiLevel();
if (apiLevel >= 11) { // Layoutlib so far has been backported to 11
return target;
}
}
}
return null;
}
private static class ConfigBundle {
FolderConfiguration config;
int localeIndex;
int dockModeIndex;
int nightModeIndex;
ConfigBundle() {
config = new FolderConfiguration();
localeIndex = 0;
dockModeIndex = 0;
nightModeIndex = 0;
}
ConfigBundle(ConfigBundle bundle) {
config = new FolderConfiguration();
config.set(bundle.config);
localeIndex = bundle.localeIndex;
dockModeIndex = bundle.dockModeIndex;
nightModeIndex = bundle.nightModeIndex;
}
}
/**
* Finds a device/config that can display {@link #mEditedConfig}.
* <p/>Once found the device and config combos are set to the config.
* <p/>If there is no compatible configuration, a custom one is created.
* @param favorCurrentConfig if true, and no best match is found, don't change
* the current config. This must only be true if the current config is compatible.
*/
private void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
LayoutDevice anyDeviceMatch = null; // a compatible device/config/locale
String anyConfigMatchName = null;
ConfigBundle anyConfigBundle = null;
LayoutDevice bestDeviceMatch = null; // an actual best match
String bestConfigMatchName = null;
ConfigBundle bestConfigBundle = null;
FolderConfiguration testConfig = new FolderConfiguration();
// get a locale that match the host locale roughly (may not be exact match on the region.)
int localeHostMatch = getLocaleMatch();
// build a list of combinations of non standard qualifiers to add to each device's
// qualifier set when testing for a match.
// These qualifiers are: locale, nightmode, car dock.
List<ConfigBundle> addConfig = new ArrayList<ConfigBundle>();
// If the edited file has locales, then we have to select a matching locale from
// the list.
// However, if it doesn't, we don't randomly take the first locale, we take one
// matching the current host locale (making sure it actually exist in the project)
int start, max;
if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) {
// add all the locales
start = 0;
max = mLocaleList.size();
} else {
// only add the locale host match
start = localeHostMatch;
max = localeHostMatch + 1; // test is <
}
for (int i = start ; i < max ; i++) {
ResourceQualifier[] l = mLocaleList.get(i);
ConfigBundle bundle = new ConfigBundle();
bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]);
bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]);
bundle.localeIndex = i;
addConfig.add(bundle);
}
// add the dock mode to the bundle combinations.
addDockModeToBundles(addConfig);
// add the night mode to the bundle combinations.
addNightModeToBundles(addConfig);
mainloop: for (LayoutDevice device : mDeviceList) {
for (DeviceConfig config : device.getConfigs()) {
// loop on the list of qualifier adds
for (ConfigBundle bundle : addConfig) {
// set the base config. This erase all data in testConfig.
testConfig.set(config.getConfig());
// add on top of it, the extra qualifiers
testConfig.add(bundle.config);
if (mEditedConfig.isMatchFor(testConfig)) {
// this is a basic match. record it in case we don't find a match
// where the edited file is a best config.
if (anyDeviceMatch == null) {
anyDeviceMatch = device;
anyConfigMatchName = config.getName();
anyConfigBundle = bundle;
}
if (isCurrentFileBestMatchFor(testConfig)) {
// this is what we want.
bestDeviceMatch = device;
bestConfigMatchName = config.getName();
bestConfigBundle = bundle;
break mainloop;
}
}
}
}
}
if (bestDeviceMatch == null) {
if (favorCurrentConfig) {
// quick check
if (mEditedConfig.isMatchFor(mCurrentConfig) == false) {
AdtPlugin.log(IStatus.ERROR,
"favorCurrentConfig can only be true if the current config is compatible");
}
// just display the warning
AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
String.format(
"'%1$s' is not a best match for any device/locale combination.",
mEditedConfig.toDisplayString()),
String.format(
"Displaying it with '%1$s'",
mCurrentConfig.toDisplayString()));
} else if (anyDeviceMatch != null) {
// select the device anyway.
selectDevice(mState.device = anyDeviceMatch);
fillConfigCombo(anyConfigMatchName);
mLocaleCombo.select(anyConfigBundle.localeIndex);
mDockCombo.select(anyConfigBundle.dockModeIndex);
mNightCombo.select(anyConfigBundle.nightModeIndex);
// TODO: display a better warning!
computeCurrentConfig();
AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
String.format(
"'%1$s' is not a best match for any device/locale combination.",
mEditedConfig.toDisplayString()),
String.format(
"Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.",
mCurrentConfig.toDisplayString()));
} else {
// TODO: there is no device/config able to display the layout, create one.
// For the base config values, we'll take the first device and config,
// and replace whatever qualifier required by the layout file.
}
} else {
selectDevice(mState.device = bestDeviceMatch);
fillConfigCombo(bestConfigMatchName);
mLocaleCombo.select(bestConfigBundle.localeIndex);
mDockCombo.select(bestConfigBundle.dockModeIndex);
mNightCombo.select(bestConfigBundle.nightModeIndex);
}
}
private void addDockModeToBundles(List<ConfigBundle> addConfig) {
ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
// loop on each item and for each, add all variations of the dock modes
for (ConfigBundle bundle : addConfig) {
int index = 0;
for (DockMode mode : DockMode.values()) {
ConfigBundle b = new ConfigBundle(bundle);
b.config.setDockModeQualifier(new DockModeQualifier(mode));
b.dockModeIndex = index++;
list.add(b);
}
}
addConfig.clear();
addConfig.addAll(list);
}
private void addNightModeToBundles(List<ConfigBundle> addConfig) {
ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
// loop on each item and for each, add all variations of the night modes
for (ConfigBundle bundle : addConfig) {
int index = 0;
for (NightMode mode : NightMode.values()) {
ConfigBundle b = new ConfigBundle(bundle);
b.config.setNightModeQualifier(new NightModeQualifier(mode));
b.nightModeIndex = index++;
list.add(b);
}
}
addConfig.clear();
addConfig.addAll(list);
}
/**
* Adapts the current device/config selection so that it's compatible with
* {@link #mEditedConfig}.
* <p/>If the current selection is compatible, nothing is changed.
* <p/>If it's not compatible, configs from the current devices are tested.
* <p/>If none are compatible, it reverts to
* {@link #findAndSetCompatibleConfig(FolderConfiguration)}
*/
private void adaptConfigSelection(boolean needBestMatch) {
// check the device config (ie sans locale)
boolean needConfigChange = true; // if still true, we need to find another config.
boolean currentConfigIsCompatible = false;
int configIndex = mDeviceConfigCombo.getSelectionIndex();
if (configIndex != -1) {
String configName = mDeviceConfigCombo.getItem(configIndex);
FolderConfiguration currentConfig = mState.device.getFolderConfigByName(configName);
if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) {
currentConfigIsCompatible = true; // current config is compatible
if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) {
needConfigChange = false;
}
}
}
if (needConfigChange) {
// if the current config/locale isn't a correct match, then
// look for another config/locale in the same device.
FolderConfiguration testConfig = new FolderConfiguration();
// first look in the current device.
String matchName = null;
int localeIndex = -1;
mainloop: for (DeviceConfig config : mState.device.getConfigs()) {
testConfig.set(config.getConfig());
// loop on the locales.
for (int i = 0 ; i < mLocaleList.size() ; i++) {
ResourceQualifier[] locale = mLocaleList.get(i);
// update the test config with the locale qualifiers
testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]);
testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]);
if (mEditedConfig.isMatchFor(testConfig) &&
isCurrentFileBestMatchFor(testConfig)) {
matchName = config.getName();
localeIndex = i;
break mainloop;
}
}
}
if (matchName != null) {
selectConfig(matchName);
mLocaleCombo.select(localeIndex);
} else {
// no match in current device with any config/locale
// attempt to find another device that can display this particular config.
findAndSetCompatibleConfig(currentConfigIsCompatible);
}
}
}
/**
* Finds a locale matching the config from a file.
* @param language the language qualifier or null if none is set.
* @param region the region qualifier or null if none is set.
* @return true if there was a change in the combobox as a result of applying the locale
*/
private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
boolean changed = false;
// find the locale match. Since the locale list is based on the content of the
// project resources there must be an exact match.
// The only trick is that the region could be null in the fileConfig but in our
// list of locales, this is represented as a RegionQualifier with value of
// FAKE_LOCALE_VALUE.
final int count = mLocaleList.size();
for (int i = 0 ; i < count ; i++) {
ResourceQualifier[] locale = mLocaleList.get(i);
// the language qualifier in the locale list is never null.
if (locale[LOCALE_LANG].equals(language)) {
// region comparison is more complex, as the region could be null.
if (region == null) {
if (RegionQualifier.FAKE_REGION_VALUE.equals(
((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
// match!
if (mLocaleCombo.getSelectionIndex() != i) {
mLocaleCombo.select(i);
changed = true;
}
break;
}
} else if (region.equals(locale[LOCALE_REGION])) {
// match!
if (mLocaleCombo.getSelectionIndex() != i) {
mLocaleCombo.select(i);
changed = true;
}
break;
}
}
}
return changed;
}
private void updateConfigDisplay(FolderConfiguration fileConfig) {
String current = fileConfig.toDisplayString();
String layoutLabel = current != null ? current : "(Default)";
mCurrentLayoutLabel.setText(layoutLabel);
mCurrentLayoutLabel.setToolTipText(layoutLabel);
}
private void saveState() {
if (mDisableUpdates == 0) {
int index = mDeviceConfigCombo.getSelectionIndex();
if (index != -1) {
mState.configName = mDeviceConfigCombo.getItem(index);
} else {
mState.configName = null;
}
// since the locales are relative to the project, only keeping the index is enough
index = mLocaleCombo.getSelectionIndex();
if (index != -1) {
mState.locale = mLocaleList.get(index);
} else {
mState.locale = null;
}
index = mThemeCombo.getSelectionIndex();
if (index != -1) {
mState.theme = mThemeCombo.getItem(index);
}
index = mDockCombo.getSelectionIndex();
if (index != -1) {
mState.dock = DockMode.getByIndex(index);
}
index = mNightCombo.getSelectionIndex();
if (index != -1) {
mState.night = NightMode.getByIndex(index);
}
index = mTargetCombo.getSelectionIndex();
if (index != -1) {
mState.target = mTargetList.get(index);
}
}
}
/**
* Stores the current config selection into the edited file.
*/
public void storeState() {
AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData());
}
/**
* Updates the locale combo.
* This must be called from the UI thread.
*/
public void updateLocales() {
if (mListener == null) {
return; // can't do anything w/o it.
}
mDisableUpdates++;
try {
// Reset the combo
mLocaleCombo.removeAll();
mLocaleList.clear();
SortedSet<String> languages = null;
boolean hasLocale = false;
// get the languages from the project.
ProjectResources project = mListener.getProjectResources();
// in cases where the opened file is not linked to a project, this could be null.
if (project != null) {
// now get the languages from the project.
languages = project.getLanguages();
for (String language : languages) {
hasLocale = true;
LanguageQualifier langQual = new LanguageQualifier(language);
// find the matching regions and add them
SortedSet<String> regions = project.getRegions(language);
for (String region : regions) {
mLocaleCombo.add(
String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$
RegionQualifier regionQual = new RegionQualifier(region);
mLocaleList.add(new ResourceQualifier[] { langQual, regionQual });
}
// now the entry for the other regions the language alone
if (regions.size() > 0) {
mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$
} else {
mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$
}
// create a region qualifier that will never be matched by qualified resources.
mLocaleList.add(new ResourceQualifier[] {
langQual,
new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
});
}
}
// add a locale not present in the project resources. This will let the dev
// tests his/her default values.
if (hasLocale) {
mLocaleCombo.add("Other");
} else {
mLocaleCombo.add("Any locale");
}
// create language/region qualifier that will never be matched by qualified resources.
mLocaleList.add(new ResourceQualifier[] {
new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
});
if (mState.locale != null) {
// FIXME: this may fails if the layout was deleted (and was the last one to have
// that local. (we have other problem in this case though)
setLocaleCombo(mState.locale[LOCALE_LANG],
mState.locale[LOCALE_REGION]);
} else {
mLocaleCombo.select(0);
}
mThemeCombo.getParent().layout();
} finally {
mDisableUpdates--;
}
}
private int getLocaleMatch() {
Locale locale = Locale.getDefault();
if (locale != null) {
String currentLanguage = locale.getLanguage();
String currentRegion = locale.getCountry();
final int count = mLocaleList.size();
for (int l = 0 ; l < count ; l++) {
ResourceQualifier[] localeArray = mLocaleList.get(l);
LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG];
RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION];
// there's always a ##/Other or ##/Any (which is the same, the region
// contains FAKE_REGION_VALUE). If we don't find a perfect region match
// we take the fake region. Since it's last in the list, this makes the
// test easy.
if (langQ.getValue().equals(currentLanguage) &&
(regionQ.getValue().equals(currentRegion) ||
regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) {
return l;
}
}
// if no locale match the current local locale, it's likely that it is
// the default one which is the last one.
return count - 1;
}
return -1;
}
/**
* Updates the theme combo.
* This must be called from the UI thread.
*/
private void updateThemes() {
if (mListener == null) {
return; // can't do anything w/o it.
}
ProjectResources frameworkProject = mListener.getFrameworkResources(getRenderingTarget());
mDisableUpdates++;
try {
// Reset the combo
mThemeCombo.removeAll();
mPlatformThemeCount = 0;
ArrayList<String> themes = new ArrayList<String>();
// get the themes, and languages from the Framework.
if (frameworkProject != null) {
// get the configured resources for the framework
Map<ResourceType, Map<String, ResourceValue>> frameworResources =
frameworkProject.getConfiguredResources(getCurrentConfig());
if (frameworResources != null) {
// get the styles.
Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
// collect the themes out of all the styles.
for (ResourceValue value : styles.values()) {
String name = value.getName();
if (name.startsWith("Theme.") || name.equals("Theme")) {
themes.add(value.getName());
mPlatformThemeCount++;
}
}
// sort them and add them to the combo
Collections.sort(themes);
for (String theme : themes) {
mThemeCombo.add(theme);
}
mPlatformThemeCount = themes.size();
themes.clear();
}
}
// now get the themes and languages from the project.
ProjectResources project = mListener.getProjectResources();
// in cases where the opened file is not linked to a project, this could be null.
if (project != null) {
// get the configured resources for the project
Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
mListener.getConfiguredProjectResources();
if (configuredProjectRes != null) {
// get the styles.
Map<String, ResourceValue> styleMap = configuredProjectRes.get(
ResourceType.STYLE);
if (styleMap != null) {
// collect the themes out of all the styles, ie styles that extend,
// directly or indirectly a platform theme.
for (ResourceValue value : styleMap.values()) {
if (isTheme(value, styleMap)) {
themes.add(value.getName());
}
}
// sort them and add them the to the combo.
if (mPlatformThemeCount > 0 && themes.size() > 0) {
mThemeCombo.add(THEME_SEPARATOR);
}
Collections.sort(themes);
for (String theme : themes) {
mThemeCombo.add(theme);
}
}
}
}
// try to reselect the previous theme.
boolean needDefaultSelection = true;
if (mState.theme != null) {
final int count = mThemeCombo.getItemCount();
for (int i = 0 ; i < count ; i++) {
if (mState.theme.equals(mThemeCombo.getItem(i))) {
mThemeCombo.select(i);
needDefaultSelection = false;
mThemeCombo.setEnabled(true);
break;
}
}
}
if (needDefaultSelection) {
if (mThemeCombo.getItemCount() > 0) {
mThemeCombo.select(0);
mThemeCombo.setEnabled(true);
} else {
mThemeCombo.setEnabled(false);
}
}
mThemeCombo.getParent().layout();
} finally {
mDisableUpdates--;
}
}
// ---- getters for the config selection values ----
public FolderConfiguration getEditedConfig() {
return mEditedConfig;
}
public FolderConfiguration getCurrentConfig() {
return mCurrentConfig;
}
public void getCurrentConfig(FolderConfiguration config) {
config.set(mCurrentConfig);
}
/**
* Returns the currently selected {@link Density}. This is guaranteed to be non null.
*/
public Density getDensity() {
if (mCurrentConfig != null) {
PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier();
if (qual != null) {
// just a sanity check
Density d = qual.getValue();
if (d != Density.NODPI) {
return d;
}
}
}
// no config? return medium as the default density.
return Density.MEDIUM;
}
/**
* Returns the current device xdpi.
*/
public float getXDpi() {
if (mState.device != null) {
float dpi = mState.device.getXDpi();
if (Float.isNaN(dpi) == false) {
return dpi;
}
}
// get the pixel density as the density.
return getDensity().getDpiValue();
}
/**
* Returns the current device ydpi.
*/
public float getYDpi() {
if (mState.device != null) {
float dpi = mState.device.getYDpi();
if (Float.isNaN(dpi) == false) {
return dpi;
}
}
// get the pixel density as the density.
return getDensity().getDpiValue();
}
public Rectangle getScreenBounds() {
// get the orientation from the current device config
ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier();
ScreenOrientation orientation = ScreenOrientation.PORTRAIT;
if (qual != null) {
orientation = qual.getValue();
}
// get the device screen dimension
ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier();
int s1, s2;
if (qual2 != null) {
s1 = qual2.getValue1();
s2 = qual2.getValue2();
} else {
s1 = 480;
s2 = 320;
}
switch (orientation) {
default:
case PORTRAIT:
return new Rectangle(0, 0, s2, s1);
case LANDSCAPE:
return new Rectangle(0, 0, s1, s2);
case SQUARE:
return new Rectangle(0, 0, s1, s1);
}
}
/**
* Returns the current theme, or null if the combo has no selection.
*
* @return the theme name, or null
*/
public String getTheme() {
int themeIndex = mThemeCombo.getSelectionIndex();
if (themeIndex != -1) {
return mThemeCombo.getItem(themeIndex);
}
return null;
}
/**
* Returns the current device string, or null if the combo has no selection.
*
* @return the device name, or null
*/
public String getDevice() {
int deviceIndex = mDeviceCombo.getSelectionIndex();
if (deviceIndex != -1) {
return mDeviceCombo.getItem(deviceIndex);
}
return null;
}
/**
* Returns whether the current theme selection is a project theme.
* <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
* @return true for project theme, false for framework theme
*/
public boolean isProjectTheme() {
return mThemeCombo.getSelectionIndex() >= mPlatformThemeCount;
}
public IAndroidTarget getRenderingTarget() {
int index = mTargetCombo.getSelectionIndex();
if (index >= 0) {
return mTargetList.get(index);
}
return null;
}
/**
* Loads the list of {@link IAndroidTarget} and inits the UI with it.
*/
private void initTargets() {
mTargetCombo.removeAll();
mTargetList.clear();
Sdk currentSdk = Sdk.getCurrent();
if (currentSdk != null) {
IAndroidTarget[] targets = currentSdk.getTargets();
int match = -1;
for (int i = 0 ; i < targets.length; i++) {
// FIXME: support add-on rendering and check based on project minSdkVersion
if (targets[i].isPlatform()) {
mTargetCombo.add(targets[i].getFullName());
mTargetList.add(targets[i]);
if (mRenderingTarget != null) {
// use equals because the rendering could be from a previous SDK, so
// it may not be the same instance.
if (mRenderingTarget.equals(targets[i])) {
match = mTargetList.indexOf(targets[i]);
}
} else if (mProjectTarget == targets[i]) {
match = mTargetList.indexOf(targets[i]);
}
}
}
mTargetCombo.setEnabled(mTargetList.size() > 1);
if (match == -1) {
mTargetCombo.deselectAll();
// the rendering target is the same as the project.
mRenderingTarget = mProjectTarget;
} else {
mTargetCombo.select(match);
// set the rendering target to the new object.
mRenderingTarget = mTargetList.get(match);
}
}
}
/**
* Loads the list of {@link LayoutDevice} and inits the UI with it.
*/
private void initDevices() {
mDeviceList = null;
Sdk sdk = Sdk.getCurrent();
if (sdk != null) {
LayoutDeviceManager manager = sdk.getLayoutDeviceManager();
mDeviceList = manager.getCombinedList();
}
// remove older devices if applicable
mDeviceCombo.removeAll();
mDeviceConfigCombo.removeAll();
// fill with the devices
if (mDeviceList != null) {
for (LayoutDevice device : mDeviceList) {
mDeviceCombo.add(device.getName());
}
mDeviceCombo.select(0);
if (mDeviceList.size() > 0) {
List<DeviceConfig> configs = mDeviceList.get(0).getConfigs();
for (DeviceConfig config : configs) {
mDeviceConfigCombo.add(config.getName());
}
mDeviceConfigCombo.select(0);
if (configs.size() == 1) {
mDeviceConfigCombo.setEnabled(false);
}
}
}
// add the custom item
mDeviceCombo.add("Custom...");
}
/**
* Selects a given {@link LayoutDevice} in the device combo, if it is found.
* @param device the device to select
* @return true if the device was found.
*/
private boolean selectDevice(LayoutDevice device) {
final int count = mDeviceList.size();
for (int i = 0 ; i < count ; i++) {
// since device comes from mDeviceList, we can use the == operator.
if (device == mDeviceList.get(i)) {
mDeviceCombo.select(i);
return true;
}
}
return false;
}
/**
* Selects a config by name.
* @param name the name of the config to select.
*/
private void selectConfig(String name) {
final int count = mDeviceConfigCombo.getItemCount();
for (int i = 0 ; i < count ; i++) {
String item = mDeviceConfigCombo.getItem(i);
if (name.equals(item)) {
mDeviceConfigCombo.select(i);
return;
}
}
}
/**
* Called when the selection of the device combo changes.
* @param recomputeLayout
*/
private void onDeviceChange(boolean recomputeLayout) {
// because changing the content of a combo triggers a change event, respect the
// mDisableUpdates flag
if (mDisableUpdates > 0) {
return;
}
String newConfigName = null;
int deviceIndex = mDeviceCombo.getSelectionIndex();
if (deviceIndex != -1) {
// check if the user is asking for the custom item
if (deviceIndex == mDeviceCombo.getItemCount() - 1) {
onCustomDeviceConfig();
return;
}
// get the previous config, so that we can look for a close match
if (mState.device != null) {
int index = mDeviceConfigCombo.getSelectionIndex();
if (index != -1) {
FolderConfiguration oldConfig = mState.device.getFolderConfigByName(
mDeviceConfigCombo.getItem(index));
LayoutDevice newDevice = mDeviceList.get(deviceIndex);
newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
}
}
mState.device = mDeviceList.get(deviceIndex);
} else {
mState.device = null;
}
fillConfigCombo(newConfigName);
computeCurrentConfig();
if (recomputeLayout) {
onDeviceConfigChange();
}
}
/**
* Handles a user request for the {@link ConfigManagerDialog}.
*/
private void onCustomDeviceConfig() {
ConfigManagerDialog dialog = new ConfigManagerDialog(getShell());
dialog.open();
// save the user devices
Sdk.getCurrent().getLayoutDeviceManager().save();
// Update the UI with no triggered event
mDisableUpdates++;
try {
LayoutDevice oldCurrent = mState.device;
// but first, update the device combo
initDevices();
// attempts to reselect the current device.
if (selectDevice(oldCurrent)) {
// current device still exists.
// reselect the config
selectConfig(mState.configName);
// reset the UI as if it was just a replacement file, since we can keep
// the current device (and possibly config).
adaptConfigSelection(false /*needBestMatch*/);
} else {
// find a new device/config to match the current file.
findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
}
} finally {
mDisableUpdates--;
}
// recompute the current config
computeCurrentConfig();
// force a redraw
onDeviceChange(true /*recomputeLayout*/);
}
/**
* Attempts to find a close config among a list
* @param oldConfig the reference config.
* @param configs the list of config to search through
* @return the name of the closest config match, or possibly null if no configs are compatible
* (this can only happen if the configs don't have a single qualifier that is the same).
*/
private String getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs) {
// create 2 lists as we're going to go through one and put the candidates in the other.
ArrayList<DeviceConfig> list1 = new ArrayList<DeviceConfig>();
ArrayList<DeviceConfig> list2 = new ArrayList<DeviceConfig>();
list1.addAll(configs);
final int count = FolderConfiguration.getQualifierCount();
for (int i = 0 ; i < count ; i++) {
// compute the new candidate list by only taking configs that have
// the same i-th qualifier as the old config
for (DeviceConfig c : list1) {
ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
FolderConfiguration folderConfig = c.getConfig();
ResourceQualifier newQualifier = folderConfig.getQualifier(i);
if (oldQualifier == null) {
if (newQualifier == null) {
list2.add(c);
}
} else if (oldQualifier.equals(newQualifier)) {
list2.add(c);
}
}
// at any moment if the new candidate list contains only one match, its name
// is returned.
if (list2.size() == 1) {
return list2.get(0).getName();
}
// if the list is empty, then all the new configs failed. It is considered ok, and
// we move to the next qualifier anyway. This way, if a qualifier is different for
// all new configs it is simply ignored.
if (list2.size() != 0) {
// move the candidates back into list1.
list1.clear();
list1.addAll(list2);
list2.clear();
}
}
// the only way to reach this point is if there's an exact match.
// (if there are more than one, then there's a duplicate config and it doesn't matter,
// we take the first one).
if (list1.size() > 0) {
return list1.get(0).getName();
}
return null;
}
/**
* fills the config combo with new values based on {@link #mState}.device.
* @param refName an optional name. if set the selection will match this name (if found)
*/
private void fillConfigCombo(String refName) {
mDeviceConfigCombo.removeAll();
if (mState.device != null) {
int selectionIndex = 0;
int i = 0;
for (DeviceConfig config : mState.device.getConfigs()) {
mDeviceConfigCombo.add(config.getName());
if (config.getName().equals(refName)) {
selectionIndex = i;
}
i++;
}
mDeviceConfigCombo.select(selectionIndex);
mDeviceConfigCombo.setEnabled(mState.device.getConfigs().size() > 1);
}
}
/**
* Called when the device config selection changes.
*/
private void onDeviceConfigChange() {
// because changing the content of a combo triggers a change event, respect the
// mDisableUpdates flag
if (mDisableUpdates > 0) {
return;
}
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
mListener.onDevicePostChange();
}
}
/**
* Call back for language combo selection
*/
private void onLocaleChange() {
// because mLocaleList triggers onLocaleChange at each modification, the filling
// of the combo with data will trigger notifications, and we don't want that.
if (mDisableUpdates > 0) {
return;
}
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
}
// Store locale project-wide setting
saveRenderState();
}
private void onDockChange() {
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
}
}
private void onDayChange() {
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
}
}
/**
* Call back for api level combo selection
*/
private void onRenderingTargetChange() {
// because mApiCombo triggers onApiLevelChange at each modification, the filling
// of the combo with data will trigger notifications, and we don't want that.
if (mDisableUpdates > 0) {
return;
}
// tell the listener a new rendering target is being set. Need to do this before updating
// mRenderingTarget.
if (mListener != null && mRenderingTarget != null) {
mListener.onRenderingTargetPreChange(mRenderingTarget);
}
int index = mTargetCombo.getSelectionIndex();
mRenderingTarget = mTargetList.get(index);
boolean computeOk = computeCurrentConfig();
// force a theme update to reflect the new rendering target.
// This must be done after computeCurrentConfig since it'll depend on the currentConfig
// to figure out the theme list.
updateThemes();
// since the state is saved in computeCurrentConfig, we need to resave it since theme
// change could have impacted it.
saveState();
if (mListener != null && mRenderingTarget != null) {
mListener.onRenderingTargetPostChange(mRenderingTarget);
}
if (computeOk && mListener != null) {
mListener.onConfigurationChange();
}
// Store project-wide render-target setting
saveRenderState();
}
/**
* Saves the current state and the current configuration
*
* @see #saveState()
*/
private boolean computeCurrentConfig() {
saveState();
if (mState.device != null) {
// get the device config from the device/config combos.
int configIndex = mDeviceConfigCombo.getSelectionIndex();
String name = mDeviceConfigCombo.getItem(configIndex);
FolderConfiguration config = mState.device.getFolderConfigByName(name);
// replace the config with the one from the device
mCurrentConfig.set(config);
// replace the locale qualifiers with the one coming from the locale combo
int index = mLocaleCombo.getSelectionIndex();
if (index != -1) {
ResourceQualifier[] localeQualifiers = mLocaleList.get(index);
mCurrentConfig.setLanguageQualifier(
(LanguageQualifier)localeQualifiers[LOCALE_LANG]);
mCurrentConfig.setRegionQualifier(
(RegionQualifier)localeQualifiers[LOCALE_REGION]);
}
index = mDockCombo.getSelectionIndex();
if (index == -1) {
index = 0; // no selection = 0
}
mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index)));
index = mNightCombo.getSelectionIndex();
if (index == -1) {
index = 0; // no selection = 0
}
mCurrentConfig.setNightModeQualifier(
new NightModeQualifier(NightMode.getByIndex(index)));
// replace the API level by the selection of the combo
index = mTargetCombo.getSelectionIndex();
if (index == -1) {
index = mTargetList.indexOf(mProjectTarget);
}
if (index != -1) {
IAndroidTarget target = mTargetList.get(index);
if (target != null) {
mCurrentConfig.setVersionQualifier(
new VersionQualifier(target.getVersion().getApiLevel()));
}
}
// update the create button.
checkCreateEnable();
return true;
}
return false;
}
private void onThemeChange() {
saveState();
int themeIndex = mThemeCombo.getSelectionIndex();
if (themeIndex != -1) {
String theme = mThemeCombo.getItem(themeIndex);
if (theme.equals(THEME_SEPARATOR)) {
mThemeCombo.select(0);
}
if (mListener != null) {
mListener.onThemeChange();
}
}
}
/**
* Returns whether the given <var>style</var> is a theme.
* This is done by making sure the parent is a theme.
* @param value the style to check
* @param styleMap the map of styles for the current project. Key is the style name.
* @return True if the given <var>style</var> is a theme.
*/
private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap) {
if (value instanceof StyleResourceValue) {
StyleResourceValue style = (StyleResourceValue)value;
boolean frameworkStyle = false;
String parentStyle = style.getParentStyle();
if (parentStyle == null) {
// if there is no specified parent style we look an implied one.
// For instance 'Theme.light' is implied child style of 'Theme',
// and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
String name = style.getName();
int index = name.lastIndexOf('.');
if (index != -1) {
parentStyle = name.substring(0, index);
}
} else {
// remove the useless @ if it's there
if (parentStyle.startsWith("@")) {
parentStyle = parentStyle.substring(1);
}
// check for framework identifier.
if (parentStyle.startsWith("android:")) {
frameworkStyle = true;
parentStyle = parentStyle.substring("android:".length());
}
// at this point we could have the format style/<name>. we want only the name
if (parentStyle.startsWith("style/")) {
parentStyle = parentStyle.substring("style/".length());
}
}
if (parentStyle != null) {
if (frameworkStyle) {
// if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
} else {
// if it's a project style, we check this is a theme.
value = styleMap.get(parentStyle);
if (value != null) {
return isTheme(value, styleMap);
}
}
}
}
return false;
}
private void checkCreateEnable() {
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
}
/**
* Checks whether the current edited file is the best match for a given config.
* <p/>
* This tests against other versions of the same layout in the project.
* <p/>
* The given config must be compatible with the current edited file.
* @param config the config to test.
* @return true if the current edited file is the best match in the project for the
* given config.
*/
private boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
ResourceFolderType.LAYOUT, config);
if (match != null) {
return match.getFile().equals(mEditedFile);
} else {
// if we stop here that means the current file is not even a match!
AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
}
return false;
}
/**
* Resets the configuration chooser to reflect the given file configuration. This is
* intended to be used by the "Show Included In" functionality where the user has
* picked a non-default configuration (such as a particular landscape layout) and the
* configuration chooser must be switched to a landscape layout. This method will
* trigger a model change.
* <p>
* This will NOT trigger a redraw event!
* <p>
* FIXME: We are currently setting the configuration file to be the configuration for
* the "outer" (the including) file, rather than the inner file, which is the file the
* user is actually editing. We need to refine this, possibly with a way for the user
* to choose which configuration they are editing. And in particular, we should be
* filtering the configuration chooser to only show options in the outer configuration
* that are compatible with the inner included file.
*
* @param file the file to be configured
*/
public void resetConfigFor(IFile file) {
setFile(file);
mEditedConfig = null;
onXmlModelLoaded();
}
/**
* Syncs this configuration to the project wide locale and render target settings. The
* locale may ignore the project-wide setting if it is a locale-specific
* configuration.
*
* @return true if one or both of the toggles were changed, false if there were no
* changes
*/
public boolean syncRenderState() {
if (mEditedConfig == null) {
// Startup; ignore
return false;
}
boolean localeChanged = false;
boolean renderTargetChanged = false;
// When a page is re-activated, force the toggles to reflect the current project
// state
Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState();
// Only sync the locale if this layout is not already a locale-specific layout!
if (!isLocaleSpecificLayout()) {
ResourceQualifier[] locale = pair.getFirst();
if (locale != null) {
localeChanged = setLocaleCombo(locale[0], locale[1]);
}
}
// Sync render target
IAndroidTarget target = pair.getSecond();
if (target != null) {
int targetIndex = mTargetList.indexOf(target);
if (targetIndex != mTargetCombo.getSelectionIndex()) {
mTargetCombo.select(targetIndex);
renderTargetChanged = true;
}
}
if (!renderTargetChanged && !localeChanged) {
return false;
}
// Update the locale and/or the render target. This code contains a logical
// merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
// such that we don't duplicate work.
if (renderTargetChanged) {
if (mListener != null && mRenderingTarget != null) {
mListener.onRenderingTargetPreChange(mRenderingTarget);
}
int targetIndex = mTargetCombo.getSelectionIndex();
mRenderingTarget = mTargetList.get(targetIndex);
}
// Compute the new configuration; we want to do this both for locale changes
// and for render targets.
boolean computeOk = computeCurrentConfig();
if (renderTargetChanged) {
// force a theme update to reflect the new rendering target.
// This must be done after computeCurrentConfig since it'll depend on the currentConfig
// to figure out the theme list.
updateThemes();
if (mListener != null && mRenderingTarget != null) {
mListener.onRenderingTargetPostChange(mRenderingTarget);
}
}
// For both locale and render target changes
if (computeOk && mListener != null) {
mListener.onConfigurationChange();
}
return true;
}
/**
* Loads the render state (the locale and the render target, which are shared among
* all the layouts meaning that changing it in one will change it in all) and returns
* the current project-wide locale and render target to be used.
*
* @return a pair of locale resource qualifiers and render target
*/
private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() {
IProject project = mEditedFile.getProject();
try {
String data = project.getPersistentProperty(NAME_RENDER_STATE);
if (data != null) {
ResourceQualifier[] locale = null;
IAndroidTarget target = null;
String[] values = data.split(SEP);
if (values.length == 2) {
locale = new ResourceQualifier[2];
String locales[] = values[0].split(SEP_LOCALE);
if (locales.length >= 2) {
if (locales[0].length() > 0) {
locale[0] = new LanguageQualifier(locales[0]);
}
if (locales[1].length() > 0) {
locale[1] = new RegionQualifier(locales[1]);
}
}
target = stringToTarget(values[1]);
}
return Pair.of(locale, target);
}
ResourceQualifier[] any = new ResourceQualifier[] {
new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE),
new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)
};
return Pair.of(any, findDefaultRenderTarget());
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
return null;
}
/** Returns true if the current layout is locale-specific */
private boolean isLocaleSpecificLayout() {
return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null;
}
/**
* Saves the render state (the current locale and render target settings) into the
* project wide settings storage
*/
private void saveRenderState() {
IProject project = mEditedFile.getProject();
try {
int index = mLocaleCombo.getSelectionIndex();
ResourceQualifier[] locale = mLocaleList.get(index);
index = mTargetCombo.getSelectionIndex();
IAndroidTarget target = mTargetList.get(index);
// Generate a persistent string from locale+target
StringBuilder sb = new StringBuilder();
if (locale != null) {
if (locale[0] != null && locale[1] != null) {
// locale[0]/[1] can be null sometimes when starting Eclipse
sb.append(((LanguageQualifier) locale[0]).getValue());
sb.append(SEP_LOCALE);
sb.append(((RegionQualifier) locale[1]).getValue());
}
}
sb.append(SEP);
if (target != null) {
sb.append(targetToString(target));
sb.append(SEP);
}
String data = sb.toString();
project.setPersistentProperty(NAME_RENDER_STATE, data);
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
}
}