/*
* 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.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.resources.ResourceType;
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.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.PixelDensityQualifier.Density;
import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenOrientationQualifier.ScreenOrientation;
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.LoadStatus;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.LanguageRegionVerifier;
import com.android.layoutlib.api.IResourceValue;
import com.android.layoutlib.api.IStyleResourceValue;
import com.android.sdklib.IAndroidTarget;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IStatus;
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.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
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.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.Map.Entry;
/**
* A composite that displays the current configuration displayed in a Graphical Layout Editor.
* <p/>
* The composite has several entry points:<br>
* - {@link #openFile(FolderConfiguration, IAndroidTarget)}<br>
* Called after the creation to init the composite with a file being opened in a new editor.<br>
*<br>
* - {@link #replaceFile(FolderConfiguration)}<br>
* Called when a file, representing the same resource but with a different config is opened<br>
* by the user.<br>
*<br>
* - {@link #changeFileOnNewConfig(FolderConfiguration)}<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 THEME_SEPARATOR = "----------"; //$NON-NLS-1$
private final static String FAKE_LOCALE_VALUE = "__"; //$NON-NLS-1$
private final static int LOCALE_LANG = 0;
private final static int LOCALE_REGION = 1;
private Button mClippingButton;
private Label mCurrentLayoutLabel;
private Combo mDeviceCombo;
private Combo mDeviceConfigCombo;
private Combo mLocaleCombo;
private Combo mThemeCombo;
private Button mCreateButton;
private int mPlatformThemeCount = 0;
private boolean mDisableUpdates = false;
private List<LayoutDevice> mDeviceList;
private final ArrayList<ResourceQualifier[] > mLocaleList =
new ArrayList<ResourceQualifier[]>();
/**
* clipping value. If true, the rendering is limited to the screensize. This is the default
* value
*/
private boolean mClipping = true;
/**
* TODO: remove as it's saved in mCurrentStave. Just need to make sure there's no NPE when mCurrentState is null.
*/
private LayoutDevice mCurrentDevice;
private SelectionState mCurrentState = null;
private boolean mSdkChanged = false;
/** 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 mTarget;
/** The {@link FolderConfiguration} being edited. */
private FolderConfiguration mEditedConfig;
/**
* 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 {
void onConfigurationChange();
void onThemeChange();
void onCreate();
void onClippingChange();
ProjectResources getProjectResources();
ProjectResources getFrameworkResources();
Map<String, Map<String, IResourceValue>> getConfiguredProjectResources();
Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources();
}
/**
* State of the config selection. This is used during UI reset to attempt to return the
* rendering to its original configuration.
*/
private static class SelectionState {
String deviceName;
String configName;
ResourceQualifier[] locale;
String theme;
}
/**
* Interface implemented by the part which owns a {@link ConfigurationComposite}
* to define and handle custom toggle buttons in the button bar. Each toggle is
* implemented using a button, with a callback when the button is selected.
*/
public static abstract class CustomToggle {
/** The UI label of the toggle. Can be null if the image exists. */
private final String mUiLabel;
/** The image to use for this toggle. Can be null if the label exists. */
private final Image mImage;
/** The tooltip for the toggle. Can be null. */
private final String mUiTooltip;
/**
* Initializes a new {@link CustomToggle}. The values set here will be used
* later to create the actual toggle.
*
* @param uiLabel The UI label of the toggle. Can be null if the image exists.
* @param image The image to use for this toggle. Can be null if the label exists.
* @param uiTooltip The tooltip for the toggle. Can be null.
*/
public CustomToggle(
String uiLabel,
Image image,
String uiTooltip) {
mUiLabel = uiLabel;
mImage = image;
mUiTooltip = uiTooltip;
}
/** Called by the {@link ConfigurationComposite} when the button is selected. */
public abstract void onSelected(boolean newState);
private void createToggle(Composite parent) {
final Button b = new Button(parent, SWT.TOGGLE | SWT.FLAT);
if (mUiTooltip != null) {
b.setToolTipText(mUiTooltip);
}
if (mImage != null) {
b.setImage(mImage);
}
if (mUiLabel != null) {
b.setText(mUiLabel);
}
b.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onSelected(b.getSelection());
}
});
}
}
/**
* Creates a new {@link ConfigurationComposite} and adds it to the parent.
*
* @param listener An {@link IConfigListener} that gets and sets configuration properties.
* Mandatory, cannot be null.
* @param customToggles An array of {@link CustomToggle} to define extra toggles button
* to display at the top of the composite. Can be empty or null.
* @param parent The parent composite.
* @param style The style of this composite.
*/
public ConfigurationComposite(IConfigListener listener,
CustomToggle[] customToggles,
Composite parent, int style) {
super(parent, style);
mListener = listener;
if (customToggles == null) {
customToggles = new CustomToggle[0];
}
GridLayout gl;
GridData gd;
int cols = 10; // device*2+config*2+locale*2+separator*2+theme+createBtn
// ---- First line: custom buttons, clipping button, editing config display.
Composite labelParent = new Composite(this, SWT.NONE);
labelParent.setLayout(gl = new GridLayout(3 + customToggles.length, false));
gl.marginWidth = gl.marginHeight = 0;
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;
for (CustomToggle toggle : customToggles) {
toggle.createToggle(labelParent);
}
mClippingButton = new Button(labelParent, SWT.TOGGLE | SWT.FLAT);
mClippingButton.setSelection(mClipping);
mClippingButton.setToolTipText("Toggles screen clipping on/off");
mClippingButton.setImage(IconFactory.getInstance().getIcon("clipping")); //$NON-NLS-1$
mClippingButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onClippingChange();
}
});
// ---- 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;
new Label(this, SWT.NONE).setText("Devices");
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*/);
}
});
new Label(this, SWT.NONE).setText("Config");
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();
}
});
new Label(this, SWT.NONE).setText("Locale");
mLocaleCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
mLocaleCombo.setLayoutData(new GridData(
GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
mLocaleCombo.addVerifyListener(new LanguageRegionVerifier());
mLocaleCombo.addSelectionListener(new SelectionListener() {
public void widgetDefaultSelected(SelectionEvent e) {
onLocaleChange();
}
public void widgetSelected(SelectionEvent e) {
onLocaleChange();
}
});
// 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;
mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN);
mThemeCombo.setEnabled(false);
mThemeCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onThemeChange();
}
});
// second separator
separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL);
separator.setLayoutData(gd = new GridData(
GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
gd.heightHint = 0;
mCreateButton = new Button(this, 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();
}
}
});
}
// ---- Init and reset/reload methods ----
/**
* Init the UI with a given file configuration and project target. This must only be called
* the first time the {@link ConfigurationComposite} is created.
* <p/>This will NOT trigger a redraw event (will not call
* {@link IConfigListener#onConfigurationChange()}.)
* The state of the selection of the various combos will be initialized to default values that
* are compatible with the opened file.
*
* @param file the file being opened
*
* @see #replaceFile(FolderConfiguration)
* @see #changeFileOnNewConfig(FolderConfiguration)
*/
public void openFile(IFile file) {
mEditedFile = file;
IProject iProject = mEditedFile.getProject();
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
// only attempt to do anything if the SDK and targets are loaded.
LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
if (sdkStatus == LoadStatus.LOADED) {
// init the devices since the SDK is loaded.
initDevices();
Sdk currentSdk = Sdk.getCurrent();
if (currentSdk != null) {
mTarget = currentSdk.getTarget(iProject);
}
LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null);
if (targetStatus == LoadStatus.LOADED) {
mResources = ResourceManager.getInstance().getProjectResources(iProject);
ResourceFolder resFolder = mResources.getResourceFolder((IFolder)file.getParent());
mEditedConfig = resFolder.getConfiguration();
// update the themes and locales.
updateThemes();
updateLocales();
// update the clipping state
AndroidTargetData data = Sdk.getCurrent().getTargetData(mTarget);
if (data != null) {
LayoutBridge bridge = data.getLayoutBridge();
setClippingSupport(bridge.apiLevel >= 4);
}
// attempt to find a device/locale that can display this particular config.
findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
// compute the final current config
computeCurrentConfig();
// update the string showing the config value
updateConfigDisplay(mEditedConfig);
saveState();
}
}
mDisableUpdates = false;
}
/**
* 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.
* <p/>This will NOT trigger a redraw event (will not call
* {@link IConfigListener#onConfigurationChange()}.)
* @param file the file being opened.
* @param fileConfig The {@link FolderConfiguration} of the opened file.
* @param target the {@link IAndroidTarget} of the file's project.
*
* @see #replaceFile(FolderConfiguration)
*/
public void replaceFile(IFile file) {
// if there is no previous selection, revert to default mode.
if (mCurrentDevice == null) {
openFile(file);
return;
}
mEditedFile = file;
IProject iProject = mEditedFile.getProject();
mResources = ResourceManager.getInstance().getProjectResources(iProject);
ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
mEditedConfig = resFolder.getConfiguration();
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
// 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(mTarget, null);
if (targetStatus == LoadStatus.LOADED) {
// update the current config selection to make sure it's
// compatible with the new file
adaptConfigSelection();
// compute the final current config
computeCurrentConfig();
// update the string showing the config value
updateConfigDisplay(mEditedConfig);
saveState();
}
}
mDisableUpdates = false;
}
/**
* Updates the UI with a new file that was opened in response to a config change.
* @param file the file being opened.
*
* @see #openFile(FolderConfiguration, IAndroidTarget)
* @see #replaceFile(FolderConfiguration)
*/
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.
mTarget = target;
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
// this is going to be followed by a call to onTargetLoaded.
// So we can only care about the layout devices in this case.
initDevices();
mDisableUpdates = false;
}
/**
* Responds to the event that the {@link IAndroidTarget} data was loaded, or the project's
* target changed
*/
public void onTargetChange() {
if (mCurrentState == null) {
// this means the file was opened before the target finished loaded.
// This is basically an initial call to openFile that's delayed.
openFile(mEditedFile);
return;
}
// update the resource and config if they are not present
if (mResources == null) {
mResources = ResourceManager.getInstance().getProjectResources(
mEditedFile.getProject());
}
if (mEditedConfig == null) {
ResourceFolder resFolder = mResources.getResourceFolder(
(IFolder)mEditedFile.getParent());
mEditedConfig = resFolder.getConfiguration();
}
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting
// new values in the widgets.
// update the themes. The locales need not be updated as they are stricly based on the
// content of the project
updateThemes();
// at this point, this means the target of the project was changed, or the whole SDK
// was changed reloaded (different location?).
// The former means the devices/configs are still there, the latter means they've
// been reloaded (in #onSdkLoaded).
if (mSdkChanged) {
findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
} else {
adaptConfigSelection();
}
// update the clipping state
AndroidTargetData data = Sdk.getCurrent().getTargetData(mTarget);
if (data != null) {
LayoutBridge bridge = data.getLayoutBridge();
setClippingSupport(bridge.apiLevel >= 4);
}
computeCurrentConfig();
mDisableUpdates = false;
}
/**
* 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;
int anyLocaleIndex = -1;
LayoutDevice bestDeviceMatch = null; // an actual best match
String bestConfigMatchName = null;
int bestLocaleIndex = -1;
FolderConfiguration testConfig = new FolderConfiguration();
mainloop: for (LayoutDevice device : mDeviceList) {
for (Entry<String, FolderConfiguration> entry :
device.getConfigs().entrySet()) {
testConfig.set(entry.getValue());
// look 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)) {
// 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 = entry.getKey();
anyLocaleIndex = i;
}
if (isCurrentFileBestMatchFor(testConfig)) {
// this is what we want.
bestDeviceMatch = device;
bestConfigMatchName = entry.getKey();
bestLocaleIndex = i;
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(mCurrentDevice = anyDeviceMatch);
fillConfigCombo(anyConfigMatchName);
mLocaleCombo.select(anyLocaleIndex);
// 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'",
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(mCurrentDevice = bestDeviceMatch);
fillConfigCombo(bestConfigMatchName);
mLocaleCombo.select(bestLocaleIndex);
}
}
/**
* 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() {
// 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 = mCurrentDevice.getConfigs().get(configName);
if (mEditedConfig.isMatchFor(currentConfig)) {
currentConfigIsCompatible = true; // current config is compatible
if (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;
Map<String, FolderConfiguration> configs = mCurrentDevice.getConfigs();
mainloop: for (Entry<String, FolderConfiguration> entry : configs.entrySet()) {
testConfig.set(entry.getValue());
// 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 = entry.getKey();
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.
*/
private void setLocaleCombo(ResourceQualifier language, ResourceQualifier region) {
// 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 (FAKE_LOCALE_VALUE.equals(
((RegionQualifier)locale[LOCALE_REGION]).getValue())) {
// match!
mLocaleCombo.select(i);
break;
}
} else if (region.equals(locale[LOCALE_REGION])) {
// match!
mLocaleCombo.select(i);
break;
}
}
}
}
private void updateConfigDisplay(FolderConfiguration fileConfig) {
String current = fileConfig.toDisplayString();
mCurrentLayoutLabel.setText(current != null ? current : "(Default)");
}
private void saveState() {
if (mCurrentDevice != null) {
if (mCurrentState == null) {
mCurrentState = new SelectionState();
}
mCurrentState.deviceName = mCurrentDevice.getName();
int index = mDeviceConfigCombo.getSelectionIndex();
if (index != -1) {
mCurrentState.configName = mDeviceConfigCombo.getItem(index);
} else {
mCurrentState.configName = null;
}
// since the locales are relative to the project, only keeping the index is enough
index = mLocaleCombo.getSelectionIndex();
if (index != -1) {
mCurrentState.locale = mLocaleList.get(index);
} else {
mCurrentState.locale = null;
}
index = mThemeCombo.getSelectionIndex();
if (index != -1) {
mCurrentState.theme = mThemeCombo.getItem(index);
}
}
}
/**
* 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 = true;
// 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(FAKE_LOCALE_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");
}
// create language/region qualifier that will never be matched by qualified resources.
mLocaleList.add(new ResourceQualifier[] {
new LanguageQualifier(FAKE_LOCALE_VALUE),
new RegionQualifier(FAKE_LOCALE_VALUE)
});
if (mCurrentState != null && mCurrentState.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(mCurrentState.locale[LOCALE_LANG],
mCurrentState.locale[LOCALE_REGION]);
} else {
mLocaleCombo.select(0);
}
mThemeCombo.getParent().layout();
mDisableUpdates = false;
}
/**
* 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();
mDisableUpdates = true;
// 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<String, Map<String, IResourceValue>> frameworResources =
mListener.getConfiguredFrameworkResources();
if (frameworResources != null) {
// get the styles.
Map<String, IResourceValue> styles = frameworResources.get(
ResourceType.STYLE.getName());
// collect the themes out of all the styles.
for (IResourceValue 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<String, Map<String, IResourceValue>> configuredProjectRes =
mListener.getConfiguredProjectResources();
if (configuredProjectRes != null) {
// get the styles.
Map<String, IResourceValue> styleMap = configuredProjectRes.get(
ResourceType.STYLE.getName());
if (styleMap != null) {
// collect the themes out of all the styles, ie styles that extend,
// directly or indirectly a platform theme.
for (IResourceValue 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.
if (mCurrentState != null && mCurrentState.theme != null) {
final int count = mThemeCombo.getItemCount();
for (int i = 0 ; i < count ; i++) {
if (mCurrentState.theme.equals(mThemeCombo.getItem(i))) {
mThemeCombo.select(i);
break;
}
}
mThemeCombo.setEnabled(true);
} else if (mThemeCombo.getItemCount() > 0) {
mThemeCombo.select(0);
mThemeCombo.setEnabled(true);
} else {
mThemeCombo.setEnabled(false);
}
mThemeCombo.getParent().layout();
mDisableUpdates = false;
}
// ---- 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 (mCurrentDevice != null) {
float dpi = mCurrentDevice.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 (mCurrentDevice != null) {
float dpi = mCurrentDevice.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.
*/
public String getTheme() {
int themeIndex = mThemeCombo.getSelectionIndex();
if (themeIndex != -1) {
return mThemeCombo.getItem(themeIndex);
}
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 boolean getClipping() {
return mClipping;
}
private void setClippingSupport(boolean b) {
mClippingButton.setEnabled(b);
if (b) {
mClippingButton.setToolTipText("Toggles screen clipping on/off");
} else {
mClipping = true;
mClippingButton.setSelection(true);
mClippingButton.setToolTipText("Non clipped rendering is not supported");
}
}
/**
* 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) {
Map<String, FolderConfiguration> configs = mDeviceList.get(0).getConfigs();
Set<String> configNames = configs.keySet();
for (String name : configNames) {
mDeviceConfigCombo.add(name);
}
mDeviceConfigCombo.select(0);
if (configNames.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 == true) {
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 (mCurrentDevice != null) {
int index = mDeviceConfigCombo.getSelectionIndex();
if (index != -1) {
FolderConfiguration oldConfig = mCurrentDevice.getConfigs().get(
mDeviceConfigCombo.getItem(index));
LayoutDevice newDevice = mDeviceList.get(deviceIndex);
newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs());
}
}
mCurrentDevice = mDeviceList.get(deviceIndex);
} else {
mCurrentDevice = 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 = true;
LayoutDevice oldCurrent = mCurrentDevice;
// but first, update the device combo
initDevices();
// attempts to reselect the current device.
if (selectDevice(oldCurrent)) {
// current device still exists.
// reselect the config
selectConfig(mCurrentState.configName);
// reset the UI as if it was just a replacement file, since we can keep
// the current device (and possibly config).
adaptConfigSelection();
} else {
// find a new device/config to match the current file.
findAndSetCompatibleConfig(false /*favorCurrentConfig*/);
}
mDisableUpdates = false;
// 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,
Map<String, FolderConfiguration> configs) {
// create 2 lists as we're going to go through one and put the candidates in the other.
ArrayList<Entry<String, FolderConfiguration>> list1 =
new ArrayList<Entry<String,FolderConfiguration>>();
ArrayList<Entry<String, FolderConfiguration>> list2 =
new ArrayList<Entry<String,FolderConfiguration>>();
list1.addAll(configs.entrySet());
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 (Entry<String, FolderConfiguration> entry : list1) {
ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
FolderConfiguration config = entry.getValue();
ResourceQualifier newQualifier = config.getQualifier(i);
if (oldQualifier == null) {
if (newQualifier == null) {
list2.add(entry);
}
} else if (oldQualifier.equals(newQualifier)) {
list2.add(entry);
}
}
// at any moment if the new candidate list contains only one match, its name
// is returned.
if (list2.size() == 1) {
return list2.get(0).getKey();
}
// 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).getKey();
}
return null;
}
/**
* fills the config combo with new values based on {@link #mCurrentDevice}.
* @param refName an optional name. if set the selection will match this name (if found)
*/
private void fillConfigCombo(String refName) {
mDeviceConfigCombo.removeAll();
if (mCurrentDevice != null) {
Set<String> configNames = mCurrentDevice.getConfigs().keySet();
int selectionIndex = 0;
int i = 0;
for (String name : configNames) {
mDeviceConfigCombo.add(name);
if (name.equals(refName)) {
selectionIndex = i;
}
i++;
}
mDeviceConfigCombo.select(selectionIndex);
mDeviceConfigCombo.setEnabled(configNames.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 == true) {
return;
}
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
}
}
/**
* Call back for language combo selection
*/
private void onLocaleChange() {
// because mLanguage triggers onLanguageChange at each modification, the filling
// of the combo with data will trigger notifications, and we don't want that.
if (mDisableUpdates == true) {
return;
}
if (computeCurrentConfig() && mListener != null) {
mListener.onConfigurationChange();
}
}
/**
* Saves the current state and the current configuration
*/
private boolean computeCurrentConfig() {
saveState();
if (mCurrentDevice != null) {
// get the device config from the device/config combos.
int configIndex = mDeviceConfigCombo.getSelectionIndex();
String name = mDeviceConfigCombo.getItem(configIndex);
FolderConfiguration config = mCurrentDevice.getConfigs().get(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 localeIndex = mLocaleCombo.getSelectionIndex();
if (localeIndex != -1) {
ResourceQualifier[] localeQualifiers = mLocaleList.get(localeIndex);
mCurrentConfig.setLanguageQualifier(
(LanguageQualifier)localeQualifiers[LOCALE_LANG]);
mCurrentConfig.setRegionQualifier(
(RegionQualifier)localeQualifiers[LOCALE_REGION]);
}
// update the create button.
checkCreateEnable();
return true;
}
return false;
}
private void onThemeChange() {
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();
}
}
saveState();
}
private void onClippingChange() {
mClipping = mClippingButton.getSelection();
if (mListener != null) {
mListener.onClippingChange();
}
}
/**
* 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(IResourceValue value, Map<String, IResourceValue> styleMap) {
if (value instanceof IStyleResourceValue) {
IStyleResourceValue style = (IStyleResourceValue)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;
}
}