aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java162
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java1091
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java2096
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java395
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java843
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java290
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java199
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java149
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java184
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java506
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java180
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java126
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java318
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java509
18 files changed, 7566 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java
new file mode 100644
index 000000000..36cd0fbbb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jdt.ui.ISharedImages;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@linkplain ActivityMenuListener} class is responsible for
+ * generating the activity menu in the {@link ConfigurationChooser}.
+ */
+class ActivityMenuListener extends SelectionAdapter {
+ private static final int ACTION_OPEN_ACTIVITY = 1;
+ private static final int ACTION_SELECT_ACTIVITY = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final String mFqcn;
+
+ ActivityMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable String fqcn) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mFqcn = fqcn;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_OPEN_ACTIVITY: {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ String fqcn = configuration.getActivity();
+ AdtPlugin.openJavaClass(mConfigChooser.getProject(), fqcn);
+ break;
+ }
+ case ACTION_SELECT_ACTIVITY: {
+ mConfigChooser.selectActivity(mFqcn);
+ mConfigChooser.onSelectActivity();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ // TODO: Allow using fragments here as well?
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ ISharedImages sharedImages = JavaUI.getSharedImages();
+ Configuration configuration = chooser.getConfiguration();
+ String current = configuration.getActivity();
+
+ if (current != null) {
+ MenuItem item = new MenuItem(menu, SWT.PUSH);
+ String label = ConfigurationChooser.getActivityLabel(current, true);
+ item.setText( String.format("Open %1$s...", label));
+ Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CUNIT);
+ item.setImage(image);
+ item.addSelectionListener(
+ new ActivityMenuListener(chooser, ACTION_OPEN_ACTIVITY, null));
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ IProject project = chooser.getProject();
+ Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CLASS);
+
+ // Add activities found to be relevant to this layout
+ String layoutName = ResourceHelper.getLayoutName(chooser.getEditedFile());
+ String pkg = ManifestInfo.get(project).getPackage();
+ List<String> preferred = ManifestInfo.guessActivities(project, layoutName, pkg);
+ current = addActivities(chooser, menu, current, image, preferred);
+
+ // Add all activities
+ List<String> activities = ManifestInfo.getProjectActivities(project);
+ if (preferred.size() > 0) {
+ // Filter out the activities we've already listed above
+ List<String> filtered = new ArrayList<String>(activities.size());
+ Set<String> remove = new HashSet<String>(preferred);
+ for (String fqcn : activities) {
+ if (!remove.contains(fqcn)) {
+ filtered.add(fqcn);
+ }
+ }
+ activities = filtered;
+ }
+
+ if (activities.size() > 0) {
+ if (preferred.size() > 0) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ addActivities(chooser, menu, current, image, activities);
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ private static String addActivities(ConfigurationChooser chooser, Menu menu, String current,
+ Image image, List<String> activities) {
+ for (final String fqcn : activities) {
+ String title = ConfigurationChooser.getActivityLabel(fqcn, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ item.setImage(image);
+
+ boolean selected = title.equals(current);
+ if (selected) {
+ item.setSelection(true);
+ current = null; // Only show the first occurrence as selected
+ // such that we don't show it selected again in the full activity list
+ }
+
+ item.addSelectionListener(new ActivityMenuListener(chooser,
+ ACTION_SELECT_ACTIVITY, fqcn));
+ }
+
+ return current;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java
new file mode 100644
index 000000000..c4253cddf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.NightModeQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.common.resources.configuration.UiModeQualifier;
+import com.android.ide.common.resources.configuration.VersionQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+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.Sdk;
+import com.android.resources.Density;
+import com.android.resources.LayoutDirection;
+import com.android.resources.NightMode;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.android.utils.Pair;
+import com.google.common.base.Objects;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+
+import java.util.List;
+
+/**
+ * A {@linkplain Configuration} is a selection of device, orientation, theme,
+ * etc for use when rendering a layout.
+ */
+public class Configuration {
+ /** The {@link FolderConfiguration} in change flags or override flags */
+ public static final int CFG_FOLDER = 1 << 0;
+ /** The {@link Device} in change flags or override flags */
+ public static final int CFG_DEVICE = 1 << 1;
+ /** The {@link State} in change flags or override flags */
+ public static final int CFG_DEVICE_STATE = 1 << 2;
+ /** The theme in change flags or override flags */
+ public static final int CFG_THEME = 1 << 3;
+ /** The locale in change flags or override flags */
+ public static final int CFG_LOCALE = 1 << 4;
+ /** The rendering {@link IAndroidTarget} in change flags or override flags */
+ public static final int CFG_TARGET = 1 << 5;
+ /** The {@link NightMode} in change flags or override flags */
+ public static final int CFG_NIGHT_MODE = 1 << 6;
+ /** The {@link UiMode} in change flags or override flags */
+ public static final int CFG_UI_MODE = 1 << 7;
+ /** The {@link UiMode} in change flags or override flags */
+ public static final int CFG_ACTIVITY = 1 << 8;
+
+ /** References all attributes */
+ public static final int MASK_ALL = 0xFFFF;
+
+ /** Attributes which affect which best-layout-file selection */
+ public static final int MASK_FILE_ATTRS =
+ CFG_DEVICE|CFG_DEVICE_STATE|CFG_LOCALE|CFG_TARGET|CFG_NIGHT_MODE|CFG_UI_MODE;
+
+ /** Attributes which affect rendering appearance */
+ public static final int MASK_RENDERING = MASK_FILE_ATTRS|CFG_THEME;
+
+ /**
+ * 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$
+
+ private final static String MARKER_FRAMEWORK = "-"; //$NON-NLS-1$
+ private final static String MARKER_PROJECT = "+"; //$NON-NLS-1$
+ private final static String SEP = ":"; //$NON-NLS-1$
+ private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
+
+ @NonNull
+ protected ConfigurationChooser mConfigChooser;
+
+ /** The {@link FolderConfiguration} representing the state of the UI controls */
+ @NonNull
+ protected final FolderConfiguration mFullConfig = new FolderConfiguration();
+
+ /** The {@link FolderConfiguration} being edited. */
+ @Nullable
+ protected FolderConfiguration mEditedConfig;
+
+ /** The target of the project of the file being edited. */
+ @Nullable
+ private IAndroidTarget mTarget;
+
+ /** The theme style to render with */
+ @Nullable
+ private String mTheme;
+
+ /** The device to render with */
+ @Nullable
+ private Device mDevice;
+
+ /** The device state */
+ @Nullable
+ private State mState;
+
+ /**
+ * The activity associated with the layout. This is just a cached value of
+ * the true value stored on the layout.
+ */
+ @Nullable
+ private String mActivity;
+
+ /** The locale to use for this configuration */
+ @NonNull
+ private Locale mLocale = Locale.ANY;
+
+ /** UI mode */
+ @NonNull
+ private UiMode mUiMode = UiMode.NORMAL;
+
+ /** Night mode */
+ @NonNull
+ private NightMode mNightMode = NightMode.NOTNIGHT;
+
+ /** The display name */
+ private String mDisplayName;
+
+ /**
+ * Creates a new {@linkplain Configuration}
+ *
+ * @param chooser the associated chooser
+ */
+ protected Configuration(@NonNull ConfigurationChooser chooser) {
+ mConfigChooser = chooser;
+ }
+
+ /**
+ * Sets the associated configuration chooser
+ *
+ * @param chooser the chooser
+ */
+ void setChooser(@NonNull ConfigurationChooser chooser) {
+ // TODO: We should get rid of the binding between configurations
+ // and configuration choosers. This is currently needed because
+ // the choosers contain vital data such as the set of available
+ // rendering targets, the set of available locales etc, which
+ // also doesn't belong inside the configuration but is needed by it.
+ mConfigChooser = chooser;
+ }
+
+ /**
+ * Gets the associated configuration chooser
+ *
+ * @return the chooser
+ */
+ @NonNull
+ ConfigurationChooser getChooser() {
+ return mConfigChooser;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration}
+ *
+ * @param chooser the associated chooser
+ * @return a new configuration
+ */
+ @NonNull
+ public static Configuration create(@NonNull ConfigurationChooser chooser) {
+ return new Configuration(chooser);
+ }
+
+ /**
+ * Creates a configuration suitable for the given file
+ *
+ * @param base the base configuration to base the file configuration off of
+ * @param file the file to look up a configuration for
+ * @return a suitable configuration
+ */
+ @NonNull
+ public static Configuration create(
+ @NonNull Configuration base,
+ @NonNull IFile file) {
+ Configuration configuration = copy(base);
+ ConfigurationChooser chooser = base.getChooser();
+ ProjectResources resources = chooser.getResources();
+ ConfigurationMatcher matcher = new ConfigurationMatcher(chooser, configuration, file,
+ resources, false);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ configuration.mEditedConfig = new FolderConfiguration();
+ configuration.mEditedConfig.set(resFolder.getConfiguration());
+
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ configuration.syncFolderConfig();
+
+ return configuration;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} that is a copy from a different configuration
+ *
+ * @param original the original to copy from
+ * @return a new configuration copied from the original
+ */
+ @NonNull
+ public static Configuration copy(@NonNull Configuration original) {
+ Configuration copy = create(original.mConfigChooser);
+ copy.mFullConfig.set(original.mFullConfig);
+ if (original.mEditedConfig != null) {
+ copy.mEditedConfig = new FolderConfiguration();
+ copy.mEditedConfig.set(original.mEditedConfig);
+ }
+ copy.mTarget = original.getTarget();
+ copy.mTheme = original.getTheme();
+ copy.mDevice = original.getDevice();
+ copy.mState = original.getDeviceState();
+ copy.mActivity = original.getActivity();
+ copy.mLocale = original.getLocale();
+ copy.mUiMode = original.getUiMode();
+ copy.mNightMode = original.getNightMode();
+ copy.mDisplayName = original.getDisplayName();
+
+ return copy;
+ }
+
+ /**
+ * Returns the associated activity
+ *
+ * @return the activity
+ */
+ @Nullable
+ public String getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Returns the chosen device.
+ *
+ * @return the chosen device
+ */
+ @Nullable
+ public Device getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the chosen device state
+ *
+ * @return the device state
+ */
+ @Nullable
+ public State getDeviceState() {
+ return mState;
+ }
+
+ /**
+ * Returns the chosen locale
+ *
+ * @return the locale
+ */
+ @NonNull
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ /**
+ * Returns the UI mode
+ *
+ * @return the UI mode
+ */
+ @NonNull
+ public UiMode getUiMode() {
+ return mUiMode;
+ }
+
+ /**
+ * Returns the day/night mode
+ *
+ * @return the night mode
+ */
+ @NonNull
+ public NightMode getNightMode() {
+ return mNightMode;
+ }
+
+ /**
+ * Returns the current theme style
+ *
+ * @return the theme style
+ */
+ @Nullable
+ public String getTheme() {
+ return mTheme;
+ }
+
+ /**
+ * Returns the rendering target
+ *
+ * @return the target
+ */
+ @Nullable
+ public IAndroidTarget getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * Returns the display name to show for this configuration
+ *
+ * @return the display name, or null if none has been assigned
+ */
+ @Nullable
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Returns whether the configuration's theme is a project theme.
+ * <p/>
+ * The returned value is meaningless if {@link #getTheme()} returns
+ * <code>null</code>.
+ *
+ * @return true for project a theme, false for a framework theme
+ */
+ public boolean isProjectTheme() {
+ String theme = getTheme();
+ if (theme != null) {
+ assert theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
+
+ return ResourceHelper.isProjectStyle(theme);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the current layout is locale-specific
+ *
+ * @return if this configuration represents a locale-specific layout
+ */
+ public boolean isLocaleSpecificLayout() {
+ return mEditedConfig == null || mEditedConfig.getLocaleQualifier() != null;
+ }
+
+ /**
+ * Returns the full, complete {@link FolderConfiguration}
+ *
+ * @return the full configuration
+ */
+ @NonNull
+ public FolderConfiguration getFullConfig() {
+ return mFullConfig;
+ }
+
+ /**
+ * Copies the full, complete {@link FolderConfiguration} into the given
+ * folder config instance.
+ *
+ * @param dest the {@link FolderConfiguration} instance to copy into
+ */
+ public void copyFullConfig(FolderConfiguration dest) {
+ dest.set(mFullConfig);
+ }
+
+ /**
+ * Returns the edited {@link FolderConfiguration} (this is not a full
+ * configuration, so you can think of it as the "constraints" used by the
+ * {@link ConfigurationMatcher} to produce a full configuration.
+ *
+ * @return the constraints configuration
+ */
+ @NonNull
+ public FolderConfiguration getEditedConfig() {
+ return mEditedConfig;
+ }
+
+ /**
+ * Sets the edited {@link FolderConfiguration} (this is not a full
+ * configuration, so you can think of it as the "constraints" used by the
+ * {@link ConfigurationMatcher} to produce a full configuration.
+ *
+ * @param editedConfig the constraints configuration
+ */
+ public void setEditedConfig(@NonNull FolderConfiguration editedConfig) {
+ mEditedConfig = editedConfig;
+ }
+
+ /**
+ * Sets the associated activity
+ *
+ * @param activity the activity
+ */
+ public void setActivity(String activity) {
+ mActivity = activity;
+ }
+
+ /**
+ * Sets the device
+ *
+ * @param device the device
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setDevice(Device device, boolean skipSync) {
+ mDevice = device;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the device state
+ *
+ * @param state the device state
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setDeviceState(State state, boolean skipSync) {
+ mState = state;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the locale
+ *
+ * @param locale the locale
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setLocale(@NonNull Locale locale, boolean skipSync) {
+ mLocale = locale;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the rendering target
+ *
+ * @param target rendering target
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setTarget(IAndroidTarget target, boolean skipSync) {
+ mTarget = target;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the display name to be shown for this configuration.
+ *
+ * @param displayName the new display name
+ */
+ public void setDisplayName(@Nullable String displayName) {
+ mDisplayName = displayName;
+ }
+
+ /**
+ * Sets the night mode
+ *
+ * @param night the night mode
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setNightMode(@NonNull NightMode night, boolean skipSync) {
+ mNightMode = night;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the UI mode
+ *
+ * @param uiMode the UI mode
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) {
+ mUiMode = uiMode;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the theme style
+ *
+ * @param theme the theme
+ */
+ public void setTheme(String theme) {
+ mTheme = theme;
+ checkThemePrefix();
+ }
+
+ /**
+ * Updates the folder configuration such that it reflects changes in
+ * configuration state such as the device orientation, the UI mode, the
+ * rendering target, etc.
+ */
+ public void syncFolderConfig() {
+ Device device = getDevice();
+ if (device == null) {
+ return;
+ }
+
+ // get the device config from the device/state combos.
+ FolderConfiguration config = DeviceConfigHelper.getFolderConfig(getDeviceState());
+
+ // replace the config with the one from the device
+ mFullConfig.set(config);
+
+ // sync the selected locale
+ Locale locale = getLocale();
+ mFullConfig.setLocaleQualifier(locale.qualifier);
+ if (!locale.hasLanguage()) {
+ // Avoid getting the layout library if the locale doesn't have any language.
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.LTR));
+ } else {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(getTarget());
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (layoutLib != null) {
+ if (layoutLib.isRtl(locale.toLocaleId())) {
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.RTL));
+ } else {
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.LTR));
+ }
+ }
+ }
+ }
+ }
+
+ // Replace the UiMode with the selected one, if one is selected
+ UiMode uiMode = getUiMode();
+ if (uiMode != null) {
+ mFullConfig.setUiModeQualifier(new UiModeQualifier(uiMode));
+ }
+
+ // Replace the NightMode with the selected one, if one is selected
+ NightMode nightMode = getNightMode();
+ if (nightMode != null) {
+ mFullConfig.setNightModeQualifier(new NightModeQualifier(nightMode));
+ }
+
+ // replace the API level by the selection of the combo
+ IAndroidTarget target = getTarget();
+ if (target == null && mConfigChooser != null) {
+ target = mConfigChooser.getProjectTarget();
+ }
+ if (target != null) {
+ int apiLevel = target.getVersion().getApiLevel();
+ mFullConfig.setVersionQualifier(new VersionQualifier(apiLevel));
+ }
+ }
+
+ /**
+ * Creates a string suitable for persistence, which can be initialized back
+ * to a configuration via {@link #initialize(String)}
+ *
+ * @return a persistent string
+ */
+ @NonNull
+ public String toPersistentString() {
+ StringBuilder sb = new StringBuilder(32);
+ Device device = getDevice();
+ if (device != null) {
+ sb.append(device.getName());
+ sb.append(SEP);
+ State state = getDeviceState();
+ if (state != null) {
+ sb.append(state.getName());
+ }
+ sb.append(SEP);
+ Locale locale = getLocale();
+ if (isLocaleSpecificLayout() && locale != null && locale.qualifier.hasLanguage()) {
+ // locale[0]/[1] can be null sometimes when starting Eclipse
+ sb.append(locale.qualifier.getLanguage());
+ sb.append(SEP_LOCALE);
+ if (locale.qualifier.hasRegion()) {
+ sb.append(locale.qualifier.getRegion());
+ }
+ }
+ sb.append(SEP);
+ // Need to escape the theme: if we write the full theme style, then
+ // we can end up with ":"'s in the string (as in @android:style/Theme) which
+ // can be mistaken for {@link #SEP}. Instead use {@link #MARKER_FRAMEWORK}.
+ String theme = getTheme();
+ if (theme != null) {
+ String themeName = ResourceHelper.styleToTheme(theme);
+ if (theme.startsWith(STYLE_RESOURCE_PREFIX)) {
+ sb.append(MARKER_PROJECT);
+ } else if (theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
+ sb.append(MARKER_FRAMEWORK);
+ }
+ sb.append(themeName);
+ }
+ sb.append(SEP);
+ UiMode uiMode = getUiMode();
+ if (uiMode != null) {
+ sb.append(uiMode.getResourceValue());
+ }
+ sb.append(SEP);
+ NightMode nightMode = getNightMode();
+ if (nightMode != null) {
+ sb.append(nightMode.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);
+ String activity = getActivity();
+ if (activity != null) {
+ sb.append(activity);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Returns the preferred theme, or null */
+ @Nullable
+ String computePreferredTheme() {
+ IProject project = mConfigChooser.getProject();
+ ManifestInfo manifest = ManifestInfo.get(project);
+
+ // Look up the screen size for the current state
+ ScreenSize screenSize = null;
+ Device device = getDevice();
+ if (device != null) {
+ List<State> states = device.getAllStates();
+ for (State state : states) {
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state);
+ if (folderConfig != null) {
+ ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier();
+ screenSize = qualifier.getValue();
+ break;
+ }
+ }
+ }
+
+ // Look up the default/fallback theme to use for this project (which
+ // depends on the screen size when no particular theme is specified
+ // in the manifest)
+ String defaultTheme = manifest.getDefaultTheme(getTarget(), screenSize);
+
+ String preferred = defaultTheme;
+ if (getTheme() == null) {
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+
+ String activity = getActivity();
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ }
+ if (preferred == null) {
+ preferred = defaultTheme;
+ }
+ setTheme(preferred);
+ }
+
+ return preferred;
+ }
+
+ private void checkThemePrefix() {
+ if (mTheme != null && !mTheme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (mTheme.isEmpty()) {
+ computePreferredTheme();
+ return;
+ }
+ ResourceRepository frameworkRes = mConfigChooser.getClient().getFrameworkResources();
+ if (frameworkRes != null
+ && frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + mTheme)) {
+ mTheme = ANDROID_STYLE_RESOURCE_PREFIX + mTheme;
+ } else {
+ mTheme = STYLE_RESOURCE_PREFIX + mTheme;
+ }
+ }
+ }
+
+ /**
+ * Initializes a string previously created with
+ * {@link #toPersistentString()}
+ *
+ * @param data the string to initialize back from
+ * @return true if the configuration was initialized
+ */
+ boolean initialize(String data) {
+ String[] values = data.split(SEP);
+ if (values.length >= 6 && values.length <= 8) {
+ for (Device d : mConfigChooser.getDevices()) {
+ if (d.getName().equals(values[0])) {
+ mDevice = d;
+ String stateName = null;
+ FolderConfiguration config = null;
+ if (!values[1].isEmpty() && !values[1].equals("null")) { //$NON-NLS-1$
+ stateName = values[1];
+ config = DeviceConfigHelper.getFolderConfig(mDevice, stateName);
+ } else if (mDevice.getAllStates().size() > 0) {
+ State first = mDevice.getAllStates().get(0);
+ stateName = first.getName();
+ config = DeviceConfigHelper.getFolderConfig(first);
+ }
+ mState = getState(mDevice, stateName);
+ if (config != null) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+ LocaleQualifier locale = Locale.ANY_QUALIFIER;
+ String locales[] = values[2].split(SEP_LOCALE);
+ if (locales.length >= 2 && locales[0].length() > 0
+ && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ String region = locales[1];
+ if (region.length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(region)) {
+ locale = LocaleQualifier.getQualifier(language + "-r" + region);
+ } else {
+ locale = new LocaleQualifier(language);
+ }
+ mLocale = Locale.create(locale);
+ }
+
+ // Decode the theme name: See {@link #getData}
+ mTheme = values[3];
+ if (mTheme.startsWith(MARKER_FRAMEWORK)) {
+ mTheme = ANDROID_STYLE_RESOURCE_PREFIX
+ + mTheme.substring(MARKER_FRAMEWORK.length());
+ } else if (mTheme.startsWith(MARKER_PROJECT)) {
+ mTheme = STYLE_RESOURCE_PREFIX
+ + mTheme.substring(MARKER_PROJECT.length());
+ } else {
+ checkThemePrefix();
+ }
+
+ mUiMode = UiMode.getEnum(values[4]);
+ if (mUiMode == null) {
+ mUiMode = UiMode.NORMAL;
+ }
+ mNightMode = NightMode.getEnum(values[5]);
+ if (mNightMode == null) {
+ mNightMode = 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<Locale, IAndroidTarget> pair = loadRenderState(mConfigChooser);
+ if (pair != null) {
+ // We only use the "global" setting
+ if (!isLocaleSpecificLayout()) {
+ mLocale = pair.getFirst();
+ }
+ mTarget = pair.getSecond();
+ }
+
+ if (values.length == 8) {
+ mActivity = values[7];
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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.
+ *
+ * @param chooser the {@link ConfigurationChooser} providing information about
+ * loaded targets
+ * @return a pair of a locale and a render target
+ */
+ @Nullable
+ static Pair<Locale, IAndroidTarget> loadRenderState(ConfigurationChooser chooser) {
+ IProject project = chooser.getProject();
+ if (project == null || !project.isAccessible()) {
+ return null;
+ }
+
+ try {
+ String data = project.getPersistentProperty(NAME_RENDER_STATE);
+ if (data != null) {
+ Locale locale = Locale.ANY;
+ IAndroidTarget target = null;
+
+ String[] values = data.split(SEP);
+ if (values.length == 2) {
+
+ LocaleQualifier qualifier = Locale.ANY_QUALIFIER;
+ String locales[] = values[0].split(SEP_LOCALE);
+ if (locales.length >= 2 && locales[0].length() > 0
+ && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ String region = locales[1];
+ if (region.length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(region)) {
+ locale = Locale.create(LocaleQualifier.getQualifier(language + "-r" + region));
+ } else {
+ locale = Locale.create(new LocaleQualifier(language));
+ }
+ } else {
+ locale = Locale.ANY;
+ }
+ if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) {
+ target = ConfigurationMatcher.findDefaultRenderTarget(chooser);
+ } else {
+ String targetString = values[1];
+ target = stringToTarget(chooser, targetString);
+ // See if we should "correct" the rendering target to a
+ // better version. If you're using a pre-release version
+ // of the render target, and a final release is
+ // available and installed, we should switch to that
+ // one instead.
+ if (target != null) {
+ AndroidVersion version = target.getVersion();
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (version.getCodename() != null && targetList != null) {
+ int targetApiLevel = version.getApiLevel() + 1;
+ for (IAndroidTarget t : targetList) {
+ if (t.getVersion().getApiLevel() == targetApiLevel
+ && t.isPlatform()) {
+ target = t;
+ break;
+ }
+ }
+ }
+ } else {
+ target = ConfigurationMatcher.findDefaultRenderTarget(chooser);
+ }
+ }
+ }
+
+ return Pair.of(locale, target);
+ }
+
+ return Pair.of(Locale.ANY, ConfigurationMatcher.findDefaultRenderTarget(chooser));
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Saves the render state (the current locale and render target settings) into the
+ * project wide settings storage
+ */
+ void saveRenderState() {
+ IProject project = mConfigChooser.getProject();
+ if (project == null) {
+ return;
+ }
+ try {
+ // Generate a persistent string from locale+target
+ StringBuilder sb = new StringBuilder(32);
+ Locale locale = getLocale();
+ if (locale != null) {
+ // locale[0]/[1] can be null sometimes when starting Eclipse
+ sb.append(locale.qualifier.getLanguage());
+ sb.append(SEP_LOCALE);
+ if (locale.qualifier.hasRegion()) {
+ sb.append(locale.qualifier.getRegion());
+ }
+ }
+ sb.append(SEP);
+ IAndroidTarget target = getTarget();
+ if (target != null) {
+ sb.append(targetToString(target));
+ sb.append(SEP);
+ }
+
+ project.setPersistentProperty(NAME_RENDER_STATE, sb.toString());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ /**
+ * 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
+ */
+ @NonNull
+ public static String targetToString(@NonNull 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 chooser the {@link ConfigurationChooser} providing information about
+ * loaded targets
+ * @param id the id that corresponds to the desired platform
+ * @return an {@link IAndroidTarget} that matches the given id, or null
+ */
+ @Nullable
+ public static IAndroidTarget stringToTarget(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull String id) {
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (targetList != null && targetList.size() > 0) {
+ for (IAndroidTarget target : targetList) {
+ if (id.equals(targetToString(target))) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 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
+ */
+ @Nullable
+ public static IAndroidTarget stringToTarget(
+ @NonNull String id) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget[] targets = currentSdk.getTargets();
+ for (IAndroidTarget target : targets) {
+ if (id.equals(targetToString(target))) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link State} by the given name for the given {@link Device}
+ *
+ * @param device the device
+ * @param name the name of the state
+ */
+ @Nullable
+ static State getState(@Nullable Device device, @Nullable String name) {
+ if (device == null) {
+ return null;
+ } else if (name != null) {
+ State state = device.getState(name);
+ if (state != null) {
+ return state;
+ }
+ }
+
+ return device.getDefaultState();
+ }
+
+ /**
+ * Returns the currently selected {@link Density}. This is guaranteed to be non null.
+ *
+ * @return the density
+ */
+ @NonNull
+ public Density getDensity() {
+ if (mFullConfig != null) {
+ DensityQualifier qual = mFullConfig.getDensityQualifier();
+ 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;
+ }
+
+ /**
+ * Get the next cyclical state after the given state
+ *
+ * @param from the state to start with
+ * @return the following state following
+ */
+ @Nullable
+ public State getNextDeviceState(@Nullable State from) {
+ Device device = getDevice();
+ if (device == null) {
+ return null;
+ }
+ List<State> states = device.getAllStates();
+ for (int i = 0; i < states.size(); i++) {
+ if (states.get(i) == from) {
+ return states.get((i + 1) % states.size());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if this configuration supports the given rendering
+ * capability
+ *
+ * @param capability the capability to check
+ * @return true if the capability is supported
+ */
+ public boolean supports(Capability capability) {
+ IAndroidTarget target = getTarget();
+ if (target != null) {
+ return RenderService.supports(target, capability);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this.getClass())
+ .add("display", getDisplayName()) //$NON-NLS-1$
+ .add("persistent", toPersistentString()) //$NON-NLS-1$
+ .toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java
new file mode 100644
index 000000000..009b8646c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java
@@ -0,0 +1,2096 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.RES_QUALIFIER_SEP;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.ide.eclipse.adt.AdtUtils.isUiThread;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_LOCALE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_THEME;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
+import static com.google.common.base.Objects.equal;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.StyleResourceValue;
+import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+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.Sdk;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.DeviceManager.DevicesChangedListener;
+import com.android.sdklib.devices.State;
+import com.android.utils.Pair;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+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.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.IEditorPart;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+
+/**
+ * The {@linkplain ConfigurationChooser} allows the user to pick a
+ * {@link Configuration} by configuring various constraints.
+ */
+public class ConfigurationChooser extends Composite
+ implements DevicesChangedListener, DisposeListener {
+ private static final String ICON_SQUARE = "square"; //$NON-NLS-1$
+ private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$
+ private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$
+ private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$
+ private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$
+ private static final String ICON_DISPLAY = "display"; //$NON-NLS-1$
+ private static final String ICON_THEMES = "themes"; //$NON-NLS-1$
+ private static final String ICON_ACTIVITY = "activity"; //$NON-NLS-1$
+
+ /** The configuration state associated with this editor */
+ private @NonNull Configuration mConfiguration = Configuration.create(this);
+
+ /** Serialized state to use when initializing the configuration after the SDK is loaded */
+ private String mInitialState;
+
+ /** The client of the configuration editor */
+ private final ConfigurationClient mClient;
+
+ /** Counter for programmatic UI changes: if greater than 0, we're within a call */
+ private int mDisableUpdates = 0;
+
+ /** List of available devices */
+ private Collection<Device> mDevices = Collections.emptyList();
+
+ /** List of available targets */
+ private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
+
+ /** List of available themes */
+ private final List<String> mThemeList = new ArrayList<String>();
+
+ /** List of available locales */
+ private final List<Locale > mLocaleList = new ArrayList<Locale>();
+
+ /** 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;
+
+ /** Dropdown for configurations */
+ private ToolItem mConfigCombo;
+
+ /** Dropdown for devices */
+ private ToolItem mDeviceCombo;
+
+ /** Dropdown for device states */
+ private ToolItem mOrientationCombo;
+
+ /** Dropdown for themes */
+ private ToolItem mThemeCombo;
+
+ /** Dropdown for locales */
+ private ToolItem mLocaleCombo;
+
+ /** Dropdown for activities */
+ private ToolItem mActivityCombo;
+
+ /** Dropdown for rendering targets */
+ private ToolItem mTargetCombo;
+
+ /** Whether the SDK has changed since the last model reload; if so we must reload targets */
+ private boolean mSdkChanged = true;
+
+ /**
+ * Creates a new {@linkplain ConfigurationChooser} 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 client the client embedding this configuration chooser
+ * @param parent The parent composite.
+ * @param initialState The initial state (serialized form) to use for the
+ * configuration
+ */
+ public ConfigurationChooser(
+ @NonNull ConfigurationClient client,
+ Composite parent,
+ @Nullable String initialState) {
+ super(parent, SWT.NONE);
+ mClient = client;
+
+ setVisible(false); // Delayed until the targets are loaded
+
+ mInitialState = initialState;
+ setLayout(new GridLayout(1, false));
+
+ IconFactory icons = IconFactory.getInstance();
+
+ // TODO: Consider switching to a CoolBar instead
+ ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+ mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN );
+ mConfigCombo.setImage(icons.getIcon("android_file")); //$NON-NLS-1$
+ mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse");
+
+ @SuppressWarnings("unused")
+ ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY));
+
+ @SuppressWarnings("unused")
+ ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT));
+ mOrientationCombo.setToolTipText("Go to next state");
+
+ @SuppressWarnings("unused")
+ ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mThemeCombo.setImage(icons.getIcon(ICON_THEMES));
+
+ @SuppressWarnings("unused")
+ ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mActivityCombo.setToolTipText("Associated activity or fragment providing context");
+ // The JDT class icon is lopsided, presumably because they've left room in the
+ // bottom right corner for badges (for static, final etc). Unfortunately, this
+ // means that the icon looks out of place when sitting close to the language globe
+ // icon, the theme icon, etc so that it looks vertically misaligned:
+ //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS));
+ // ...so use one that is centered instead:
+ mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY));
+
+ @SuppressWarnings("unused")
+ ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
+ ToolBar rightToolBar = toolBar;
+
+ mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
+ mLocaleCombo.setImage(FlagManager.getGlobeIcon());
+ mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse");
+
+ @SuppressWarnings("unused")
+ ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR);
+
+ mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
+ mTargetCombo.setImage(AdtPlugin.getAndroidLogo());
+ mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse");
+
+ SelectionListener listener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+
+ if (source == mConfigCombo) {
+ ConfigurationMenuListener.show(ConfigurationChooser.this, mConfigCombo);
+ } else if (source == mActivityCombo) {
+ ActivityMenuListener.show(ConfigurationChooser.this, mActivityCombo);
+ } else if (source == mLocaleCombo) {
+ LocaleMenuListener.show(ConfigurationChooser.this, mLocaleCombo);
+ } else if (source == mDeviceCombo) {
+ DeviceMenuListener.show(ConfigurationChooser.this, mDeviceCombo);
+ } else if (source == mTargetCombo) {
+ TargetMenuListener.show(ConfigurationChooser.this, mTargetCombo);
+ } else if (source == mThemeCombo) {
+ ThemeMenuAction.showThemeMenu(ConfigurationChooser.this, mThemeCombo,
+ mThemeList);
+ } else if (source == mOrientationCombo) {
+ if (e.detail == SWT.ARROW) {
+ OrientationMenuAction.showMenu(ConfigurationChooser.this,
+ mOrientationCombo);
+ } else {
+ gotoNextState();
+ }
+ }
+ }
+ };
+ mConfigCombo.addSelectionListener(listener);
+ mActivityCombo.addSelectionListener(listener);
+ mLocaleCombo.addSelectionListener(listener);
+ mDeviceCombo.addSelectionListener(listener);
+ mTargetCombo.addSelectionListener(listener);
+ mThemeCombo.addSelectionListener(listener);
+ mOrientationCombo.addSelectionListener(listener);
+
+ addDisposeListener(this);
+
+ initDevices();
+ initTargets();
+ }
+
+ /**
+ * Returns the edited file
+ *
+ * @return the file
+ */
+ @Nullable
+ public IFile getEditedFile() {
+ return mEditedFile;
+ }
+
+ /**
+ * Returns the project of the edited file
+ *
+ * @return the project
+ */
+ @Nullable
+ public IProject getProject() {
+ if (mEditedFile != null) {
+ return mEditedFile.getProject();
+ } else {
+ return null;
+ }
+ }
+
+ ConfigurationClient getClient() {
+ return mClient;
+ }
+
+ /**
+ * Returns the project resources for the project being configured by this
+ * chooser
+ *
+ * @return the project resources
+ */
+ @Nullable
+ public ProjectResources getResources() {
+ return mResources;
+ }
+
+ /**
+ * Returns the full, complete {@link FolderConfiguration}
+ *
+ * @return the full configuration
+ */
+ public FolderConfiguration getFullConfiguration() {
+ return mConfiguration.getFullConfig();
+ }
+
+ /**
+ * Returns the project target
+ *
+ * @return the project target
+ */
+ public IAndroidTarget getProjectTarget() {
+ return mProjectTarget;
+ }
+
+ /**
+ * Returns the configuration being edited by this {@linkplain ConfigurationChooser}
+ *
+ * @return the configuration
+ */
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Returns the list of locales
+ * @return a list of {@link ResourceQualifier} pairs
+ */
+ @NonNull
+ public List<Locale> getLocaleList() {
+ return mLocaleList;
+ }
+
+ /**
+ * Returns the list of available devices
+ *
+ * @return a list of {@link Device} objects
+ */
+ @NonNull
+ public Collection<Device> getDevices() {
+ return mDevices;
+ }
+
+ /**
+ * Returns the list of available render targets
+ *
+ * @return a list of {@link IAndroidTarget} objects
+ */
+ @NonNull
+ public List<IAndroidTarget> getTargetList() {
+ return mTargetList;
+ }
+
+ // ---- Configuration State Lookup ----
+
+ /**
+ * Returns the rendering target to be used
+ *
+ * @return the target
+ */
+ @NonNull
+ public IAndroidTarget getTarget() {
+ IAndroidTarget target = mConfiguration.getTarget();
+ if (target == null) {
+ target = mProjectTarget;
+ }
+
+ return target;
+ }
+
+ /**
+ * Returns the current device string, or null if no device is selected
+ *
+ * @return the device name, or null
+ */
+ @Nullable
+ public String getDeviceName() {
+ Device device = mConfiguration.getDevice();
+ if (device != null) {
+ return device.getName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current theme, or null if none has been selected
+ *
+ * @return the theme name, or null
+ */
+ @Nullable
+ public String getThemeName() {
+ String theme = mConfiguration.getTheme();
+ if (theme != null) {
+ theme = ResourceHelper.styleToTheme(theme);
+ }
+
+ return theme;
+ }
+
+ /** Move to the next device state, changing the icon if it changes orientation */
+ private void gotoNextState() {
+ State state = mConfiguration.getDeviceState();
+ State flipped = mConfiguration.getNextDeviceState(state);
+ if (flipped != state) {
+ selectDeviceState(flipped);
+ onDeviceConfigChange();
+ }
+ }
+
+ // ---- Implements DisposeListener ----
+
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ dispose();
+ }
+
+ @Override
+ public void dispose() {
+ if (!isDisposed()) {
+ super.dispose();
+
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ DeviceManager manager = sdk.getDeviceManager();
+ manager.unregisterListener(this);
+ }
+ }
+ }
+
+ // ---- 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;
+ ensureInitialized();
+ }
+
+ /**
+ * 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
+ * @param file the file being opened.
+ */
+ public void replaceFile(IFile file) {
+ // if there is no previous selection, revert to default mode.
+ if (mConfiguration.getDevice() == null) {
+ setFile(file); // onTargetChanged will be called later.
+ return;
+ }
+
+ setFile(file);
+ IProject project = mEditedFile.getProject();
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ mConfiguration.setEditedConfig(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) {
+ setVisible(true);
+
+ 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
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ mConfiguration.syncFolderConfig();
+
+ // update the string showing the config value
+ selectConfiguration(mConfiguration.getEditedConfig());
+ updateActivity();
+ }
+ } else if (sdkStatus == LoadStatus.FAILED) {
+ setVisible(true);
+ }
+ } 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) {
+ setFile(file);
+ IProject project = mEditedFile.getProject();
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ FolderConfiguration config = resFolder.getConfiguration();
+ mConfiguration.setEditedConfig(config);
+
+ // All that's needed is to update the string showing the config value
+ // (since the config combo settings chosen by the user).
+ selectConfiguration(config);
+ }
+
+ /**
+ * 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);
+
+ IFolder parent = (IFolder) mEditedFile.getParent();
+ ResourceFolder resFolder = mResources.getResourceFolder(parent);
+ if (resFolder != null) {
+ mConfiguration.setEditedConfig(resFolder.getConfiguration());
+ } else {
+ FolderConfiguration config = FolderConfiguration.getConfig(
+ parent.getName().split(RES_QUALIFIER_SEP));
+ if (config != null) {
+ mConfiguration.setEditedConfig(config);
+ } else {
+ mConfiguration.setEditedConfig(new FolderConfiguration());
+ }
+ }
+
+ onXmlModelLoaded();
+ }
+
+
+ /**
+ * Sets the current configuration to match the given folder configuration,
+ * the given theme name, the given device and device state.
+ *
+ * @param configuration new folder configuration to use
+ */
+ public void setConfiguration(@NonNull Configuration configuration) {
+ if (mClient != null) {
+ mClient.aboutToChange(MASK_ALL);
+ }
+
+ Configuration oldConfiguration = mConfiguration;
+ mConfiguration = configuration;
+ mConfiguration.setChooser(this);
+
+ selectTheme(configuration.getTheme());
+ selectLocale(configuration.getLocale());
+ selectDevice(configuration.getDevice());
+ selectDeviceState(configuration.getDeviceState());
+ selectTarget(configuration.getTarget());
+ selectActivity(configuration.getActivity());
+
+ // This may be a second refresh after triggered by theme above
+ if (mClient != null) {
+ LayoutCanvas canvas = mClient.getCanvas();
+ if (canvas != null) {
+ assert mConfiguration != oldConfiguration;
+ canvas.getPreviewManager().updateChooserConfig(oldConfiguration, mConfiguration);
+ }
+
+ boolean accepted = mClient.changed(MASK_ALL);
+ if (!accepted) {
+ configuration = oldConfiguration;
+ selectTheme(configuration.getTheme());
+ selectLocale(configuration.getLocale());
+ selectDevice(configuration.getDevice());
+ selectDeviceState(configuration.getDeviceState());
+ selectTarget(configuration.getTarget());
+ selectActivity(configuration.getActivity());
+ if (canvas != null && mConfiguration != oldConfiguration) {
+ canvas.getPreviewManager().updateChooserConfig(mConfiguration,
+ oldConfiguration);
+ }
+ return;
+ } else {
+ int changed = 0;
+ if (!equal(oldConfiguration.getTheme(), mConfiguration.getTheme())) {
+ changed |= CFG_THEME;
+ }
+ if (!equal(oldConfiguration.getDevice(), mConfiguration.getDevice())) {
+ changed |= CFG_DEVICE | CFG_DEVICE_STATE;
+ }
+ if (changed != 0) {
+ syncToVariations(changed, mEditedFile, mConfiguration, false, true);
+ }
+ }
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * 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 {
+ updateDevices();
+ updateTargets();
+ ensureInitialized();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ /**
+ * Responds 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 it will attempt 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 happen (but the method
+ * must be called back when they are.)
+ * <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).
+ *
+ * @return the target data for the rendering target used to render the
+ * layout
+ *
+ * @see #saveConstraints()
+ * @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) {
+ updateDevices();
+ updateTargets();
+ ensureInitialized();
+ mSdkChanged = false;
+ }
+
+ IProject project = mEditedFile.getProject();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ mProjectTarget = currentSdk.getTarget(project);
+ }
+
+ LoadStatus targetStatus = LoadStatus.FAILED;
+ if (mProjectTarget != null) {
+ targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
+ updateTargets();
+ ensureInitialized();
+ }
+
+ if (targetStatus == LoadStatus.LOADED) {
+ setVisible(true);
+ if (mResources == null) {
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+ }
+ if (mConfiguration.getEditedConfig() == null) {
+ IFolder parent = (IFolder) mEditedFile.getParent();
+ ResourceFolder resFolder = mResources.getResourceFolder(parent);
+ if (resFolder != null) {
+ mConfiguration.setEditedConfig(resFolder.getConfiguration());
+ } else {
+ FolderConfiguration config = FolderConfiguration.getConfig(
+ parent.getName().split(RES_QUALIFIER_SEP));
+ if (config != null) {
+ mConfiguration.setEditedConfig(config);
+ } else {
+ mConfiguration.setEditedConfig(new FolderConfiguration());
+ }
+ }
+ }
+
+ targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
+
+ // get the file stored state
+ ensureInitialized();
+ boolean loadedConfigData = mConfiguration.getDevice() != null &&
+ mConfiguration.getDeviceState() != null;
+
+ // Load locale list. This must be run after we initialize the
+ // configuration above, since it attempts to sync the UI with
+ // the value loaded into the configuration.
+ 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.
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ if (loadedConfigData) {
+ // first make sure we have the config to adapt
+ selectDevice(mConfiguration.getDevice());
+ selectDeviceState(mConfiguration.getDeviceState());
+ mConfiguration.syncFolderConfig();
+
+ matcher.adaptConfigSelection(false);
+
+ IAndroidTarget target = mConfiguration.getTarget();
+ selectTarget(target);
+ targetData = Sdk.getCurrent().getTargetData(target);
+ } else {
+ matcher.findAndSetCompatibleConfig(false);
+
+ // Default to modern layout lib
+ IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(this);
+ if (target != null) {
+ targetData = Sdk.getCurrent().getTargetData(target);
+ selectTarget(target);
+ mConfiguration.setTarget(target, true);
+ }
+ }
+
+ // Update activity: This is done before updateThemes() since
+ // the themes selection can depend on the currently selected activity
+ // (e.g. when there are manifest registrations for the theme to use
+ // for a given activity)
+ updateActivity();
+
+ // Update themes. This is done after updating the devices above,
+ // since we want to look at the chosen device size to decide
+ // what the default theme (for example, with Honeycomb we choose
+ // Holo as the default theme but only if the screen size is XLARGE
+ // (and of course only if the manifest does not specify another
+ // default theme).
+ updateThemes();
+
+ // update the string showing the config value
+ selectConfiguration(mConfiguration.getEditedConfig());
+
+ // compute the final current config
+ mConfiguration.syncFolderConfig();
+ } else if (targetStatus == LoadStatus.FAILED) {
+ setVisible(true);
+ }
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ return targetData;
+ }
+
+ /**
+ * This is a temporary workaround for a infrequently happening bug; apparently
+ * there are cases where the configuration chooser isn't shown
+ */
+ public void ensureVisible() {
+ if (!isVisible()) {
+ LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
+ if (sdkStatus == LoadStatus.LOADED) {
+ onXmlModelLoaded();
+ }
+ }
+ }
+
+ /**
+ * An alternate layout for this layout has been created. This means that the
+ * current layout may no longer be a best fit. However, since we support multiple
+ * layouts being open at the same time, we need to adjust the current configuration
+ * back to something where this layout <b>is</b> a best match.
+ */
+ public void onAlternateLayoutCreated() {
+ IFile best = ConfigurationMatcher.getBestFileMatch(this);
+ if (best != null && !best.equals(mEditedFile)) {
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ mConfiguration.syncFolderConfig();
+ if (mClient != null) {
+ mClient.changed(MASK_ALL);
+ }
+ }
+ }
+
+ /**
+ * Loads the list of {@link Device}s and inits the UI with it.
+ */
+ private void initDevices() {
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ DeviceManager manager = sdk.getDeviceManager();
+ // This method can be called more than once, so avoid duplicate entries
+ manager.unregisterListener(this);
+ manager.registerListener(this);
+ mDevices = manager.getDevices(DeviceManager.ALL_DEVICES);
+ } else {
+ mDevices = new ArrayList<Device>();
+ }
+ }
+
+ /**
+ * Loads the list of {@link IAndroidTarget} and inits the UI with it.
+ */
+ private boolean initTargets() {
+ mTargetList.clear();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget[] targets = currentSdk.getTargets();
+ for (int i = 0 ; i < targets.length; i++) {
+ if (targets[i].hasRenderingLibrary()) {
+ mTargetList.add(targets[i]);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Ensures that the configuration has been initialized */
+ public void ensureInitialized() {
+ if (mConfiguration.getDevice() == null && mEditedFile != null) {
+ String data = ConfigurationDescription.getDescription(mEditedFile);
+ if (mInitialState != null) {
+ data = mInitialState;
+ mInitialState = null;
+ }
+ if (data != null) {
+ mConfiguration.initialize(data);
+ mConfiguration.syncFolderConfig();
+ }
+ }
+ }
+
+ private void updateDevices() {
+ if (mDevices.size() == 0) {
+ initDevices();
+ }
+ }
+
+ private void updateTargets() {
+ if (mTargetList.size() == 0) {
+ if (!initTargets()) {
+ return;
+ }
+ }
+
+ IAndroidTarget renderingTarget = mConfiguration.getTarget();
+
+ IAndroidTarget match = null;
+ for (IAndroidTarget target : mTargetList) {
+ if (renderingTarget != null) {
+ // use equals because the rendering could be from a previous SDK, so
+ // it may not be the same instance.
+ if (renderingTarget.equals(target)) {
+ match = target;
+ }
+ } else if (mProjectTarget == target) {
+ match = target;
+ }
+
+ }
+
+ if (match == null) {
+ // the rendering target is the same as the project.
+ renderingTarget = mProjectTarget;
+ } else {
+ // set the rendering target to the new object.
+ renderingTarget = match;
+ }
+
+ mConfiguration.setTarget(renderingTarget, true);
+ selectTarget(renderingTarget);
+ }
+
+ /** Update the toolbar whenever a label has changed, to not only
+ * cause the layout in the current toolbar to update, but to possibly
+ * wrap the toolbars and update the layout of the surrounding area.
+ */
+ private void resizeToolBar() {
+ Point size = getSize();
+ Point newSize = computeSize(size.x, SWT.DEFAULT, true);
+ setSize(newSize);
+ Composite parent = getParent();
+ parent.layout();
+ parent.redraw();
+ }
+
+
+ Image getOrientationIcon(ScreenOrientation orientation, boolean flip) {
+ IconFactory icons = IconFactory.getInstance();
+ switch (orientation) {
+ case LANDSCAPE:
+ return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
+ case SQUARE:
+ return icons.getIcon(ICON_SQUARE);
+ case PORTRAIT:
+ default:
+ return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
+ }
+ }
+
+ ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) {
+ IconFactory icons = IconFactory.getInstance();
+ switch (orientation) {
+ case LANDSCAPE:
+ return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
+ case SQUARE:
+ return icons.getImageDescriptor(ICON_SQUARE);
+ case PORTRAIT:
+ default:
+ return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
+ }
+ }
+
+ @NonNull
+ ScreenOrientation getOrientation(State state) {
+ FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state);
+ ScreenOrientation orientation = null;
+ if (config != null && config.getScreenOrientationQualifier() != null) {
+ orientation = config.getScreenOrientationQualifier().getValue();
+ }
+
+ if (orientation == null) {
+ orientation = ScreenOrientation.PORTRAIT;
+ }
+
+ return orientation;
+ }
+
+ /**
+ * Stores the current config selection into the edited file such that we can
+ * bring it back the next time this layout is opened.
+ */
+ public void saveConstraints() {
+ String description = mConfiguration.toPersistentString();
+ if (description != null && !description.isEmpty()) {
+ ConfigurationDescription.setDescription(mEditedFile, description);
+ }
+ }
+
+ // ---- Setting the current UI state ----
+
+ void selectDeviceState(@Nullable State state) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mOrientationCombo.setData(state);
+
+ State nextState = mConfiguration.getNextDeviceState(state);
+ mOrientationCombo.setImage(getOrientationIcon(getOrientation(state),
+ nextState != state));
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectTarget(IAndroidTarget target) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mTargetCombo.setData(target);
+ String label = getRenderingTargetLabel(target, true);
+ mTargetCombo.setText(label);
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ /**
+ * Selects a given {@link Device} in the device combo, if it is found.
+ * @param device the device to select
+ * @return true if the device was found.
+ */
+ boolean selectDevice(@Nullable Device device) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mDeviceCombo.setData(device);
+ if (device != null) {
+ mDeviceCombo.setText(getDeviceLabel(device, true));
+ } else {
+ mDeviceCombo.setText("Device");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+
+ return false;
+ }
+
+ void selectActivity(@Nullable String fqcn) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ if (fqcn != null) {
+ mActivityCombo.setData(fqcn);
+ String label = getActivityLabel(fqcn, true);
+ mActivityCombo.setText(label);
+ } else {
+ mActivityCombo.setText("(Select)");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectTheme(@Nullable String theme) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ assert theme == null || theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme;
+ mThemeCombo.setData(theme);
+ if (theme != null) {
+ mThemeCombo.setText(getThemeLabel(theme, true));
+ } else {
+ // FIXME eclipse claims this is dead code.
+ mThemeCombo.setText("(Set Theme)");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectLocale(@Nullable Locale locale) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mLocaleCombo.setData(locale);
+ String label = Strings.nullToEmpty(getLocaleLabel(this, locale, true));
+ mLocaleCombo.setText(label);
+
+ Image image = getFlagImage(locale);
+ mLocaleCombo.setImage(image);
+
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ @NonNull
+ Image getFlagImage(@Nullable Locale locale) {
+ if (locale != null) {
+ return locale.getFlagImage();
+ }
+
+ return FlagManager.getGlobeIcon();
+ }
+
+ private void selectConfiguration(FolderConfiguration fileConfig) {
+ /* For now, don't show any text in the configuration combo, use just an
+ icon. This has the advantage that the configuration contents don't
+ shift around, so you can for example click back and forth between
+ portrait and landscape without the icon moving under the mouse.
+ If this works well, remove this whole method post ADT 21.
+ assert isUiThread();
+ try {
+ String current = mEditedFile.getParent().getName();
+ if (current.equals(FD_RES_LAYOUT)) {
+ current = "default";
+ }
+
+ // Pretty things up a bit
+ //if (current == null || current.equals("default")) {
+ // current = "Default Configuration";
+ //}
+ mConfigCombo.setText(current);
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ */
+ }
+
+ /**
+ * 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 setLocale(@Nullable Locale locale) {
+ boolean changed = !Objects.equal(mConfiguration.getLocale(), locale);
+ selectLocale(locale);
+
+ return changed;
+ }
+
+ // ---- Creating UI labels ----
+
+ /**
+ * Returns a suitable label to use to display the given activity
+ *
+ * @param fqcn the activity class to look up a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getActivityLabel(String fqcn, boolean brief) {
+ if (brief) {
+ String label = fqcn;
+ int packageIndex = label.lastIndexOf('.');
+ if (packageIndex != -1) {
+ label = label.substring(packageIndex + 1);
+ }
+ int innerClass = label.lastIndexOf('$');
+ if (innerClass != -1) {
+ label = label.substring(innerClass + 1);
+ }
+
+ // Also strip out the "Activity" or "Fragment" common suffix
+ // if this is a long name
+ if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix
+ label = label.substring(0, label.length() - 8);
+ } else if (label.endsWith("Fragment") && label.length() > 8 + 12) {
+ label = label.substring(0, label.length() - 8);
+ }
+
+ return label;
+ }
+
+ return fqcn;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given theme
+ *
+ * @param theme the theme to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getThemeLabel(String theme, boolean brief) {
+ theme = ResourceHelper.styleToTheme(theme);
+
+ if (brief) {
+ int index = theme.lastIndexOf('.');
+ if (index < theme.length() - 1) {
+ return theme.substring(index + 1);
+ }
+ }
+ return theme;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given rendering target
+ *
+ * @param target the target to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) {
+ if (target == null) {
+ return "<null>";
+ }
+
+ AndroidVersion version = target.getVersion();
+
+ if (brief) {
+ if (target.isPlatform()) {
+ return Integer.toString(version.getApiLevel());
+ } else {
+ return target.getName() + ':' + Integer.toString(version.getApiLevel());
+ }
+ }
+
+ String label = String.format("API %1$d: %2$s",
+ version.getApiLevel(),
+ target.getShortClasspathName());
+
+ return label;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given device
+ *
+ * @param device the device to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getDeviceLabel(@Nullable Device device, boolean brief) {
+ if (device == null) {
+ return "";
+ }
+ String name = device.getName();
+
+ if (brief) {
+ // Produce a really brief summary of the device name, suitable for
+ // use in the narrow space available in the toolbar for example
+ int nexus = name.indexOf("Nexus"); //$NON-NLS-1$
+ if (nexus != -1) {
+ int begin = name.indexOf('(');
+ if (begin != -1) {
+ begin++;
+ int end = name.indexOf(')', begin);
+ if (end != -1) {
+ return name.substring(begin, end).trim();
+ }
+ }
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given locale
+ *
+ * @param chooser the chooser, if known
+ * @param locale the locale to look up a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ @Nullable
+ public static String getLocaleLabel(
+ @Nullable ConfigurationChooser chooser,
+ @Nullable Locale locale,
+ boolean brief) {
+ if (locale == null) {
+ return null;
+ }
+
+ if (!locale.hasLanguage()) {
+ if (brief) {
+ // Just use the icon
+ return "";
+ }
+
+ boolean hasLocale = false;
+ ResourceRepository projectRes = chooser != null ? chooser.mClient.getProjectResources()
+ : null;
+ if (projectRes != null) {
+ hasLocale = projectRes.getLanguages().size() > 0;
+ }
+
+ if (hasLocale) {
+ return "Other";
+ } else {
+ return "Any";
+ }
+ }
+
+ String languageCode = locale.qualifier.getLanguage();
+ String languageName = LocaleManager.getLanguageName(languageCode);
+
+ if (!locale.hasRegion()) {
+ // TODO: Make the region string use "Other" instead of "Any" if
+ // there is more than one region for a given language
+ //if (regions.size() > 0) {
+ // return String.format("%1$s / Other", language);
+ //} else {
+ // return String.format("%1$s / Any", language);
+ //}
+ if (!brief && languageName != null) {
+ return String.format("%1$s (%2$s)", languageName, languageCode);
+ } else {
+ return languageCode;
+ }
+ } else {
+ String regionCode = locale.qualifier.getRegion();
+ if (!brief && languageName != null) {
+ String regionName = LocaleManager.getRegionName(regionCode);
+ if (regionName != null) {
+ return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode,
+ regionName, regionCode);
+ }
+ return String.format("%1$s (%2$s) in %3$s", languageName, languageCode,
+ regionCode);
+ }
+ return String.format("%1$s / %2$s", languageCode, regionCode);
+ }
+ }
+
+ // ---- Implements DevicesChangedListener ----
+
+ @Override
+ public void onDevicesChanged() {
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ mDevices = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES);
+ } else {
+ mDevices = new ArrayList<Device>();
+ }
+ }
+
+ // ---- Reacting to UI changes ----
+
+ /**
+ * Called when the selection of the device combo changes.
+ */
+ void onDeviceChange() {
+ // because changing the content of a combo triggers a change event, respect the
+ // mDisableUpdates flag
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ // Attempt to preserve the device state
+ String stateName = null;
+ Device prevDevice = mConfiguration.getDevice();
+ State prevState = mConfiguration.getDeviceState();
+ Device device = (Device) mDeviceCombo.getData();
+ if (prevDevice != null && prevState != null && device != null) {
+ // get the previous config, so that we can look for a close match
+ FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState);
+ if (oldConfig != null) {
+ stateName = ConfigurationMatcher.getClosestMatch(oldConfig, device.getAllStates());
+ }
+ }
+ mConfiguration.setDevice(device, true);
+ State newState = Configuration.getState(device, stateName);
+ mConfiguration.setDeviceState(newState, true);
+ selectDeviceState(newState);
+ mConfiguration.syncFolderConfig();
+
+ // Notify
+ IFile file = mEditedFile;
+ boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
+ if (!accepted) {
+ mConfiguration.setDevice(prevDevice, true);
+ mConfiguration.setDeviceState(prevState, true);
+ mConfiguration.syncFolderConfig();
+ selectDevice(prevDevice);
+ selectDeviceState(prevState);
+ return;
+ } else {
+ syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, file, mConfiguration, false, true);
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Synchronizes changes to the given attributes (indicated by the mask
+ * referencing the {@code CFG_} configuration attribute bit flags in
+ * {@link Configuration} to the layout variations of the given updated file.
+ *
+ * @param flags the attributes which were updated
+ * @param updatedFile the file which was updated
+ * @param base the base configuration to base the chooser off of
+ * @param includeSelf whether the updated file itself should be updated
+ * @param async whether the updates should be performed asynchronously
+ */
+ public void syncToVariations(
+ final int flags,
+ final @NonNull IFile updatedFile,
+ final @NonNull Configuration base,
+ final boolean includeSelf,
+ boolean async) {
+ if (async) {
+ getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ doSyncToVariations(flags, updatedFile, includeSelf, base);
+ }
+ });
+ } else {
+ doSyncToVariations(flags, updatedFile, includeSelf, base);
+ }
+ }
+
+ private void doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf,
+ Configuration base) {
+ // Synchronize the given changes to other configurations as well
+ List<IFile> files = AdtUtils.getResourceVariations(updatedFile, includeSelf);
+ for (IFile file : files) {
+ Configuration configuration = Configuration.create(base, file);
+ configuration.setTheme(base.getTheme());
+ configuration.setActivity(base.getActivity());
+ Collection<IEditorPart> editors = AdtUtils.findEditorsFor(file, false);
+ boolean found = false;
+ for (IEditorPart editor : editors) {
+ if (editor instanceof CommonXmlEditor) {
+ CommonXmlDelegate delegate = ((CommonXmlEditor) editor).getDelegate();
+ if (delegate instanceof LayoutEditorDelegate) {
+ editor = ((LayoutEditorDelegate) delegate).getGraphicalEditor();
+ }
+ }
+ if (editor instanceof GraphicalEditorPart) {
+ ConfigurationChooser chooser =
+ ((GraphicalEditorPart) editor).getConfigurationChooser();
+ chooser.setConfiguration(configuration);
+ found = true;
+ }
+ }
+ if (!found) {
+ // Just update the file persistence
+ String description = configuration.toPersistentString();
+ ConfigurationDescription.setDescription(file, description);
+ }
+ }
+ }
+
+ /**
+ * Called when the device config selection changes.
+ */
+ void onDeviceConfigChange() {
+ // because changing the content of a combo triggers a change event, respect the
+ // mDisableUpdates flag
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ State prev = mConfiguration.getDeviceState();
+ State state = (State) mOrientationCombo.getData();
+ mConfiguration.setDeviceState(state, false);
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
+ if (!accepted) {
+ mConfiguration.setDeviceState(prev, false);
+ selectDeviceState(prev);
+ return;
+ }
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Call back for language combo selection
+ */
+ 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;
+ }
+
+ Locale prev = mConfiguration.getLocale();
+ Locale locale = (Locale) mLocaleCombo.getData();
+ if (locale == null) {
+ locale = Locale.ANY;
+ }
+ mConfiguration.setLocale(locale, false);
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_LOCALE);
+ if (!accepted) {
+ mConfiguration.setLocale(prev, false);
+ selectLocale(prev);
+ }
+ }
+
+ // Store locale project-wide setting
+ mConfiguration.saveRenderState();
+ }
+
+
+ void onThemeChange() {
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ String prev = mConfiguration.getTheme();
+ mConfiguration.setTheme((String) mThemeCombo.getData());
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_THEME);
+ if (!accepted) {
+ mConfiguration.setTheme(prev);
+ selectTheme(prev);
+ return;
+ } else {
+ syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, mEditedFile, mConfiguration,
+ false, true);
+ }
+ }
+
+ saveConstraints();
+ }
+
+ void notifyFolderConfigChanged() {
+ if (mDisableUpdates > 0 || mClient == null) {
+ return;
+ }
+
+ if (mClient.changed(CFG_FOLDER)) {
+ saveConstraints();
+ }
+ }
+
+ void onSelectActivity() {
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ String activity = (String) mActivityCombo.getData();
+ mConfiguration.setActivity(activity);
+
+ if (activity == null) {
+ return;
+ }
+
+ // See if there is a default theme assigned to this activity, and if so, use it
+ ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject());
+ String preferred = null;
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) {
+ // Yes, switch to it
+ selectTheme(preferred);
+ onThemeChange();
+ }
+
+ // Persist in XML
+ if (mClient != null) {
+ mClient.setActivity(activity);
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Call back for api level combo selection
+ */
+ 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;
+ }
+
+ IAndroidTarget prevTarget = mConfiguration.getTarget();
+ String prevTheme = mConfiguration.getTheme();
+
+ int changeFlags = 0;
+
+ // tell the listener a new rendering target is being set. Need to do this before updating
+ // mRenderingTarget.
+ if (prevTarget != null) {
+ changeFlags |= CFG_TARGET;
+ mClient.aboutToChange(changeFlags);
+ }
+
+ IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData();
+ mConfiguration.setTarget(target, true);
+
+ // 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.
+ String oldTheme = mConfiguration.getTheme();
+ updateThemes();
+ // updateThemes may change the theme (based on theme availability in the new rendering
+ // target) so mark theme change if necessary
+ if (!Objects.equal(oldTheme, mConfiguration.getTheme())) {
+ changeFlags |= CFG_THEME;
+ }
+
+ if (target != null) {
+ changeFlags |= CFG_TARGET;
+ changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier
+ }
+
+ // Store project-wide render-target setting
+ mConfiguration.saveRenderState();
+
+ mConfiguration.syncFolderConfig();
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(changeFlags);
+ if (!accepted) {
+ mConfiguration.setTarget(prevTarget, true);
+ mConfiguration.setTheme(prevTheme);
+ mConfiguration.syncFolderConfig();
+ selectTheme(prevTheme);
+ selectTarget(prevTarget);
+ }
+ }
+ }
+
+ /**
+ * 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 (mConfiguration.getEditedConfig() == null) {
+ // Startup; ignore
+ return false;
+ }
+
+ boolean renderTargetChanged = false;
+
+ // When a page is re-activated, force the toggles to reflect the current project
+ // state
+
+ Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this);
+
+ int changeFlags = 0;
+ // Only sync the locale if this layout is not already a locale-specific layout!
+ if (pair != null && !mConfiguration.isLocaleSpecificLayout()) {
+ Locale locale = pair.getFirst();
+ if (locale != null) {
+ boolean localeChanged = setLocale(locale);
+ if (localeChanged) {
+ changeFlags |= CFG_LOCALE;
+ }
+ } else {
+ locale = Locale.ANY;
+ }
+ mConfiguration.setLocale(locale, true);
+ }
+
+ // Sync render target
+ IAndroidTarget configurationTarget = mConfiguration.getTarget();
+ IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget;
+ if (target != null && configurationTarget != target) {
+ if (mClient != null && configurationTarget != null) {
+ changeFlags |= CFG_TARGET;
+ mClient.aboutToChange(changeFlags);
+ }
+
+ mConfiguration.setTarget(target, true);
+ selectTarget(target);
+ renderTargetChanged = true;
+ }
+
+ // Neither locale nor render target changed: nothing to do
+ if (changeFlags == 0) {
+ 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.
+
+ // Compute the new configuration; we want to do this both for locale changes
+ // and for render targets.
+ mConfiguration.syncFolderConfig();
+ changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier
+
+ 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 (mClient != null) {
+ mClient.changed(changeFlags);
+ }
+
+ return true;
+ }
+
+ // ---- Populate data structures with themes, locales, etc ----
+
+ /**
+ * Updates the internal list of themes.
+ */
+ private void updateThemes() {
+ if (mClient == null) {
+ return; // can't do anything without it.
+ }
+
+ ResourceRepository frameworkRes = mClient.getFrameworkResources(
+ mConfiguration.getTarget());
+
+ mDisableUpdates++;
+
+ try {
+ if (mEditedFile != null) {
+ String theme = mConfiguration.getTheme();
+ if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) {
+ mConfiguration.setTheme(null);
+ mConfiguration.computePreferredTheme();
+ }
+ assert mConfiguration.getTheme() != null;
+ }
+
+ mThemeList.clear();
+
+ ArrayList<String> themes = new ArrayList<String>();
+ ResourceRepository projectRes = mClient.getProjectResources();
+ // in cases where the opened file is not linked to a project, this could be null.
+ if (projectRes != null) {
+ // get the configured resources for the project
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
+ mClient.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, null)) {
+ String theme = value.getName();
+ themes.add(theme);
+ }
+ }
+
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ mThemeList.add(theme);
+ }
+ }
+ }
+ themes.clear();
+ }
+
+ // get the themes, and languages from the Framework.
+ if (frameworkRes != null) {
+ // get the configured resources for the framework
+ Map<ResourceType, Map<String, ResourceValue>> frameworResources =
+ frameworkRes.getConfiguredResources(mConfiguration.getFullConfig());
+
+ 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")) { //$NON-NLS-1$ //$NON-NLS-2$
+ themes.add(value.getName());
+ }
+ }
+
+ // sort them and add them to the combo
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ }
+ mThemeList.add(theme);
+ }
+
+ themes.clear();
+ }
+ }
+
+ // Migration: In the past we didn't store the style prefix in the settings;
+ // this meant we might lose track of whether the theme is a project style
+ // or a framework style. For now we need to migrate. Search through the
+ // theme list until we have a match
+ String theme = mConfiguration.getTheme();
+ if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
+ String projectStyle = STYLE_RESOURCE_PREFIX + theme;
+ String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ for (String t : mThemeList) {
+ if (t.equals(projectStyle)) {
+ mConfiguration.setTheme(projectStyle);
+ break;
+ } else if (t.equals(frameworkStyle)) {
+ mConfiguration.setTheme(frameworkStyle);
+ break;
+ }
+ }
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ // Arbitrary guess
+ if (theme.startsWith("Theme.")) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ } else {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+ }
+
+ // TODO: Handle the case where you have a theme persisted that isn't available??
+ // We could look up mConfiguration.theme and make sure it appears in the list! And if
+ // not, picking one.
+ selectTheme(mConfiguration.getTheme());
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ private void updateActivity() {
+ if (mEditedFile != null) {
+ String preferred = getPreferredActivity(mEditedFile);
+ selectActivity(preferred);
+ }
+ }
+
+ /**
+ * Updates the locale combo.
+ * This must be called from the UI thread.
+ */
+ public void updateLocales() {
+ if (mClient == null) {
+ return; // can't do anything w/o it.
+ }
+
+ mDisableUpdates++;
+
+ try {
+ mLocaleList.clear();
+
+ SortedSet<String> languages = null;
+
+ // get the languages from the project.
+ ResourceRepository projectRes = mClient.getProjectResources();
+
+ // in cases where the opened file is not linked to a project, this could be null.
+ if (projectRes != null) {
+ // now get the languages from the project.
+ languages = projectRes.getLanguages();
+
+ for (String language : languages) {
+ // find the matching regions and add them
+ SortedSet<String> regions = projectRes.getRegions(language);
+ for (String region : regions) {
+ LocaleQualifier locale = LocaleQualifier.getQualifier(language + "-r" + region);
+ if (locale != null) {
+ mLocaleList.add(Locale.create(locale));
+ }
+ }
+
+ // now the entry for the other regions the language alone
+ // create a region qualifier that will never be matched by qualified resources.
+ LocaleQualifier locale = new LocaleQualifier(language);
+ mLocaleList.add(Locale.create(locale));
+ }
+ }
+
+ // create language/region qualifier that will never be matched by qualified resources.
+ mLocaleList.add(Locale.ANY);
+
+ Locale locale = mConfiguration.getLocale();
+ setLocale(locale);
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ @Nullable
+ private String getPreferredActivity(@NonNull IFile file) {
+ // Store/restore the activity context in the config state to help with
+ // performance if for some reason we can't write it into the XML file and to
+ // avoid having to open the model below
+ if (mConfiguration.getActivity() != null) {
+ return mConfiguration.getActivity();
+ }
+
+ IProject project = file.getProject();
+
+ // Look up from XML file
+ Document document = DomUtilities.getDocument(file);
+ if (document != null) {
+ Element element = document.getDocumentElement();
+ if (element != null) {
+ String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT);
+ if (activity != null && !activity.isEmpty()) {
+ if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$
+ ManifestInfo manifest = ManifestInfo.get(project);
+ String pkg = manifest.getPackage();
+ if (!pkg.isEmpty()) {
+ if (activity.startsWith(".")) { //$NON-NLS-1$
+ activity = pkg + activity;
+ } else {
+ activity = activity + '.' + pkg;
+ }
+ }
+ }
+
+ mConfiguration.setActivity(activity);
+ saveConstraints();
+ return activity;
+ }
+ }
+ }
+
+ // No, not available there: try to infer it from the code index
+ String includedIn = null;
+ Reference includedWithin = mClient.getIncludedWithin();
+ if (mClient != null && includedWithin != null) {
+ includedIn = includedWithin.getName();
+ }
+
+ ManifestInfo manifest = ManifestInfo.get(project);
+ String pkg = manifest.getPackage();
+ String layoutName = ResourceHelper.getLayoutName(mEditedFile);
+
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+ if (includedIn != null) {
+ layoutName = includedIn;
+ }
+
+ String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
+
+ if (activity == null) {
+ List<String> activities = ManifestInfo.getProjectActivities(project);
+ if (activities.size() == 1) {
+ activity = activities.get(0);
+ }
+ }
+
+ if (activity != null) {
+ mConfiguration.setActivity(activity);
+ saveConstraints();
+ return activity;
+ }
+
+ // TODO: Do anything else, such as pick the first activity found?
+ // Or just leave some default label instead?
+ // Also, figure out what to store in the mState so I don't keep trying
+
+ return null;
+ }
+
+ /**
+ * 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.
+ * @param seen the map of styles we have already processed (or null if not yet
+ * initialized). Only the keys are significant (since there is no IdentityHashSet).
+ * @return True if the given <var>style</var> is a theme.
+ */
+ private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap,
+ IdentityHashMap<ResourceValue, Boolean> seen) {
+ 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_NS_NAME_PREFIX)) {
+ frameworkStyle = true;
+ parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.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.
+ ResourceValue parentValue = styleMap.get(parentStyle);
+
+ // also prevent stack overflow in case the dev mistakenly declared
+ // the parent of the style as the style itself.
+ if (parentValue != null && !parentValue.equals(value)) {
+ if (seen == null) {
+ seen = new IdentityHashMap<ResourceValue, Boolean>();
+ seen.put(value, Boolean.TRUE);
+ } else if (seen.containsKey(parentValue)) {
+ return false;
+ }
+ seen.put(parentValue, Boolean.TRUE);
+ return isTheme(parentValue, styleMap, seen);
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this configuration chooser represents the best match for
+ * the given file
+ *
+ * @param file the file to test
+ * @param config the config to test
+ * @return true if the given config is the best match for the given file
+ */
+ public boolean isBestMatchFor(IFile file, FolderConfiguration config) {
+ ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
+ ResourceType.LAYOUT, config);
+ if (match != null) {
+ return match.getFile().equals(mEditedFile);
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java
new file mode 100644
index 000000000..3df2feda3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+
+import java.util.Map;
+
+/**
+ * Interface implemented by clients who embed a {@link ConfigurationChooser}.
+ */
+public interface ConfigurationClient {
+ /**
+ * The configuration is about to be changed.
+ *
+ * @param flags details about what changed; consult the {@code CFG_} flags
+ * in {@link Configuration} such as
+ * {@link Configuration#CFG_DEVICE},
+ * {@link Configuration#CFG_LOCALE}, etc.
+ */
+ void aboutToChange(int flags);
+
+ /**
+ * The configuration has changed. If the client returns false, it means that
+ * the change was rejected. This typically means that changing the
+ * configuration in this particular way makes a configuration which has a
+ * better file match than the current client's file, so it will open that
+ * file to edit the new configuration -- and the current configuration
+ * should go back to editing the state prior to this change.
+ *
+ * @param flags details about what changed; consult the {@code CFG_} flags
+ * such as {@link Configuration#CFG_DEVICE},
+ * {@link Configuration#CFG_LOCALE}, etc.
+ * @return true if the change was accepted, false if it was rejected.
+ */
+ boolean changed(int flags);
+
+ /**
+ * Compute the project resources
+ *
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getProjectResources();
+
+ /**
+ * Compute the framework resources
+ *
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getFrameworkResources();
+
+ /**
+ * Compute the framework resources for the given Android API target
+ *
+ * @param target the target to look up framework resources for
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target);
+
+ /**
+ * Returns the configured project resources for the current file and
+ * configuration
+ *
+ * @return resource type maps to names to resource values
+ */
+ @NonNull
+ Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
+
+ /**
+ * Returns the configured framework resources for the current file and
+ * configuration
+ *
+ * @return resource type maps to names to resource values
+ */
+ @NonNull
+ Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
+
+ /**
+ * If the current layout is an included layout rendered within an outer layout,
+ * returns the outer layout.
+ *
+ * @return the outer including layout, or null
+ */
+ @Nullable
+ Reference getIncludedWithin();
+
+ /**
+ * Called when the "Create" button is clicked.
+ */
+ void createConfigFile();
+
+ /**
+ * Called when an associated activity is picked
+ *
+ * @param fqcn the fully qualified class name for the associated activity context
+ */
+ void setActivity(@NonNull String fqcn);
+
+ /**
+ * Returns the associated layout canvas, if any
+ *
+ * @return the canvas, if any
+ */
+ @Nullable
+ LayoutCanvas getCanvas();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java
new file mode 100644
index 000000000..956ac1839
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_THEME;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.NightMode;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.google.common.base.Splitter;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.QualifiedName;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.List;
+
+/** A description of a configuration, used for persistence */
+public class ConfigurationDescription {
+ private static final String TAG_PREVIEWS = "previews"; //$NON-NLS-1$
+ private static final String TAG_PREVIEW = "preview"; //$NON-NLS-1$
+ private static final String ATTR_TARGET = "target"; //$NON-NLS-1$
+ private static final String ATTR_CONFIG = "config"; //$NON-NLS-1$
+ private static final String ATTR_LOCALE = "locale"; //$NON-NLS-1$
+ private static final String ATTR_ACTIVITY = "activity"; //$NON-NLS-1$
+ private static final String ATTR_DEVICE = "device"; //$NON-NLS-1$
+ private static final String ATTR_STATE = "devicestate"; //$NON-NLS-1$
+ private static final String ATTR_UIMODE = "ui"; //$NON-NLS-1$
+ private static final String ATTR_NIGHTMODE = "night"; //$NON-NLS-1$
+ private final static String SEP_LOCALE = "-"; //$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$
+
+ /** The project corresponding to this configuration's description */
+ public final IProject project;
+
+ /** The display name */
+ public String displayName;
+
+ /** The theme */
+ public String theme;
+
+ /** The target */
+ public IAndroidTarget target;
+
+ /** The display name */
+ public FolderConfiguration folder;
+
+ /** The locale */
+ public Locale locale = Locale.ANY;
+
+ /** The device */
+ public Device device;
+
+ /** The device state */
+ public State state;
+
+ /** The activity */
+ public String activity;
+
+ /** UI mode */
+ @NonNull
+ public UiMode uiMode = UiMode.NORMAL;
+
+ /** Night mode */
+ @NonNull
+ public NightMode nightMode = NightMode.NOTNIGHT;
+
+ private ConfigurationDescription(@Nullable IProject project) {
+ this.project = project;
+ }
+
+ /**
+ * Returns the persistent configuration description from the given file
+ *
+ * @param file the file to look up a description from
+ * @return the description or null if never written
+ */
+ @Nullable
+ public static String getDescription(@NonNull IFile file) {
+ return AdtPlugin.getFileProperty(file, NAME_CONFIG_STATE);
+ }
+
+ /**
+ * Sets the persistent configuration description data for the given file
+ *
+ * @param file the file to associate the description with
+ * @param description the description
+ */
+ public static void setDescription(@NonNull IFile file, @NonNull String description) {
+ AdtPlugin.setFileProperty(file, NAME_CONFIG_STATE, description);
+ }
+
+ /**
+ * Creates a description from a given configuration
+ *
+ * @param project the project for this configuration's description
+ * @param configuration the configuration to describe
+ * @return a new configuration
+ */
+ public static ConfigurationDescription fromConfiguration(
+ @Nullable IProject project,
+ @NonNull Configuration configuration) {
+ ConfigurationDescription description = new ConfigurationDescription(project);
+ description.displayName = configuration.getDisplayName();
+ description.theme = configuration.getTheme();
+ description.target = configuration.getTarget();
+ description.folder = new FolderConfiguration();
+ description.folder.set(configuration.getFullConfig());
+ description.locale = configuration.getLocale();
+ description.device = configuration.getDevice();
+ description.state = configuration.getDeviceState();
+ description.activity = configuration.getActivity();
+ return description;
+ }
+
+ /**
+ * Initializes a string previously created with
+ * {@link #toXml(Document)}
+ *
+ * @param project the project for this configuration's description
+ * @param element the element to read back from
+ * @param deviceList list of available devices
+ * @return true if the configuration was initialized
+ */
+ @Nullable
+ public static ConfigurationDescription fromXml(
+ @Nullable IProject project,
+ @NonNull Element element,
+ @NonNull Collection<Device> deviceList) {
+ ConfigurationDescription description = new ConfigurationDescription(project);
+
+ if (!TAG_PREVIEW.equals(element.getTagName())) {
+ return null;
+ }
+
+ String displayName = element.getAttribute(ATTR_NAME);
+ if (!displayName.isEmpty()) {
+ description.displayName = displayName;
+ }
+
+ String config = element.getAttribute(ATTR_CONFIG);
+ Iterable<String> segments = Splitter.on('-').split(config);
+ description.folder = FolderConfiguration.getConfig(segments);
+
+ String theme = element.getAttribute(ATTR_THEME);
+ if (!theme.isEmpty()) {
+ description.theme = theme;
+ }
+
+ String targetId = element.getAttribute(ATTR_TARGET);
+ if (!targetId.isEmpty()) {
+ IAndroidTarget target = Configuration.stringToTarget(targetId);
+ description.target = target;
+ }
+
+ String localeString = element.getAttribute(ATTR_LOCALE);
+ if (!localeString.isEmpty()) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+ String locales[] = localeString.split(SEP_LOCALE);
+ if (locales[0].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ if (locales.length >= 2 && locales[1].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[1])) {
+ description.locale = Locale.create(LocaleQualifier.getQualifier(language + "-r" + locales[1]));
+ } else {
+ description.locale = Locale.create(new LocaleQualifier(language));
+ }
+ } else {
+ description.locale = Locale.ANY;
+ }
+
+
+ }
+
+ String activity = element.getAttribute(ATTR_ACTIVITY);
+ if (activity.isEmpty()) {
+ activity = null;
+ }
+
+ String deviceString = element.getAttribute(ATTR_DEVICE);
+ if (!deviceString.isEmpty()) {
+ for (Device d : deviceList) {
+ if (d.getName().equals(deviceString)) {
+ description.device = d;
+ String stateName = element.getAttribute(ATTR_STATE);
+ if (stateName.isEmpty() || stateName.equals("null")) {
+ description.state = Configuration.getState(d, stateName);
+ } else if (d.getAllStates().size() > 0) {
+ description.state = d.getAllStates().get(0);
+ }
+ break;
+ }
+ }
+ }
+
+ String uiModeString = element.getAttribute(ATTR_UIMODE);
+ if (!uiModeString.isEmpty()) {
+ description.uiMode = UiMode.getEnum(uiModeString);
+ if (description.uiMode == null) {
+ description.uiMode = UiMode.NORMAL;
+ }
+ }
+
+ String nightModeString = element.getAttribute(ATTR_NIGHTMODE);
+ if (!nightModeString.isEmpty()) {
+ description.nightMode = NightMode.getEnum(nightModeString);
+ if (description.nightMode == null) {
+ description.nightMode = NightMode.NOTNIGHT;
+ }
+ }
+
+
+ // Should I really be storing the FULL configuration? Might be trouble if
+ // you bring a different device
+
+ return description;
+ }
+
+ /**
+ * Write this description into the given document as a new element.
+ *
+ * @param document the document to add the description to
+ * @return the newly inserted element
+ */
+ @NonNull
+ public Element toXml(Document document) {
+ Element element = document.createElement(TAG_PREVIEW);
+
+ element.setAttribute(ATTR_NAME, displayName);
+ FolderConfiguration fullConfig = folder;
+ String folderName = fullConfig.getFolderName(ResourceFolderType.LAYOUT);
+ element.setAttribute(ATTR_CONFIG, folderName);
+ if (theme != null) {
+ element.setAttribute(ATTR_THEME, theme);
+ }
+ if (target != null) {
+ element.setAttribute(ATTR_TARGET, Configuration.targetToString(target));
+ }
+
+ if (locale != null && (locale.hasLanguage() || locale.hasRegion())) {
+ String value;
+ if (locale.hasRegion()) {
+ value = locale.qualifier.getLanguage() + SEP_LOCALE + locale.qualifier.getRegion();
+ } else {
+ value = locale.qualifier.getLanguage();
+ }
+ element.setAttribute(ATTR_LOCALE, value);
+ }
+
+ if (device != null) {
+ element.setAttribute(ATTR_DEVICE, device.getName());
+ if (state != null) {
+ element.setAttribute(ATTR_STATE, state.getName());
+ }
+ }
+
+ if (activity != null) {
+ element.setAttribute(ATTR_ACTIVITY, activity);
+ }
+
+ if (uiMode != null && uiMode != UiMode.NORMAL) {
+ element.setAttribute(ATTR_UIMODE, uiMode.getResourceValue());
+ }
+
+ if (nightMode != null && nightMode != NightMode.NOTNIGHT) {
+ element.setAttribute(ATTR_NIGHTMODE, nightMode.getResourceValue());
+ }
+
+ Element parent = document.getDocumentElement();
+ if (parent == null) {
+ parent = document.createElement(TAG_PREVIEWS);
+ document.appendChild(parent);
+ }
+ parent.appendChild(element);
+
+ return element;
+ }
+
+ /** Returns the preferred theme, or null */
+ @Nullable
+ String computePreferredTheme() {
+ if (project == null) {
+ return "Theme";
+ }
+ ManifestInfo manifest = ManifestInfo.get(project);
+
+ // Look up the screen size for the current state
+ ScreenSize screenSize = null;
+ if (device != null) {
+ List<State> states = device.getAllStates();
+ for (State s : states) {
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s);
+ if (folderConfig != null) {
+ ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier();
+ screenSize = qualifier.getValue();
+ break;
+ }
+ }
+ }
+
+ // Look up the default/fallback theme to use for this project (which
+ // depends on the screen size when no particular theme is specified
+ // in the manifest)
+ String defaultTheme = manifest.getDefaultTheme(target, screenSize);
+
+ String preferred = defaultTheme;
+ if (theme == null) {
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ }
+ if (preferred == null) {
+ preferred = defaultTheme;
+ }
+ theme = preferred;
+ }
+
+ return preferred;
+ }
+
+ private void checkThemePrefix() {
+ if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (theme.isEmpty()) {
+ computePreferredTheme();
+ return;
+ }
+
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AndroidTargetData data = sdk.getTargetData(target);
+
+ if (data != null) {
+ ResourceRepository resources = data.getFrameworkResources();
+ if (resources != null
+ && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ return;
+ }
+ }
+ }
+ }
+
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java
new file mode 100644
index 000000000..9724d4015
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java
@@ -0,0 +1,843 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.NightModeQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.common.resources.configuration.UiModeQualifier;
+import com.android.ide.common.resources.configuration.VersionQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.resources.Density;
+import com.android.resources.NightMode;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.android.sdklib.repository.PkgProps;
+import com.android.utils.Pair;
+import com.android.utils.SparseIntArray;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.ui.IEditorPart;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Produces matches for configurations
+ * <p>
+ * See algorithm described here:
+ * http://developer.android.com/guide/topics/resources/providing-resources.html
+ */
+public class ConfigurationMatcher {
+ private static final boolean PREFER_RECENT_RENDER_TARGETS = true;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final Configuration mConfiguration;
+ private final IFile mEditedFile;
+ private final ProjectResources mResources;
+ private final boolean mUpdateUi;
+
+ ConfigurationMatcher(ConfigurationChooser chooser) {
+ this(chooser, chooser.getConfiguration(), chooser.getEditedFile(),
+ chooser.getResources(), true);
+ }
+
+ ConfigurationMatcher(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration,
+ @Nullable IFile editedFile,
+ @Nullable ProjectResources resources,
+ boolean updateUi) {
+ mConfigChooser = chooser;
+ mConfiguration = configuration;
+ mEditedFile = editedFile;
+ mResources = resources;
+ mUpdateUi = updateUi;
+ }
+
+ // ---- Finding matching configurations ----
+
+ private static class ConfigBundle {
+ private final FolderConfiguration config;
+ private int localeIndex;
+ private int dockModeIndex;
+ private int nightModeIndex;
+
+ private ConfigBundle() {
+ config = new FolderConfiguration();
+ }
+
+ private ConfigBundle(ConfigBundle bundle) {
+ config = new FolderConfiguration();
+ config.set(bundle.config);
+ localeIndex = bundle.localeIndex;
+ dockModeIndex = bundle.dockModeIndex;
+ nightModeIndex = bundle.nightModeIndex;
+ }
+ }
+
+ private static class ConfigMatch {
+ final FolderConfiguration testConfig;
+ final Device device;
+ final State state;
+ final ConfigBundle bundle;
+
+ public ConfigMatch(@NonNull FolderConfiguration testConfig, @NonNull Device device,
+ @NonNull State state, @NonNull ConfigBundle bundle) {
+ this.testConfig = testConfig;
+ this.device = device;
+ this.state = state;
+ this.bundle = bundle;
+ }
+
+ @Override
+ public String toString() {
+ return device.getName() + " - " + state.getName();
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
+ ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
+ ResourceType.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;
+ }
+
+ /**
+ * Adapts the current device/config selection so that it's compatible with
+ * the configuration.
+ * <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(boolean)}
+ */
+ 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;
+ State selectedState = mConfiguration.getDeviceState();
+ FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
+ if (selectedState != null) {
+ FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState);
+ if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) {
+ currentConfigIsCompatible = true; // current config is compatible
+ if (!needBestMatch || isCurrentFileBestMatchFor(currentConfig)) {
+ needConfigChange = false;
+ }
+ }
+ }
+
+ if (needConfigChange) {
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+
+ // if the current state/locale isn't a correct match, then
+ // look for another state/locale in the same device.
+ FolderConfiguration testConfig = new FolderConfiguration();
+
+ // first look in the current device.
+ State matchState = null;
+ int localeIndex = -1;
+ Device device = mConfiguration.getDevice();
+ if (device != null) {
+ mainloop: for (State state : device.getAllStates()) {
+ testConfig.set(DeviceConfigHelper.getFolderConfig(state));
+
+ // loop on the locales.
+ for (int i = 0 ; i < localeList.size() ; i++) {
+ Locale locale = localeList.get(i);
+
+ // update the test config with the locale qualifiers
+ testConfig.setLocaleQualifier(locale.qualifier);
+
+
+ if (editedConfig.isMatchFor(testConfig) &&
+ isCurrentFileBestMatchFor(testConfig)) {
+ matchState = state;
+ localeIndex = i;
+ break mainloop;
+ }
+ }
+ }
+ }
+
+ if (matchState != null) {
+ mConfiguration.setDeviceState(matchState, true);
+ Locale locale = localeList.get(localeIndex);
+ mConfiguration.setLocale(locale, true);
+ if (mUpdateUi) {
+ mConfigChooser.selectDeviceState(matchState);
+ mConfigChooser.selectLocale(locale);
+ }
+ mConfiguration.syncFolderConfig();
+ } else {
+ // no match in current device with any state/locale
+ // attempt to find another device that can display this
+ // particular state.
+ findAndSetCompatibleConfig(currentConfigIsCompatible);
+ }
+ }
+ }
+
+ /**
+ * Finds a device/config that can display a configuration.
+ * <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.
+ */
+ void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+ Collection<Device> devices = mConfigChooser.getDevices();
+ FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
+ FolderConfiguration currentConfig = mConfiguration.getFullConfig();
+
+ // list of compatible device/state/locale
+ List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
+
+ // list of actual best match (ie the file is a best match for the
+ // device/state)
+ List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
+
+ // 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, night-mode, car dock.
+ List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
+
+ // 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 (editedConfig.getLocaleQualifier() != null || localeHostMatch == -1) {
+ // add all the locales
+ start = 0;
+ max = localeList.size();
+ } else {
+ // only add the locale host match
+ start = localeHostMatch;
+ max = localeHostMatch + 1; // test is <
+ }
+
+ for (int i = start ; i < max ; i++) {
+ Locale l = localeList.get(i);
+
+ ConfigBundle bundle = new ConfigBundle();
+ bundle.config.setLocaleQualifier(l.qualifier);
+
+ bundle.localeIndex = i;
+ configBundles.add(bundle);
+ }
+
+ // add the dock mode to the bundle combinations.
+ addDockModeToBundles(configBundles);
+
+ // add the night mode to the bundle combinations.
+ addNightModeToBundles(configBundles);
+
+ addRenderTargetToBundles(configBundles);
+
+ for (Device device : devices) {
+ for (State state : device.getAllStates()) {
+
+ // loop on the list of config bundles to create full
+ // configurations.
+ FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state);
+ for (ConfigBundle bundle : configBundles) {
+ // create a new config with device config
+ FolderConfiguration testConfig = new FolderConfiguration();
+ testConfig.set(stateConfig);
+
+ // add on top of it, the extra qualifiers from the bundle
+ testConfig.add(bundle.config);
+
+ if (editedConfig.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.
+ anyMatches.add(new ConfigMatch(testConfig, device, state, bundle));
+
+ if (isCurrentFileBestMatchFor(testConfig)) {
+ // this is what we want.
+ bestMatches.add(new ConfigMatch(testConfig, device, state, bundle));
+ }
+ }
+ }
+ }
+ }
+
+ if (bestMatches.size() == 0) {
+ if (favorCurrentConfig) {
+ // quick check
+ if (!editedConfig.isMatchFor(currentConfig)) {
+ 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.",
+ editedConfig.toDisplayString()),
+ String.format(
+ "Displaying it with '%1$s'",
+ currentConfig.toDisplayString()));
+ } else if (anyMatches.size() > 0) {
+ // select the best device anyway.
+ ConfigMatch match = selectConfigMatch(anyMatches);
+ mConfiguration.setDevice(match.device, true);
+ mConfiguration.setDeviceState(match.state, true);
+ mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
+ mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
+ mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex),
+ true);
+
+ if (mUpdateUi) {
+ mConfigChooser.selectDevice(mConfiguration.getDevice());
+ mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
+ mConfigChooser.selectLocale(mConfiguration.getLocale());
+ }
+
+ mConfiguration.syncFolderConfig();
+
+ // TODO: display a better warning!
+ AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
+ String.format(
+ "'%1$s' is not a best match for any device/locale combination.",
+ editedConfig.toDisplayString()),
+ String.format(
+ "Displaying it with '%1$s' which is compatible, but will " +
+ "actually be displayed with another more specific version of " +
+ "the layout.",
+ currentConfig.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 state,
+ // and replace whatever qualifier required by the layout file.
+ }
+ } else {
+ ConfigMatch match = selectConfigMatch(bestMatches);
+ mConfiguration.setDevice(match.device, true);
+ mConfiguration.setDeviceState(match.state, true);
+ mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
+ mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
+ mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true);
+
+ mConfiguration.syncFolderConfig();
+
+ if (mUpdateUi) {
+ mConfigChooser.selectDevice(mConfiguration.getDevice());
+ mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
+ mConfigChooser.selectLocale(mConfiguration.getLocale());
+ }
+ }
+ }
+
+ private void addRenderTargetToBundles(List<ConfigBundle> configBundles) {
+ Pair<Locale, IAndroidTarget> state = Configuration.loadRenderState(mConfigChooser);
+ if (state != null) {
+ IAndroidTarget target = state.getSecond();
+ if (target != null) {
+ int apiLevel = target.getVersion().getApiLevel();
+ for (ConfigBundle bundle : configBundles) {
+ bundle.config.setVersionQualifier(
+ new VersionQualifier(apiLevel));
+ }
+ }
+ }
+ }
+
+ 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 (UiMode mode : UiMode.values()) {
+ ConfigBundle b = new ConfigBundle(bundle);
+ b.config.setUiModeQualifier(new UiModeQualifier(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);
+ }
+
+ private int getLocaleMatch() {
+ java.util.Locale defaultLocale = java.util.Locale.getDefault();
+ if (defaultLocale != null) {
+ String currentLanguage = defaultLocale.getLanguage();
+ String currentRegion = defaultLocale.getCountry();
+
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+ final int count = localeList.size();
+ for (int l = 0; l < count; l++) {
+ Locale locale = localeList.get(l);
+ LocaleQualifier qualifier = locale.qualifier;
+
+ // 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 (qualifier.getLanguage().equals(currentLanguage) &&
+ (qualifier.getRegion() == null || qualifier.getRegion().equals(currentRegion))) {
+ 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;
+ }
+
+ private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
+ // API 11-13: look for a x-large device
+ Comparator<ConfigMatch> comparator = null;
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ IAndroidTarget projectTarget = sdk.getTarget(mEditedFile.getProject());
+ if (projectTarget != null) {
+ int apiLevel = projectTarget.getVersion().getApiLevel();
+ if (apiLevel >= 11 && apiLevel < 14) {
+ // TODO: Maybe check the compatible-screen tag in the manifest to figure out
+ // what kind of device should be used for display.
+ comparator = new TabletConfigComparator();
+ }
+ }
+ }
+ if (comparator == null) {
+ // lets look for a high density device
+ comparator = new PhoneConfigComparator();
+ }
+ Collections.sort(matches, comparator);
+
+ // Look at the currently active editor to see if it's a layout editor, and if so,
+ // look up its configuration and if the configuration is in our match list,
+ // use it. This means we "preserve" the current configuration when you open
+ // new layouts.
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ if (delegate != null
+ // (Only do this when the two files are in the same project)
+ && delegate.getEditor().getProject() == mEditedFile.getProject()) {
+ FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration();
+ if (configuration != null) {
+ for (ConfigMatch match : matches) {
+ if (configuration.equals(match.testConfig)) {
+ return match;
+ }
+ }
+ }
+ }
+
+ // the list has been sorted so that the first item is the best config
+ return matches.get(0);
+ }
+
+ /** Return the default render target to use, or null if no strong preference */
+ @Nullable
+ static IAndroidTarget findDefaultRenderTarget(ConfigurationChooser chooser) {
+ if (PREFER_RECENT_RENDER_TARGETS) {
+ // Use the most recent target
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (!targetList.isEmpty()) {
+ return targetList.get(targetList.size() - 1);
+ }
+ }
+
+ IProject project = chooser.getProject();
+ // Default to layoutlib version 5
+ Sdk current = Sdk.getCurrent();
+ if (current != null) {
+ IAndroidTarget projectTarget = current.getTarget(project);
+ int minProjectApi = Integer.MAX_VALUE;
+ if (projectTarget != null) {
+ if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) {
+ // Renderable non-platform targets are all going to be adequate (they
+ // will have at least version 5 of layoutlib) so use the project
+ // target as the render target.
+ return projectTarget;
+ }
+
+ if (projectTarget.getVersion().isPreview()
+ && projectTarget.hasRenderingLibrary()) {
+ // If the project target is a preview version, then just use it
+ return projectTarget;
+ }
+
+ minProjectApi = projectTarget.getVersion().getApiLevel();
+ }
+
+ // We want to pick a render target that contains at least version 5 (and
+ // preferably version 6) of the layout library. To do this, we go through the
+ // targets and pick the -smallest- API level that is both simultaneously at
+ // least as big as the project API level, and supports layoutlib level 5+.
+ IAndroidTarget best = null;
+ int bestApiLevel = Integer.MAX_VALUE;
+
+ for (IAndroidTarget target : current.getTargets()) {
+ // Non-platform targets are not chosen as the default render target
+ if (!target.isPlatform()) {
+ continue;
+ }
+
+ int apiLevel = target.getVersion().getApiLevel();
+
+ // Ignore targets that have a lower API level than the minimum project
+ // API level:
+ if (apiLevel < minProjectApi) {
+ continue;
+ }
+
+ // Look up the layout lib API level. This property is new so it will only
+ // be defined for version 6 or higher, which means non-null is adequate
+ // to see if this target is eligible:
+ String property = target.getProperty(PkgProps.LAYOUTLIB_API);
+ // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate:
+ if (property != null || apiLevel >= 11) {
+ if (apiLevel < bestApiLevel) {
+ bestApiLevel = apiLevel;
+ best = target;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempts to find a close state among a list
+ *
+ * @param oldConfig the reference config.
+ * @param states the list of states to search through
+ * @return the name of the closest state match, or possibly null if no states are compatible
+ * (this can only happen if the states don't have a single qualifier that is the same).
+ */
+ @Nullable
+ static String getClosestMatch(@NonNull FolderConfiguration oldConfig,
+ @NonNull List<State> states) {
+
+ // create 2 lists as we're going to go through one and put the
+ // candidates in the other.
+ List<State> list1 = new ArrayList<State>(states.size());
+ List<State> list2 = new ArrayList<State>(states.size());
+
+ list1.addAll(states);
+
+ final int count = FolderConfiguration.getQualifierCount();
+ for (int i = 0 ; i < count ; i++) {
+ // compute the new candidate list by only taking states that have
+ // the same i-th qualifier as the old state
+ for (State s : list1) {
+ ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
+
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s);
+ ResourceQualifier newQualifier =
+ folderConfig != null ? folderConfig.getQualifier(i) : null;
+
+ if (oldQualifier == null) {
+ if (newQualifier == null) {
+ list2.add(s);
+ }
+ } else if (oldQualifier.equals(newQualifier)) {
+ list2.add(s);
+ }
+ }
+
+ // 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 states failed. It is considered ok, and
+ // we move to the next qualifier anyway. This way, if a qualifier is different for
+ // all new states 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 state and it doesn't matter,
+ // we take the first one).
+ if (list1.size() > 0) {
+ return list1.get(0).getName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the layout {@link IFile} which best matches the configuration
+ * selected in the given configuration chooser.
+ *
+ * @param chooser the associated configuration chooser holding project state
+ * @return the file which best matches the settings
+ */
+ @Nullable
+ public static IFile getBestFileMatch(ConfigurationChooser chooser) {
+ // get the resources of the file's project.
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(chooser.getProject());
+ if (resources == null) {
+ return null;
+ }
+
+ // From the resources, look for a matching file
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile == null) {
+ return null;
+ }
+ String name = editedFile.getName();
+ FolderConfiguration config = chooser.getConfiguration().getFullConfig();
+ ResourceFile match = resources.getMatchingFile(name, ResourceType.LAYOUT, config);
+
+ if (match != null) {
+ // In Eclipse, the match's file is always an instance of IFileWrapper
+ return ((IFileWrapper) match.getFile()).getIFile();
+ }
+
+ return null;
+ }
+
+ /**
+ * Note: this comparator imposes orderings that are inconsistent with equals.
+ */
+ private static class TabletConfigComparator implements Comparator<ConfigMatch> {
+ @Override
+ public int compare(ConfigMatch o1, ConfigMatch o2) {
+ FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
+ FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
+ if (config1 == null) {
+ if (config2 == null) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (config2 == null) {
+ return 1;
+ }
+
+ ScreenSizeQualifier size1 = config1.getScreenSizeQualifier();
+ ScreenSizeQualifier size2 = config2.getScreenSizeQualifier();
+ ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL;
+ ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL;
+
+ // X-LARGE is better than all others (which are considered identical)
+ // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
+
+ if (ss1 == ScreenSize.XLARGE) {
+ if (ss2 == ScreenSize.XLARGE) {
+ ScreenOrientationQualifier orientation1 =
+ config1.getScreenOrientationQualifier();
+ ScreenOrientation so1 = orientation1.getValue();
+ if (so1 == null) {
+ so1 = ScreenOrientation.PORTRAIT;
+ }
+ ScreenOrientationQualifier orientation2 =
+ config2.getScreenOrientationQualifier();
+ ScreenOrientation so2 = orientation2.getValue();
+ if (so2 == null) {
+ so2 = ScreenOrientation.PORTRAIT;
+ }
+
+ if (so1 == ScreenOrientation.LANDSCAPE) {
+ if (so2 == ScreenOrientation.LANDSCAPE) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (so2 == ScreenOrientation.LANDSCAPE) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else {
+ return -1;
+ }
+ } else if (ss2 == ScreenSize.XLARGE) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Note: this comparator imposes orderings that are inconsistent with equals.
+ */
+ private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
+
+ private final SparseIntArray mDensitySort = new SparseIntArray(4);
+
+ public PhoneConfigComparator() {
+ // put the sort order for the density.
+ mDensitySort.put(Density.HIGH.getDpiValue(), 1);
+ mDensitySort.put(Density.MEDIUM.getDpiValue(), 2);
+ mDensitySort.put(Density.XHIGH.getDpiValue(), 3);
+ mDensitySort.put(Density.LOW.getDpiValue(), 4);
+ }
+
+ @Override
+ public int compare(ConfigMatch o1, ConfigMatch o2) {
+ FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
+ FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
+ if (config1 == null) {
+ if (config2 == null) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (config2 == null) {
+ return 1;
+ }
+
+ int dpi1 = Density.DEFAULT_DENSITY;
+ int dpi2 = Density.DEFAULT_DENSITY;
+
+ DensityQualifier dpiQualifier1 = config1.getDensityQualifier();
+ if (dpiQualifier1 != null) {
+ Density value = dpiQualifier1.getValue();
+ dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
+ }
+ dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/);
+
+ DensityQualifier dpiQualifier2 = config2.getDensityQualifier();
+ if (dpiQualifier2 != null) {
+ Density value = dpiQualifier2.getValue();
+ dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
+ }
+ dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/);
+
+ if (dpi1 == dpi2) {
+ // portrait is better
+ ScreenOrientation so1 = ScreenOrientation.PORTRAIT;
+ ScreenOrientationQualifier orientationQualifier1 =
+ config1.getScreenOrientationQualifier();
+ if (orientationQualifier1 != null) {
+ so1 = orientationQualifier1.getValue();
+ if (so1 == null) {
+ so1 = ScreenOrientation.PORTRAIT;
+ }
+ }
+ ScreenOrientation so2 = ScreenOrientation.PORTRAIT;
+ ScreenOrientationQualifier orientationQualifier2 =
+ config2.getScreenOrientationQualifier();
+ if (orientationQualifier2 != null) {
+ so2 = orientationQualifier2.getValue();
+ if (so2 == null) {
+ so2 = ScreenOrientation.PORTRAIT;
+ }
+ }
+
+ if (so1 == ScreenOrientation.PORTRAIT) {
+ if (so2 == ScreenOrientation.PORTRAIT) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (so2 == ScreenOrientation.PORTRAIT) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ return dpi1 - dpi2;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java
new file mode 100644
index 000000000..a791c63f8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2012 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.LOCALES;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.VARIATIONS;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+
+import java.util.List;
+
+/**
+ * The {@linkplain ConfigurationMenuListener} class is responsible for
+ * generating the configuration menu in the {@link ConfigurationChooser}.
+ */
+class ConfigurationMenuListener extends SelectionAdapter {
+ private static final String ICON_NEW_CONFIG = "newConfig"; //$NON-NLS-1$
+ private static final int ACTION_SELECT_CONFIG = 1;
+ private static final int ACTION_CREATE_CONFIG_FILE = 2;
+ private static final int ACTION_ADD = 3;
+ private static final int ACTION_DELETE_ALL = 4;
+ private static final int ACTION_PREVIEW_MODE = 5;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final IFile mResource;
+ private final RenderPreviewMode mMode;
+
+ ConfigurationMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable IFile resource,
+ @Nullable RenderPreviewMode mode) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mResource = resource;
+ mMode = mode;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_SELECT_CONFIG: {
+ try {
+ AdtPlugin.openFile(mResource, null, false);
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, null);
+ }
+ return;
+ }
+ case ACTION_CREATE_CONFIG_FILE: {
+ ConfigurationClient client = mConfigChooser.getClient();
+ if (client != null) {
+ client.createConfigFile();
+ }
+ return;
+ }
+ }
+
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ IFile editedFile = mConfigChooser.getEditedFile();
+
+ if (delegate == null || editedFile == null) {
+ return;
+ }
+ // (Only do this when the two files are in the same project)
+ IProject project = delegate.getEditor().getProject();
+ if (project == null ||
+ !project.equals(editedFile.getProject())) {
+ return;
+ }
+ LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl();
+ RenderPreviewManager previewManager = canvas.getPreviewManager();
+
+ switch (mAction) {
+ case ACTION_ADD: {
+ previewManager.addAsThumbnail();
+ break;
+ }
+ case ACTION_PREVIEW_MODE: {
+ previewManager.selectMode(mMode);
+ break;
+ }
+ case ACTION_DELETE_ALL: {
+ previewManager.deleteManualPreviews();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ canvas.setFitScale(true /*onlyZoomOut*/, false /*allowZoomIn*/);
+ canvas.redraw();
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ RenderPreviewMode mode = AdtPrefs.getPrefs().getRenderPreviewMode();
+
+ // Configuration Previews
+ create(menu, "Add As Thumbnail...",
+ new ConfigurationMenuListener(chooser, ACTION_ADD, null, null),
+ SWT.PUSH, false);
+ if (mode == RenderPreviewMode.CUSTOM) {
+ MenuItem item = create(menu, "Delete All Thumbnails",
+ new ConfigurationMenuListener(chooser, ACTION_DELETE_ALL, null, null),
+ SWT.PUSH, false);
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ if (delegate != null) {
+ LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl();
+ RenderPreviewManager previewManager = canvas.getPreviewManager();
+ if (!previewManager.hasManualPreviews()) {
+ item.setEnabled(false);
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem configSeparator = new MenuItem(menu, SWT.SEPARATOR);
+
+ create(menu, "Preview Representative Sample",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ DEFAULT), SWT.RADIO, mode == DEFAULT);
+ create(menu, "Preview All Screen Sizes",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ SCREENS), SWT.RADIO, mode == SCREENS);
+
+ MenuItem localeItem = create(menu, "Preview All Locales",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ LOCALES), SWT.RADIO, mode == LOCALES);
+ if (chooser.getLocaleList().size() <= 1) {
+ localeItem.setEnabled(false);
+ }
+
+ boolean canPreviewIncluded = false;
+ IProject project = chooser.getProject();
+ if (project != null) {
+ IncludeFinder finder = IncludeFinder.get(project);
+ final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
+ canPreviewIncluded = includedBy != null && !includedBy.isEmpty();
+ }
+ //if (!graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ // canPreviewIncluded = false;
+ //}
+ MenuItem includedItem = create(menu, "Preview Included",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ INCLUDES), SWT.RADIO, mode == INCLUDES);
+ if (!canPreviewIncluded) {
+ includedItem.setEnabled(false);
+ }
+
+ IFile file = chooser.getEditedFile();
+ List<IFile> variations = AdtUtils.getResourceVariations(file, true);
+ MenuItem variationsItem = create(menu, "Preview Layout Versions",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ VARIATIONS), SWT.RADIO, mode == VARIATIONS);
+ if (variations.size() <= 1) {
+ variationsItem.setEnabled(false);
+ }
+
+ create(menu, "Manual Previews",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ CUSTOM), SWT.RADIO, mode == CUSTOM);
+ create(menu, "None",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ NONE), SWT.RADIO, mode == NONE);
+
+ if (variations.size() > 1) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ResourceManager manager = ResourceManager.getInstance();
+ for (final IFile resource : variations) {
+ IFolder parent = (IFolder) resource.getParent();
+ ResourceFolder parentResource = manager.getResourceFolder(parent);
+ FolderConfiguration configuration = parentResource.getConfiguration();
+ String title = configuration.toDisplayString();
+
+ MenuItem item = create(menu, title,
+ new ConfigurationMenuListener(chooser, ACTION_SELECT_CONFIG,
+ resource, null),
+ SWT.CHECK, false);
+
+ if (file != null) {
+ boolean selected = file.equals(resource);
+ if (selected) {
+ item.setSelection(true);
+ item.setEnabled(false);
+ }
+ }
+ }
+ }
+
+ Configuration configuration = chooser.getConfiguration();
+ if (configuration.getEditedConfig() != null &&
+ !configuration.getEditedConfig().equals(configuration.getFullConfig())) {
+ if (variations.size() > 0) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ // Add action for creating a new configuration
+ MenuItem item = create(menu, "Create New...",
+ new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE,
+ null, null),
+ SWT.PUSH, false);
+ item.setImage(IconFactory.getInstance().getIcon(ICON_NEW_CONFIG));
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @NonNull
+ public static MenuItem create(@NonNull Menu menu, String title,
+ ConfigurationMenuListener listener, int style, boolean selected) {
+ MenuItem item = new MenuItem(menu, style);
+ item.setText(title);
+ item.addSelectionListener(listener);
+ if (selected) {
+ item.setSelection(true);
+ }
+ return item;
+ }
+
+ @NonNull
+ static MenuItem addTogglePreviewModeAction(
+ @NonNull Menu menu,
+ @NonNull String title,
+ @NonNull ConfigurationChooser chooser,
+ @NonNull RenderPreviewMode mode) {
+ boolean selected = AdtPrefs.getPrefs().getRenderPreviewMode() == mode;
+ if (selected) {
+ mode = RenderPreviewMode.NONE;
+ }
+ return create(menu, title,
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, mode),
+ SWT.CHECK, selected);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java
new file mode 100644
index 000000000..72910f9cc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2012 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 static com.android.ide.common.rendering.HardwareConfigHelper.MANUFACTURER_GENERIC;
+import static com.android.ide.common.rendering.HardwareConfigHelper.getGenericLabel;
+import static com.android.ide.common.rendering.HardwareConfigHelper.getNexusLabel;
+import static com.android.ide.common.rendering.HardwareConfigHelper.isGeneric;
+import static com.android.ide.common.rendering.HardwareConfigHelper.isNexus;
+import static com.android.ide.common.rendering.HardwareConfigHelper.sortNexusList;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The {@linkplain DeviceMenuListener} class is responsible for generating the device
+ * menu in the {@link ConfigurationChooser}.
+ */
+class DeviceMenuListener extends SelectionAdapter {
+ private final ConfigurationChooser mConfigChooser;
+ private final Device mDevice;
+
+ DeviceMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ @Nullable Device device) {
+ mConfigChooser = configChooser;
+ mDevice = device;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mConfigChooser.selectDevice(mDevice);
+ mConfigChooser.onDeviceChange();
+ }
+
+ static void show(final ConfigurationChooser chooser, ToolItem combo) {
+ Configuration configuration = chooser.getConfiguration();
+ Device current = configuration.getDevice();
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+
+ Collection<Device> deviceCollection = chooser.getDevices();
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AvdManager avdManager = sdk.getAvdManager();
+ if (avdManager != null) {
+ boolean separatorNeeded = false;
+ AvdInfo[] avds = avdManager.getValidAvds();
+ for (AvdInfo avd : avds) {
+ for (Device device : deviceCollection) {
+ if (device.getManufacturer().equals(avd.getDeviceManufacturer())
+ && device.getName().equals(avd.getDeviceName())) {
+ separatorNeeded = true;
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(avd.getName());
+ item.setSelection(current == device);
+
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ }
+
+ if (separatorNeeded) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+ }
+ }
+
+ // Group the devices by manufacturer, then put them in the menu.
+ // If we don't have anything but Nexus devices, group them together rather than
+ // make many manufacturer submenus.
+ boolean haveNexus = false;
+ boolean haveNonNexus = false;
+ if (!deviceCollection.isEmpty()) {
+ Map<String, List<Device>> manufacturers = new TreeMap<String, List<Device>>();
+ for (Device device : deviceCollection) {
+ List<Device> devices;
+ if (isNexus(device)) {
+ haveNexus = true;
+ } else if (!isGeneric(device)) {
+ haveNonNexus = true;
+ }
+ if (manufacturers.containsKey(device.getManufacturer())) {
+ devices = manufacturers.get(device.getManufacturer());
+ } else {
+ devices = new ArrayList<Device>();
+ manufacturers.put(device.getManufacturer(), devices);
+ }
+ devices.add(device);
+ }
+ if (haveNonNexus) {
+ for (List<Device> devices : manufacturers.values()) {
+ Menu manufacturerMenu = menu;
+ if (manufacturers.size() > 1) {
+ MenuItem item = new MenuItem(menu, SWT.CASCADE);
+ item.setText(devices.get(0).getManufacturer());
+ manufacturerMenu = new Menu(menu);
+ item.setMenu(manufacturerMenu);
+ }
+ for (final Device device : devices) {
+ MenuItem deviceItem = new MenuItem(manufacturerMenu, SWT.CHECK);
+ deviceItem.setText(getGenericLabel(device));
+ deviceItem.setSelection(current == device);
+ deviceItem.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ } else {
+ List<Device> nexus = new ArrayList<Device>();
+ List<Device> generic = new ArrayList<Device>();
+ if (haveNexus) {
+ // Nexus
+ for (List<Device> devices : manufacturers.values()) {
+ for (Device device : devices) {
+ if (isNexus(device)) {
+ if (device.getManufacturer().equals(MANUFACTURER_GENERIC)) {
+ generic.add(device);
+ } else {
+ nexus.add(device);
+ }
+ } else {
+ generic.add(device);
+ }
+ }
+ }
+ }
+
+ if (!nexus.isEmpty()) {
+ sortNexusList(nexus);
+ for (final Device device : nexus) {
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(getNexusLabel(device));
+ item.setSelection(current == device);
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ // Generate the generic menu.
+ Collections.reverse(generic);
+ for (final Device device : generic) {
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(getGenericLabel(device));
+ item.setSelection(current == device);
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ConfigurationMenuListener.addTogglePreviewModeAction(menu,
+ "Preview All Screens", chooser, RenderPreviewMode.SCREENS);
+
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java
new file mode 100644
index 000000000..15623cf30
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.google.common.collect.Maps;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.wb.internal.core.DesignerPlugin;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The {@linkplain FlagManager} provides access to flags for regions known
+ * to {@link LocaleManager}. It also contains some locale related display
+ * functions.
+ * <p>
+ * All the flag images came from the WindowBuilder subversion repository
+ * http://dev.eclipse.org/svnroot/tools/org.eclipse.windowbuilder/trunk (and in
+ * particular, a snapshot of revision 424). However, it appears that the icons
+ * are from http://www.famfamfam.com/lab/icons/flags/ which states that "these
+ * flag icons are available for free use for any purpose with no requirement for
+ * attribution." Adding the URL here such that we can check back occasionally
+ * and see if there are corrections or updates. Also note that the flag names
+ * are in ISO 3166-1 alpha-2 country codes.
+ */
+public class FlagManager {
+ private static final FlagManager sInstance = new FlagManager();
+
+ /**
+ * Returns the {@linkplain FlagManager} singleton
+ *
+ * @return the {@linkplain FlagManager} singleton, never null
+ */
+ @NonNull
+ public static FlagManager get() {
+ return sInstance;
+ }
+
+ /** Use the {@link #get()} factory method */
+ private FlagManager() {
+ }
+
+ /** Map from region to flag icon */
+ private final Map<String, Image> mImageMap = Maps.newHashMap();
+
+ /**
+ * Returns the empty flag icon used to indicate an unknown country
+ *
+ * @return the globe icon used to indicate an unknown country
+ */
+ public static Image getEmptyIcon() {
+ return DesignerPlugin.getImage("nls/flags/flag_empty.png"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the globe icon used to indicate "any" language
+ *
+ * @return the globe icon used to indicate "any" language
+ */
+ public static Image getGlobeIcon() {
+ return IconFactory.getInstance().getIcon("globe"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the flag for the given language and region.
+ *
+ * @param language the language, or null (if null, region must not be null),
+ * the 2 letter language code (ISO 639-1), in lower case
+ * @param region the region, or null (if null, language must not be null),
+ * the 2 letter region code (ISO 3166-1 alpha-2), in upper case
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@Nullable String language, @Nullable String region) {
+ assert region != null || language != null;
+ if (region == null || region.isEmpty()) {
+ // Look up the region for a given language
+ assert language != null;
+
+ // Special cases where we have a dedicated flag available:
+ if (language.equals("ca")) { //$NON-NLS-1$
+ return getIcon("catalonia"); //$NON-NLS-1$
+ }
+ else if (language.equals("gd")) { //$NON-NLS-1$
+ return getIcon("scotland"); //$NON-NLS-1$
+ }
+ else if (language.equals("cy")) { //$NON-NLS-1$
+ return getIcon("wales"); //$NON-NLS-1$
+ }
+
+ // Prefer the local registration of the current locale; even if
+ // for example the default locale for English is the US, if the current
+ // default locale is English, then use its associated country, which could
+ // for example be Australia.
+ Locale locale = Locale.getDefault();
+ if (language.equals(locale.getLanguage())) {
+ Image flag = getFlag(locale.getCountry());
+ if (flag != null) {
+ return flag;
+ }
+ }
+
+ region = LocaleManager.getLanguageRegion(language);
+ }
+
+ if (region == null || region.isEmpty()) {
+ // No country specified, and the language is for a country we
+ // don't have a flag for
+ return null;
+ }
+
+ return getIcon(region);
+ }
+
+ /**
+ * Returns the flag for the given language and region.
+ *
+ * @param language the language qualifier, or null (if null, region must not be null),
+ * @param region the region, or null (if null, language must not be null),
+ * @return a suitable flag icon, or null
+ */
+ public Image getFlag(@Nullable LocaleQualifier locale) {
+ if (locale == null) {
+ return null;
+ }
+ String languageCode = locale.getLanguage();
+ String regionCode = locale.getRegion();
+ if (LocaleQualifier.FAKE_VALUE.equals(languageCode)) {
+ languageCode = null;
+ }
+ return getFlag(languageCode, regionCode);
+ }
+
+ /**
+ * Returns a flag for a given resource folder name (such as
+ * {@code values-en-rUS}), or null
+ *
+ * @param folder the folder name
+ * @return a corresponding flag icon, or null if none was found
+ */
+ @Nullable
+ public Image getFlagForFolderName(@NonNull String folder) {
+ FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(folder);
+ if (configuration != null) {
+ return get().getFlag(configuration);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the flag for the given folder
+ *
+ * @param configuration the folder configuration
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@NonNull FolderConfiguration configuration) {
+ return getFlag(configuration.getLocaleQualifier());
+ }
+
+
+
+ /**
+ * Returns the flag for the given region.
+ *
+ * @param region the 2 letter region code (ISO 3166-1 alpha-2), in upper case
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@NonNull String region) {
+ assert region.length() == 2
+ && Character.isUpperCase(region.charAt(0))
+ && Character.isUpperCase(region.charAt(1)) : region;
+
+ return getIcon(region);
+ }
+
+ private Image getIcon(@NonNull String base) {
+ Image flagImage = mImageMap.get(base);
+ if (flagImage == null) {
+ // TODO: Special case locale currently running on system such
+ // that the current country matches the current locale
+ if (mImageMap.containsKey(base)) {
+ // Already checked: there's just no image there
+ return null;
+ }
+ String flagFileName = base.toLowerCase(Locale.US) + ".png"; //$NON-NLS-1$
+ flagImage = DesignerPlugin.getImage("nls/flags/" + flagFileName); //$NON-NLS-1$
+ mImageMap.put(base, flagImage);
+ }
+
+ return flagImage;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java
new file mode 100644
index 000000000..97ff66845
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java
@@ -0,0 +1,149 @@
+/*
+ * 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.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode;
+import com.android.resources.ResourceFolderType;
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Dialog to choose a non existing {@link FolderConfiguration}.
+ */
+public final class LayoutCreatorDialog extends GridDialog {
+
+ private ConfigurationSelector mSelector;
+ private Composite mStatusComposite;
+ private Label mStatusLabel;
+ private Label mStatusImage;
+
+ private final FolderConfiguration mConfig = new FolderConfiguration();
+ private final String mFileName;
+
+ /**
+ * Creates a dialog, and init the UI from a {@link FolderConfiguration}.
+ * @param parentShell the parent {@link Shell}.
+ * @param fileName the filename associated with the configuration
+ * @param config The starting configuration.
+ */
+ public LayoutCreatorDialog(Shell parentShell, String fileName, FolderConfiguration config) {
+ super(parentShell, 1, false);
+
+ mFileName = fileName;
+
+ // FIXME: add some data to know what configurations already exist.
+ mConfig.set(config);
+ }
+
+ @Override
+ public void createDialogContent(Composite parent) {
+ new Label(parent, SWT.NONE).setText(
+ String.format("Configuration for the alternate version of %1$s", mFileName));
+
+ mSelector = new ConfigurationSelector(parent, SelectorMode.CONFIG_ONLY);
+ mSelector.setConfiguration(mConfig);
+
+ // because the ConfigSelector is running in CONFIG_ONLY mode, the current config
+ // displayed by it is not mConfig anymore, so get the current config.
+ mSelector.getConfiguration(mConfig);
+
+ // parent's layout is a GridLayout as specified in the javadoc.
+ GridData gd = new GridData();
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mSelector.setLayoutData(gd);
+
+ // add a listener to check on the validity of the FolderConfiguration as
+ // they are built.
+ mSelector.setOnChangeListener(new Runnable() {
+ @Override
+ public void run() {
+ ConfigurationState state = mSelector.getState();
+
+ switch (state) {
+ case OK:
+ mSelector.getConfiguration(mConfig);
+
+ resetStatus();
+ mStatusImage.setImage(null);
+ getButton(IDialogConstants.OK_ID).setEnabled(true);
+ break;
+ case INVALID_CONFIG:
+ ResourceQualifier invalidQualifier = mSelector.getInvalidQualifier();
+ mStatusLabel.setText(String.format(
+ "Invalid Configuration: %1$s has no filter set.",
+ invalidQualifier.getName()));
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ case REGION_WITHOUT_LANGUAGE:
+ mStatusLabel.setText(
+ "The Region qualifier requires the Language qualifier.");
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ }
+
+ // need to relayout, because of the change in size in mErrorImage.
+ mStatusComposite.layout();
+ }
+ });
+
+ mStatusComposite = new Composite(parent, SWT.NONE);
+ mStatusComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout gl = new GridLayout(2, false);
+ mStatusComposite.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mStatusImage = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ resetStatus();
+ }
+
+ /**
+ * Sets the edited configuration on the given configuration parameter
+ *
+ * @param config the configuration to apply the current edits to
+ */
+ public void getConfiguration(FolderConfiguration config) {
+ config.set(mConfig);
+ }
+
+ /**
+ * resets the status label to show the file that will be created.
+ */
+ private void resetStatus() {
+ String displayString = Dialog.shortenText(String.format("New File: res/%1$s/%2$s",
+ mConfig.getFolderName(ResourceFolderType.LAYOUT), mFileName),
+ mStatusLabel);
+ mStatusLabel.setText(displayString);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java
new file mode 100644
index 000000000..6cb396394
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2012 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 static com.android.ide.common.resources.configuration.LocaleQualifier.FAKE_VALUE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * A language,region pair
+ */
+public class Locale {
+ /**
+ * A special marker region qualifier representing any region
+ */
+ public static final LocaleQualifier ANY_QUALIFIER = new LocaleQualifier(FAKE_VALUE);
+
+ /**
+ * A locale which matches any language and region
+ */
+ public static final Locale ANY = new Locale(ANY_QUALIFIER);
+
+ /**
+ * The locale qualifier, or {@link #ANY_QUALIFIER} if this locale matches
+ * any locale
+ */
+ @NonNull
+ public final LocaleQualifier qualifier;
+
+ /**
+ * Constructs a new {@linkplain Locale} matching a given language in a given
+ * locale.
+ *
+ * @param locale the locale
+ */
+ private Locale(@NonNull
+ LocaleQualifier locale) {
+ qualifier = locale;
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} matching a given language in a given
+ * specific locale.
+ *
+ * @param locale the locale
+ * @return a locale with the given locale
+ */
+ @NonNull
+ public static Locale create(@NonNull
+ LocaleQualifier locale) {
+ return new Locale(locale);
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} for the given folder configuration
+ *
+ * @param folder the folder configuration
+ * @return a locale with the given language and region
+ */
+ public static Locale create(FolderConfiguration folder) {
+ LocaleQualifier locale = folder.getLocaleQualifier();
+ if (locale == null) {
+ return ANY;
+ } else {
+ return new Locale(locale);
+ }
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} for the given locale string, e.g.
+ * "zh", "en-rUS", or "b+eng+US".
+ *
+ * @param localeString the locale description
+ * @return the corresponding locale
+ */
+ @NonNull
+ public static Locale create(@NonNull
+ String localeString) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+
+ LocaleQualifier qualifier = LocaleQualifier.getQualifier(localeString);
+ if (qualifier != null) {
+ return new Locale(qualifier);
+ } else {
+ return ANY;
+ }
+ }
+
+ /**
+ * Returns a flag image to use for this locale
+ *
+ * @return a flag image, or a default globe icon
+ */
+ @NonNull
+ public Image getFlagImage() {
+ String languageCode = qualifier.hasLanguage() ? qualifier.getLanguage() : null;
+ if (languageCode == null) {
+ return FlagManager.getGlobeIcon();
+ }
+ String regionCode = hasRegion() ? qualifier.getRegion() : null;
+ FlagManager icons = FlagManager.get();
+ Image image = icons.getFlag(languageCode, regionCode);
+ if (image != null) {
+ return image;
+ } else {
+ return FlagManager.getGlobeIcon();
+ }
+ }
+
+ /**
+ * Returns true if this locale specifies a specific language. This is true
+ * for all locales except {@link #ANY}.
+ *
+ * @return true if this locale specifies a specific language
+ */
+ public boolean hasLanguage() {
+ return !qualifier.hasFakeValue();
+ }
+
+ /**
+ * Returns true if this locale specifies a specific region
+ *
+ * @return true if this locale specifies a region
+ */
+ public boolean hasRegion() {
+ return qualifier.getRegion() != null && !FAKE_VALUE.equals(qualifier.getRegion());
+ }
+
+ /**
+ * Returns the locale formatted as language-region. If region is not set,
+ * language is returned. If language is not set, empty string is returned.
+ */
+ public String toLocaleId() {
+ return qualifier == ANY_QUALIFIER ? "" : qualifier.getTag();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + qualifier.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable
+ Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Locale other = (Locale) obj;
+ if (!qualifier.equals(other.qualifier))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return qualifier.getTag();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java
new file mode 100644
index 000000000..2bc5417b0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.AddTranslationDialog;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+
+/**
+ * The {@linkplain LocaleMenuListener} class is responsible for generating the locale
+ * menu in the {@link ConfigurationChooser}.
+ */
+class LocaleMenuListener extends SelectionAdapter {
+ private static final int ACTION_SET_LOCALE = 1;
+ private static final int ACTION_ADD_TRANSLATION = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final Locale mLocale;
+
+ LocaleMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable Locale locale) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mLocale = locale;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_SET_LOCALE: {
+ mConfigChooser.selectLocale(mLocale);
+ mConfigChooser.onLocaleChange();
+ break;
+ }
+ case ACTION_ADD_TRANSLATION: {
+ IProject project = mConfigChooser.getProject();
+ Shell shell = mConfigChooser.getShell();
+ AddTranslationDialog dialog = new AddTranslationDialog(shell, project);
+ dialog.open();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ }
+
+ static void show(final ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ Configuration configuration = chooser.getConfiguration();
+ List<Locale> locales = chooser.getLocaleList();
+ Locale current = configuration.getLocale();
+
+ for (Locale locale : locales) {
+ String title = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ Image image = locale.getFlagImage();
+ item.setImage(image);
+
+ boolean selected = current == locale;
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ LocaleMenuListener listener = new LocaleMenuListener(chooser, ACTION_SET_LOCALE,
+ locale);
+ item.addSelectionListener(listener);
+ }
+
+ if (locales.size() > 1) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ConfigurationMenuListener.addTogglePreviewModeAction(menu,
+ "Preview All Locales", chooser, RenderPreviewMode.LOCALES);
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ MenuItem item = new MenuItem(menu, SWT.PUSH);
+ item.setText("Add New Translation...");
+ LocaleMenuListener listener = new LocaleMenuListener(chooser,
+ ACTION_ADD_TRANSLATION, null);
+ item.addSelectionListener(listener);
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java
new file mode 100644
index 000000000..50778e2f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.NightMode;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.google.common.base.Objects;
+
+/**
+ * An {@linkplain NestedConfiguration} is a {@link Configuration} which inherits
+ * all of its values from a different configuration, except for one or more
+ * attributes where it overrides a custom value.
+ * <p>
+ * Unlike a {@link VaryingConfiguration}, a {@linkplain NestedConfiguration}
+ * will always return the same overridden value, regardless of the inherited
+ * value.
+ * <p>
+ * For example, an {@linkplain NestedConfiguration} may fix the locale to always
+ * be "en", but otherwise inherit everything else.
+ */
+public class NestedConfiguration extends Configuration {
+ /** The configuration we are inheriting non-overridden values from */
+ protected Configuration mParent;
+
+ /** Bitmask of attributes to be overridden in this configuration */
+ private int mOverride;
+
+ /**
+ * Constructs a new {@linkplain NestedConfiguration}.
+ * Construct via
+ *
+ * @param chooser the associated chooser
+ * @param configuration the configuration to inherit from
+ */
+ protected NestedConfiguration(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration) {
+ super(chooser);
+ mParent = configuration;
+
+ mFullConfig.set(mParent.mFullConfig);
+ if (mParent.getEditedConfig() != null) {
+ mEditedConfig = new FolderConfiguration();
+ mEditedConfig.set(mParent.mEditedConfig);
+ }
+ }
+
+ /**
+ * Returns the override flags for this configuration. Corresponds to
+ * the {@code CFG_} flags in {@link ConfigurationClient}.
+ *
+ * @return the bitmask
+ */
+ public int getOverrideFlags() {
+ return mOverride;
+ }
+
+ /**
+ * Creates a new {@linkplain NestedConfiguration} that has the same overriding
+ * attributes as the given other {@linkplain NestedConfiguration}, and gets
+ * its values from the given {@linkplain Configuration}.
+ *
+ * @param other the configuration to copy overrides from
+ * @param values the configuration to copy values from
+ * @param parent the parent to tie the configuration to for inheriting values
+ * @return a new configuration
+ */
+ @NonNull
+ public static NestedConfiguration create(
+ @NonNull NestedConfiguration other,
+ @NonNull Configuration values,
+ @NonNull Configuration parent) {
+ NestedConfiguration configuration =
+ new NestedConfiguration(other.mConfigChooser, parent);
+ initFrom(configuration, other, values, true /*sync*/);
+ return configuration;
+ }
+
+ /**
+ * Initializes a new {@linkplain NestedConfiguration} with the overriding
+ * attributes as the given other {@linkplain NestedConfiguration}, and gets
+ * its values from the given {@linkplain Configuration}.
+ *
+ * @param configuration the configuration to initialize
+ * @param other the configuration to copy overrides from
+ * @param values the configuration to copy values from
+ * @param sync if true, sync the folder configuration from
+ */
+ protected static void initFrom(NestedConfiguration configuration,
+ NestedConfiguration other, Configuration values, boolean sync) {
+ configuration.mOverride = other.mOverride;
+ configuration.setDisplayName(values.getDisplayName());
+ configuration.setActivity(values.getActivity());
+
+ if (configuration.isOverridingLocale()) {
+ configuration.setLocale(values.getLocale(), true);
+ }
+ if (configuration.isOverridingTarget()) {
+ configuration.setTarget(values.getTarget(), true);
+ }
+ if (configuration.isOverridingDevice()) {
+ configuration.setDevice(values.getDevice(), true);
+ }
+ if (configuration.isOverridingDeviceState()) {
+ configuration.setDeviceState(values.getDeviceState(), true);
+ }
+ if (configuration.isOverridingNightMode()) {
+ configuration.setNightMode(values.getNightMode(), true);
+ }
+ if (configuration.isOverridingUiMode()) {
+ configuration.setUiMode(values.getUiMode(), true);
+ }
+ if (sync) {
+ configuration.syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the parent configuration that this configuration is inheriting from.
+ *
+ * @param parent the parent configuration
+ */
+ public void setParent(@NonNull Configuration parent) {
+ mParent = parent;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} which inherits values from the
+ * given parent {@linkplain Configuration}, possibly overriding some as
+ * well.
+ *
+ * @param chooser the associated chooser
+ * @param parent the configuration to inherit values from
+ * @return a new configuration
+ */
+ @NonNull
+ public static NestedConfiguration create(@NonNull ConfigurationChooser chooser,
+ @NonNull Configuration parent) {
+ return new NestedConfiguration(chooser, parent);
+ }
+
+ @Override
+ @Nullable
+ public String getTheme() {
+ // Never overridden: this is a static attribute of a layout, not something which
+ // varies by configuration or at runtime
+ return mParent.getTheme();
+ }
+
+ @Override
+ public void setTheme(String theme) {
+ // Never overridden
+ mParent.setTheme(theme);
+ }
+
+ /**
+ * Sets whether the locale should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideLocale(boolean override) {
+ mOverride |= CFG_LOCALE;
+ }
+
+ /**
+ * Returns true if the locale is overridden
+ *
+ * @return true if the locale is overridden
+ */
+ public final boolean isOverridingLocale() {
+ return (mOverride & CFG_LOCALE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public Locale getLocale() {
+ if (isOverridingLocale()) {
+ return super.getLocale();
+ } else {
+ return mParent.getLocale();
+ }
+ }
+
+ @Override
+ public void setLocale(@NonNull Locale locale, boolean skipSync) {
+ if (isOverridingLocale()) {
+ super.setLocale(locale, skipSync);
+ } else {
+ mParent.setLocale(locale, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the rendering target should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideTarget(boolean override) {
+ mOverride |= CFG_TARGET;
+ }
+
+ /**
+ * Returns true if the target is overridden
+ *
+ * @return true if the target is overridden
+ */
+ public final boolean isOverridingTarget() {
+ return (mOverride & CFG_TARGET) != 0;
+ }
+
+ @Override
+ @Nullable
+ public IAndroidTarget getTarget() {
+ if (isOverridingTarget()) {
+ return super.getTarget();
+ } else {
+ return mParent.getTarget();
+ }
+ }
+
+ @Override
+ public void setTarget(IAndroidTarget target, boolean skipSync) {
+ if (isOverridingTarget()) {
+ super.setTarget(target, skipSync);
+ } else {
+ mParent.setTarget(target, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the device should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideDevice(boolean override) {
+ mOverride |= CFG_DEVICE;
+ }
+
+ /**
+ * Returns true if the device is overridden
+ *
+ * @return true if the device is overridden
+ */
+ public final boolean isOverridingDevice() {
+ return (mOverride & CFG_DEVICE) != 0;
+ }
+
+ @Override
+ @Nullable
+ public Device getDevice() {
+ if (isOverridingDevice()) {
+ return super.getDevice();
+ } else {
+ return mParent.getDevice();
+ }
+ }
+
+ @Override
+ public void setDevice(Device device, boolean skipSync) {
+ if (isOverridingDevice()) {
+ super.setDevice(device, skipSync);
+ } else {
+ mParent.setDevice(device, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the device state should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideDeviceState(boolean override) {
+ mOverride |= CFG_DEVICE_STATE;
+ }
+
+ /**
+ * Returns true if the device state is overridden
+ *
+ * @return true if the device state is overridden
+ */
+ public final boolean isOverridingDeviceState() {
+ return (mOverride & CFG_DEVICE_STATE) != 0;
+ }
+
+ @Override
+ @Nullable
+ public State getDeviceState() {
+ if (isOverridingDeviceState()) {
+ return super.getDeviceState();
+ } else {
+ State state = mParent.getDeviceState();
+ if (isOverridingDevice()) {
+ // If the device differs, I need to look up a suitable equivalent state
+ // on our device
+ if (state != null) {
+ Device device = super.getDevice();
+ if (device != null) {
+ return device.getState(state.getName());
+ }
+ }
+ }
+
+ return state;
+ }
+ }
+
+ @Override
+ public void setDeviceState(State state, boolean skipSync) {
+ if (isOverridingDeviceState()) {
+ super.setDeviceState(state, skipSync);
+ } else {
+ if (isOverridingDevice()) {
+ Device device = super.getDevice();
+ if (device != null) {
+ State equivalentState = device.getState(state.getName());
+ if (equivalentState != null) {
+ state = equivalentState;
+ }
+ }
+ }
+ mParent.setDeviceState(state, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the night mode should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideNightMode(boolean override) {
+ mOverride |= CFG_NIGHT_MODE;
+ }
+
+ /**
+ * Returns true if the night mode is overridden
+ *
+ * @return true if the night mode is overridden
+ */
+ public final boolean isOverridingNightMode() {
+ return (mOverride & CFG_NIGHT_MODE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public NightMode getNightMode() {
+ if (isOverridingNightMode()) {
+ return super.getNightMode();
+ } else {
+ return mParent.getNightMode();
+ }
+ }
+
+ @Override
+ public void setNightMode(@NonNull NightMode night, boolean skipSync) {
+ if (isOverridingNightMode()) {
+ super.setNightMode(night, skipSync);
+ } else {
+ mParent.setNightMode(night, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the UI mode should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideUiMode(boolean override) {
+ mOverride |= CFG_UI_MODE;
+ }
+
+ /**
+ * Returns true if the UI mode is overridden
+ *
+ * @return true if the UI mode is overridden
+ */
+ public final boolean isOverridingUiMode() {
+ return (mOverride & CFG_UI_MODE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public UiMode getUiMode() {
+ if (isOverridingUiMode()) {
+ return super.getUiMode();
+ } else {
+ return mParent.getUiMode();
+ }
+ }
+
+ @Override
+ public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) {
+ if (isOverridingUiMode()) {
+ super.setUiMode(uiMode, skipSync);
+ } else {
+ mParent.setUiMode(uiMode, skipSync);
+ }
+ }
+
+ /**
+ * Returns the configuration this {@linkplain NestedConfiguration} is
+ * inheriting from
+ *
+ * @return the configuration this configuration is inheriting from
+ */
+ @NonNull
+ public Configuration getParent() {
+ return mParent;
+ }
+
+ @Override
+ @Nullable
+ public String getActivity() {
+ return mParent.getActivity();
+ }
+
+ @Override
+ public void setActivity(String activity) {
+ super.setActivity(activity);
+ }
+
+ /**
+ * Returns a computed display name (ignoring the value stored by
+ * {@link #setDisplayName(String)}) by looking at the override flags
+ * and picking a suitable name.
+ *
+ * @return a suitable display name
+ */
+ @Nullable
+ public String computeDisplayName() {
+ return computeDisplayName(mOverride, this);
+ }
+
+ /**
+ * Computes a display name for the given configuration, using the given
+ * override flags (which correspond to the {@code CFG_} constants in
+ * {@link ConfigurationClient}
+ *
+ * @param flags the override bitmask
+ * @param configuration the configuration to fetch values from
+ * @return a suitable display name
+ */
+ @Nullable
+ public static String computeDisplayName(int flags, @NonNull Configuration configuration) {
+ if ((flags & CFG_LOCALE) != 0) {
+ return ConfigurationChooser.getLocaleLabel(configuration.mConfigChooser,
+ configuration.getLocale(), false);
+ }
+
+ if ((flags & CFG_TARGET) != 0) {
+ return ConfigurationChooser.getRenderingTargetLabel(configuration.getTarget(), false);
+ }
+
+ if ((flags & CFG_DEVICE) != 0) {
+ return ConfigurationChooser.getDeviceLabel(configuration.getDevice(), true);
+ }
+
+ if ((flags & CFG_DEVICE_STATE) != 0) {
+ State deviceState = configuration.getDeviceState();
+ if (deviceState != null) {
+ return deviceState.getName();
+ }
+ }
+
+ if ((flags & CFG_NIGHT_MODE) != 0) {
+ return configuration.getNightMode().getLongDisplayValue();
+ }
+
+ if ((flags & CFG_UI_MODE) != 0) {
+ configuration.getUiMode().getLongDisplayValue();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this.getClass())
+ .add("parent", mParent.getDisplayName()) //$NON-NLS-1$
+ .add("display", getDisplayName()) //$NON-NLS-1$
+ .add("overrideLocale", isOverridingLocale()) //$NON-NLS-1$
+ .add("overrideTarget", isOverridingTarget()) //$NON-NLS-1$
+ .add("overrideDevice", isOverridingDevice()) //$NON-NLS-1$
+ .add("overrideDeviceState", isOverridingDeviceState()) //$NON-NLS-1$
+ .add("persistent", toPersistentString()) //$NON-NLS-1$
+ .toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java
new file mode 100644
index 000000000..5cad29afc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2012 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.internal.editors.layout.gle2.SubmenuAction;
+import com.android.resources.NightMode;
+import com.android.resources.ScreenOrientation;
+import com.android.resources.UiMode;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+
+/**
+ * Action which creates a submenu that shows the available orientations as well
+ * as some related options for night mode and dock mode
+ */
+class OrientationMenuAction extends SubmenuAction {
+ // Constants used to indicate what type of menu is being shown, such that
+ // the submenus can lazily construct their contents
+ private static final int MENU_NIGHTMODE = 1;
+ private static final int MENU_UIMODE = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ /** Type of menu; one of the constants {@link #MENU_NIGHTMODE} etc */
+ private final int mType;
+
+ OrientationMenuAction(int type, String title, ConfigurationChooser configuration) {
+ super(title);
+ mType = type;
+ mConfigChooser = configuration;
+ }
+
+ static void showMenu(ConfigurationChooser configChooser, ToolItem combo) {
+ MenuManager manager = new MenuManager();
+
+ // Show toggles for all the available states
+
+ Configuration configuration = configChooser.getConfiguration();
+ Device device = configuration.getDevice();
+ State current = configuration.getDeviceState();
+ if (device != null) {
+ List<State> states = device.getAllStates();
+
+ if (states.size() > 1 && current != null) {
+ State flip = configuration.getNextDeviceState(current);
+ String flipName = flip != null ? flip.getName() : current.getName();
+ manager.add(new DeviceConfigAction(configChooser,
+ String.format("Switch to %1$s", flipName), flip, false, true));
+ manager.add(new Separator());
+ }
+
+ for (State config : states) {
+ manager.add(new DeviceConfigAction(configChooser, config.getName(),
+ config, config == current, false));
+ }
+ manager.add(new Separator());
+ }
+ manager.add(new OrientationMenuAction(MENU_UIMODE, "UI Mode", configChooser));
+ manager.add(new Separator());
+ manager.add(new OrientationMenuAction(MENU_NIGHTMODE, "Night Mode", configChooser));
+
+ Menu menu = manager.createContextMenu(configChooser.getShell());
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ switch (mType) {
+ case MENU_NIGHTMODE: {
+ NightMode selected = mConfigChooser.getConfiguration().getNightMode();
+ for (NightMode mode : NightMode.values()) {
+ boolean checked = mode == selected;
+ SelectNightModeAction action = new SelectNightModeAction(mode, checked);
+ new ActionContributionItem(action).fill(menu, -1);
+
+ }
+ break;
+ }
+ case MENU_UIMODE: {
+ UiMode selected = mConfigChooser.getConfiguration().getUiMode();
+ for (UiMode mode : UiMode.values()) {
+ boolean checked = mode == selected;
+ SelectUiModeAction action = new SelectUiModeAction(mode, checked);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ break;
+ }
+ }
+ }
+
+
+ private class SelectNightModeAction extends Action {
+ private final NightMode mMode;
+
+ private SelectNightModeAction(NightMode mode, boolean checked) {
+ super(mode.getLongDisplayValue(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ if (checked) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ configuration.setNightMode(mMode, false);
+ mConfigChooser.notifyFolderConfigChanged();
+ }
+ }
+
+ private class SelectUiModeAction extends Action {
+ private final UiMode mMode;
+
+ private SelectUiModeAction(UiMode mode, boolean checked) {
+ super(mode.getLongDisplayValue(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ if (checked) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ configuration.setUiMode(mMode, false);
+ }
+ }
+
+ private static class DeviceConfigAction extends Action {
+ private final ConfigurationChooser mConfiguration;
+ private final State mState;
+
+ private DeviceConfigAction(ConfigurationChooser configuration, String title,
+ State state, boolean checked, boolean flip) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mConfiguration = configuration;
+ mState = state;
+ if (checked) {
+ setChecked(true);
+ }
+ ScreenOrientation orientation = configuration.getOrientation(state);
+ setImageDescriptor(configuration.getOrientationImage(orientation, flip));
+ }
+
+ @Override
+ public void run() {
+ mConfiguration.selectDeviceState(mState);
+ mConfiguration.onDeviceConfigChange();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java
new file mode 100644
index 000000000..d062849d1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+
+/**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+class SelectThemeAction extends Action {
+ private final ConfigurationChooser mConfiguration;
+ private final String mTheme;
+
+ public SelectThemeAction(ConfigurationChooser configuration, String title, String theme,
+ boolean selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ assert theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme;
+ mConfiguration = configuration;
+ mTheme = theme;
+ if (selected) {
+ setChecked(selected);
+ }
+ }
+
+ @Override
+ public void run() {
+ mConfiguration.selectTheme(mTheme);
+ mConfiguration.onThemeChange();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java
new file mode 100644
index 000000000..71905f7c9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+import java.util.RandomAccess;
+
+/**
+ * The {@linkplain TargetMenuListener} class is responsible for
+ * generating the rendering target menu in the {@link ConfigurationChooser}.
+ */
+class TargetMenuListener extends SelectionAdapter {
+ private final ConfigurationChooser mConfigChooser;
+ private final IAndroidTarget mTarget;
+ private final boolean mPickBest;
+
+ TargetMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ @Nullable IAndroidTarget target,
+ boolean pickBest) {
+ mConfigChooser = configChooser;
+ mTarget = target;
+ mPickBest = pickBest;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IAndroidTarget target = mTarget;
+ AdtPrefs prefs = AdtPrefs.getPrefs();
+ if (mPickBest) {
+ boolean autoPick = prefs.isAutoPickRenderTarget();
+ autoPick = !autoPick;
+ prefs.setAutoPickRenderTarget(autoPick);
+ if (autoPick) {
+ target = ConfigurationMatcher.findDefaultRenderTarget(mConfigChooser);
+ } else {
+ // Turn it off, but keep current target until another one is chosen
+ return;
+ }
+ } else {
+ // Manually picked some other target: turn off auto-pick
+ prefs.setAutoPickRenderTarget(false);
+ }
+ mConfigChooser.selectTarget(target);
+ mConfigChooser.onRenderingTargetChange();
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ Configuration configuration = chooser.getConfiguration();
+ IAndroidTarget current = configuration.getTarget();
+ List<IAndroidTarget> targets = chooser.getTargetList();
+ boolean haveRecent = false;
+
+ MenuItem menuItem = new MenuItem(menu, SWT.CHECK);
+ menuItem.setText("Automatically Pick Best");
+ menuItem.addSelectionListener(new TargetMenuListener(chooser, null, true));
+ if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) {
+ menuItem.setSelection(true);
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ // Process in reverse order: most important targets first
+ assert targets instanceof RandomAccess;
+ for (int i = targets.size() - 1; i >= 0; i--) {
+ IAndroidTarget target = targets.get(i);
+
+ AndroidVersion version = target.getVersion();
+ if (version.getApiLevel() >= 7) {
+ haveRecent = true;
+ } else if (haveRecent) {
+ // Don't show ancient rendering targets; they're pretty broken
+ // (unless of course all you have are ancient targets)
+ break;
+ }
+
+ String title = ConfigurationChooser.getRenderingTargetLabel(target, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+
+ boolean selected = current == target;
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ item.addSelectionListener(new TargetMenuListener(chooser, target, false));
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java
new file mode 100644
index 000000000..b1ce21d36
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+
+import com.android.ide.eclipse.adt.internal.editors.Hyperlinks;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SubmenuAction;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Action which creates a submenu displaying available themes
+ */
+class ThemeMenuAction extends SubmenuAction {
+ private static final String DEVICE_LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault.Light"; //$NON-NLS-1$
+ private static final String HOLO_LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo.Light"; //$NON-NLS-1$
+ private static final String DEVICE_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault"; //$NON-NLS-1$
+ private static final String HOLO_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$
+ private static final String LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX +"Theme.Light"; //$NON-NLS-1$
+ private static final String THEME_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX +"Theme"; //$NON-NLS-1$
+
+ // Constants used to indicate what type of menu is being shown, such that
+ // the submenus can lazily construct their contents
+ private static final int MENU_MANIFEST = 1;
+ private static final int MENU_PROJECT = 2;
+ private static final int MENU_THEME = 3;
+ private static final int MENU_THEME_LIGHT = 4;
+ private static final int MENU_HOLO = 5;
+ private static final int MENU_HOLO_LIGHT = 6;
+ private static final int MENU_DEVICE = 7;
+ private static final int MENU_DEVICE_LIGHT = 8;
+ private static final int MENU_ALL = 9;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final List<String> mThemeList;
+ /** Type of menu; one of the constants {@link #MENU_ALL} etc */
+ private final int mType;
+
+ ThemeMenuAction(int type, String title, ConfigurationChooser configuration,
+ List<String> themeList) {
+ super(title);
+ mType = type;
+ mConfigChooser = configuration;
+ mThemeList = themeList;
+ }
+
+ static void showThemeMenu(ConfigurationChooser configChooser, ToolItem combo,
+ List<String> themeList) {
+ MenuManager manager = new MenuManager();
+
+ // First show the currently selected theme (grayed out since you can't
+ // reselect it)
+ Configuration configuration = configChooser.getConfiguration();
+ String currentTheme = configuration.getTheme();
+ String currentName = null;
+ if (currentTheme != null) {
+ currentName = ResourceHelper.styleToTheme(currentTheme);
+ SelectThemeAction action = new SelectThemeAction(configChooser,
+ currentName,
+ currentTheme,
+ true /* selected */);
+ action.setEnabled(false);
+ manager.add(action);
+ manager.add(new Separator());
+ }
+
+ String preferred = configuration.computePreferredTheme();
+ if (preferred != null && !preferred.equals(currentTheme)) {
+ manager.add(new SelectThemeAction(configChooser,
+ ResourceHelper.styleToTheme(preferred),
+ preferred, false /* selected */));
+ manager.add(new Separator());
+ }
+
+ IAndroidTarget target = configuration.getTarget();
+ int apiLevel = target != null ? target.getVersion().getApiLevel() : 1;
+ boolean hasHolo = apiLevel >= 11; // Honeycomb
+ boolean hasDeviceDefault = apiLevel >= 14; // ICS
+
+ // TODO: Add variations of the current theme here, e.g.
+ // if you're using Theme.Holo, add Theme.Holo.Dialog, Theme.Holo.Panel,
+ // Theme.Holo.Wallpaper etc
+
+ manager.add(new ThemeMenuAction(MENU_PROJECT, "Project Themes",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_MANIFEST, "Manifest Themes",
+ configChooser, themeList));
+
+ manager.add(new Separator());
+
+ if (hasHolo) {
+ manager.add(new ThemeMenuAction(MENU_HOLO, "Holo",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_HOLO_LIGHT, "Holo.Light",
+ configChooser, themeList));
+ }
+ if (hasDeviceDefault) {
+ manager.add(new ThemeMenuAction(MENU_DEVICE, "DeviceDefault",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_DEVICE_LIGHT, "DeviceDefault.Light",
+ configChooser, themeList));
+ }
+ manager.add(new ThemeMenuAction(MENU_THEME, "Theme",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_THEME_LIGHT, "Theme.Light",
+ configChooser, themeList));
+
+ // TODO: Add generic types like Wallpaper, Dialog, Alert, etc here, with
+ // submenus for picking it within each theme category?
+
+ manager.add(new Separator());
+ manager.add(new ThemeMenuAction(MENU_ALL, "All",
+ configChooser, themeList));
+
+ if (currentTheme != null) {
+ assert currentName != null;
+ manager.add(new Separator());
+ String title = String.format("Open %1$s Declaration...", currentName);
+ manager.add(new OpenThemeAction(title, configChooser.getEditedFile(), currentTheme));
+ }
+
+ Menu menu = manager.createContextMenu(configChooser.getShell());
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ switch (mType) {
+ case MENU_ALL:
+ addMenuItems(menu, mThemeList);
+ break;
+
+ case MENU_MANIFEST: {
+ IProject project = mConfigChooser.getEditedFile().getProject();
+ ManifestInfo manifest = ManifestInfo.get(project);
+ Configuration configuration = mConfigChooser.getConfiguration();
+ String activity = configuration.getActivity();
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ String theme = attributes.getTheme();
+ if (theme != null) {
+ addMenuItem(menu, theme, isSelectedTheme(theme));
+ }
+ }
+ }
+
+ String manifestTheme = manifest.getManifestTheme();
+ boolean found = false;
+ Set<String> allThemes = new HashSet<String>();
+ if (manifestTheme != null) {
+ found = true;
+ allThemes.add(manifestTheme);
+ }
+ for (ActivityAttributes info : manifest.getActivityAttributesMap().values()) {
+ if (info.getTheme() != null) {
+ found = true;
+ allThemes.add(info.getTheme());
+ }
+ }
+ List<String> sorted = new ArrayList<String>(allThemes);
+ Collections.sort(sorted);
+ String current = configuration.getTheme();
+ for (String theme : sorted) {
+ boolean selected = theme.equals(current);
+ addMenuItem(menu, theme, selected);
+ }
+ if (!found) {
+ addDisabledMessageItem("No themes are registered in the manifest");
+ }
+ break;
+ }
+ case MENU_PROJECT: {
+ int size = mThemeList.size();
+ List<String> themes = new ArrayList<String>(size);
+ for (int i = 0; i < size; i++) {
+ String theme = mThemeList.get(i);
+ if (ResourceHelper.isProjectStyle(theme)) {
+ themes.add(theme);
+ }
+ }
+ if (themes.isEmpty()) {
+ addDisabledMessageItem("There are no local theme styles in the project");
+ } else {
+ addMenuItems(menu, themes);
+ }
+ break;
+ }
+ case MENU_THEME: {
+ // Can't just use the usual filterThemes() call here because we need
+ // to exclude on multiple prefixes: Holo, DeviceDefault, Light, ...
+ List<String> themes = new ArrayList<String>(mThemeList.size());
+ for (String theme : mThemeList) {
+ if (theme.startsWith(THEME_PREFIX)
+ && !theme.startsWith(LIGHT_PREFIX)
+ && !theme.startsWith(HOLO_PREFIX)
+ && !theme.startsWith(DEVICE_PREFIX)) {
+ themes.add(theme);
+ }
+ }
+
+ addMenuItems(menu, themes);
+ break;
+ }
+ case MENU_THEME_LIGHT:
+ addMenuItems(menu, filterThemes(LIGHT_PREFIX, null));
+ break;
+ case MENU_HOLO:
+ addMenuItems(menu, filterThemes(HOLO_PREFIX, HOLO_LIGHT_PREFIX));
+ break;
+ case MENU_HOLO_LIGHT:
+ addMenuItems(menu, filterThemes(HOLO_LIGHT_PREFIX, null));
+ break;
+ case MENU_DEVICE:
+ addMenuItems(menu, filterThemes(DEVICE_PREFIX, DEVICE_LIGHT_PREFIX));
+ break;
+ case MENU_DEVICE_LIGHT:
+ addMenuItems(menu, filterThemes(DEVICE_LIGHT_PREFIX, null));
+ break;
+ }
+ }
+
+ private List<String> filterThemes(String include, String exclude) {
+ List<String> themes = new ArrayList<String>(mThemeList.size());
+ for (String theme : mThemeList) {
+ if (theme.startsWith(include) && (exclude == null || !theme.startsWith(exclude))) {
+ themes.add(theme);
+ }
+ }
+
+ return themes;
+ }
+
+ private void addMenuItems(Menu menu, List<String> themes) {
+ String current = mConfigChooser.getConfiguration().getTheme();
+ for (String theme : themes) {
+ addMenuItem(menu, theme, theme.equals(current));
+ }
+ }
+
+ private boolean isSelectedTheme(String theme) {
+ return theme.equals(mConfigChooser.getConfiguration().getTheme());
+ }
+
+ private void addMenuItem(Menu menu, String theme, boolean selected) {
+ String title = ResourceHelper.styleToTheme(theme);
+ SelectThemeAction action = new SelectThemeAction(mConfigChooser, title, theme, selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+
+ private static class OpenThemeAction extends Action {
+ private final String mTheme;
+ private final IFile mFile;
+
+ private OpenThemeAction(String title, IFile file, String theme) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mFile = file;
+ mTheme = theme;
+ }
+
+ @Override
+ public void run() {
+ IProject project = mFile.getProject();
+ IHyperlink[] links = Hyperlinks.getResourceLinks(null, mTheme, project, null);
+ if (links != null && links.length > 0) {
+ IHyperlink link = links[0];
+ link.open();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java
new file mode 100644
index 000000000..f472cd6b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.resources.Density;
+import com.android.resources.NightMode;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Hardware;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An {@linkplain VaryingConfiguration} is a {@link Configuration} which
+ * inherits all of its values from a different configuration, except for one or
+ * more attributes where it overrides a custom value, and the overridden value
+ * will always <b>differ</b> from the inherited value!
+ * <p>
+ * For example, a {@linkplain VaryingConfiguration} may state that it
+ * overrides the locale, and if the inherited locale is "en", then the returned
+ * locale from the {@linkplain VaryingConfiguration} may be for example "nb",
+ * but never "en".
+ * <p>
+ * The configuration will attempt to make its changed inherited value to be as
+ * different as possible from the inherited value. Thus, a configuration which
+ * overrides the device will probably return a phone-sized screen if the
+ * inherited device is a tablet, or vice versa.
+ */
+public class VaryingConfiguration extends NestedConfiguration {
+ /** Variation version; see {@link #setVariation(int)} */
+ private int mVariation;
+
+ /** Variation version count; see {@link #setVariationCount(int)} */
+ private int mVariationCount;
+
+ /** Bitmask of attributes to be varied/alternated from the parent */
+ private int mAlternate;
+
+ /**
+ * Constructs a new {@linkplain VaryingConfiguration}.
+ * Construct via
+ *
+ * @param chooser the associated chooser
+ * @param configuration the configuration to inherit from
+ */
+ private VaryingConfiguration(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration) {
+ super(chooser, configuration);
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} which inherits values from the
+ * given parent {@linkplain Configuration}, possibly overriding some as
+ * well.
+ *
+ * @param chooser the associated chooser
+ * @param parent the configuration to inherit values from
+ * @return a new configuration
+ */
+ @NonNull
+ public static VaryingConfiguration create(@NonNull ConfigurationChooser chooser,
+ @NonNull Configuration parent) {
+ return new VaryingConfiguration(chooser, parent);
+ }
+
+ /**
+ * Creates a new {@linkplain VaryingConfiguration} that has the same overriding
+ * attributes as the given other {@linkplain VaryingConfiguration}.
+ *
+ * @param other the configuration to copy overrides from
+ * @param parent the parent to tie the configuration to for inheriting values
+ * @return a new configuration
+ */
+ @NonNull
+ public static VaryingConfiguration create(
+ @NonNull VaryingConfiguration other,
+ @NonNull Configuration parent) {
+ VaryingConfiguration configuration =
+ new VaryingConfiguration(other.mConfigChooser, parent);
+ initFrom(configuration, other, other, false);
+ configuration.mAlternate = other.mAlternate;
+ configuration.mVariation = other.mVariation;
+ configuration.mVariationCount = other.mVariationCount;
+ configuration.syncFolderConfig();
+
+ return configuration;
+ }
+
+ /**
+ * Returns the alternate flags for this configuration. Corresponds to
+ * the {@code CFG_} flags in {@link ConfigurationClient}.
+ *
+ * @return the bitmask
+ */
+ public int getAlternateFlags() {
+ return mAlternate;
+ }
+
+ @Override
+ public void syncFolderConfig() {
+ super.syncFolderConfig();
+ updateDisplayName();
+ }
+
+ /**
+ * Sets the variation version for this
+ * {@linkplain VaryingConfiguration}. There might be multiple
+ * {@linkplain VaryingConfiguration} instances inheriting from a
+ * {@link Configuration}. The variation version allows them to choose
+ * different complementing values, so they don't all flip to the same other
+ * (out of multiple choices) value. The {@link #setVariationCount(int)}
+ * value can be used to determine how to partition the buckets of values.
+ * Also updates the variation count if necessary.
+ *
+ * @param variation variation version
+ */
+ public void setVariation(int variation) {
+ mVariation = variation;
+ mVariationCount = Math.max(mVariationCount, variation + 1);
+ }
+
+ /**
+ * Sets the number of {@link VaryingConfiguration} variations mapped
+ * to the same parent configuration as this one. See
+ * {@link #setVariation(int)} for details.
+ *
+ * @param count the total number of variation versions
+ */
+ public void setVariationCount(int count) {
+ mVariationCount = count;
+ }
+
+ /**
+ * Updates the display name in this configuration based on the values and override settings
+ */
+ public void updateDisplayName() {
+ setDisplayName(computeDisplayName());
+ }
+
+ @Override
+ @NonNull
+ public Locale getLocale() {
+ if (isOverridingLocale()) {
+ return super.getLocale();
+ }
+ Locale locale = mParent.getLocale();
+ if (isAlternatingLocale() && locale != null) {
+ List<Locale> locales = mConfigChooser.getLocaleList();
+ for (Locale l : locales) {
+ // TODO: Try to be smarter about which one we pick; for example, try
+ // to pick a language that is substantially different from the inherited
+ // language, such as either with the strings of the largest or shortest
+ // length, or perhaps based on some geography or population metrics
+ if (!l.equals(locale)) {
+ locale = l;
+ break;
+ }
+ }
+ }
+
+ return locale;
+ }
+
+ @Override
+ @Nullable
+ public IAndroidTarget getTarget() {
+ if (isOverridingTarget()) {
+ return super.getTarget();
+ }
+ IAndroidTarget target = mParent.getTarget();
+ if (isAlternatingTarget() && target != null) {
+ List<IAndroidTarget> targets = mConfigChooser.getTargetList();
+ if (!targets.isEmpty()) {
+ // Pick a different target: if you're showing the most recent render target,
+ // then pick the lowest supported target, and vice versa
+ IAndroidTarget mostRecent = targets.get(targets.size() - 1);
+ if (target.equals(mostRecent)) {
+ // Find oldest supported
+ ManifestInfo info = ManifestInfo.get(mConfigChooser.getProject());
+ int minSdkVersion = info.getMinSdkVersion();
+ for (IAndroidTarget t : targets) {
+ if (t.getVersion().getApiLevel() >= minSdkVersion) {
+ target = t;
+ break;
+ }
+ }
+ } else {
+ target = mostRecent;
+ }
+ }
+ }
+
+ return target;
+ }
+
+ // Cached values, key=parent's device, cached value=device
+ private Device mPrevParentDevice;
+ private Device mPrevDevice;
+
+ @Override
+ @Nullable
+ public Device getDevice() {
+ if (isOverridingDevice()) {
+ return super.getDevice();
+ }
+ Device device = mParent.getDevice();
+ if (isAlternatingDevice() && device != null) {
+ if (device == mPrevParentDevice) {
+ return mPrevDevice;
+ }
+
+ mPrevParentDevice = device;
+
+ // Pick a different device
+ Collection<Device> devices = mConfigChooser.getDevices();
+
+ // Divide up the available devices into {@link #mVariationCount} + 1 buckets
+ // (the + 1 is for the bucket now taken up by the inherited value).
+ // Then assign buckets to each {@link #mVariation} version, and pick one
+ // from the bucket assigned to this current configuration's variation version.
+
+ // I could just divide up the device list count, but that would treat a lot of
+ // very similar phones as having the same kind of variety as the 7" and 10"
+ // tablets which are sitting right next to each other in the device list.
+ // Instead, do this by screen size.
+
+
+ double smallest = 100;
+ double biggest = 1;
+ for (Device d : devices) {
+ double size = getScreenSize(d);
+ if (size < 0) {
+ continue; // no data
+ }
+ if (size >= biggest) {
+ biggest = size;
+ }
+ if (size <= smallest) {
+ smallest = size;
+ }
+ }
+
+ int bucketCount = mVariationCount + 1;
+ double inchesPerBucket = (biggest - smallest) / bucketCount;
+
+ double overriddenSize = getScreenSize(device);
+ int overriddenBucket = (int) ((overriddenSize - smallest) / inchesPerBucket);
+ int bucket = (mVariation < overriddenBucket) ? mVariation : mVariation + 1;
+ double from = inchesPerBucket * bucket + smallest;
+ double to = from + inchesPerBucket;
+ if (biggest - to < 0.1) {
+ to = biggest + 0.1;
+ }
+
+ boolean canScaleNinePatch = supports(Capability.FIXED_SCALABLE_NINE_PATCH);
+ for (Device d : devices) {
+ double size = getScreenSize(d);
+ if (size >= from && size < to) {
+ if (!canScaleNinePatch) {
+ Density density = getDensity(d);
+ if (density == Density.TV || density == Density.LOW) {
+ continue;
+ }
+ }
+
+ device = d;
+ break;
+ }
+ }
+
+ mPrevDevice = device;
+ }
+
+ return device;
+ }
+
+ /**
+ * Returns the density of the given device
+ *
+ * @param device the device to check
+ * @return the density or null
+ */
+ @Nullable
+ private static Density getDensity(@NonNull Device device) {
+ Hardware hardware = device.getDefaultHardware();
+ if (hardware != null) {
+ Screen screen = hardware.getScreen();
+ if (screen != null) {
+ return screen.getPixelDensity();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the diagonal length of the given device
+ *
+ * @param device the device to check
+ * @return the diagonal length or -1
+ */
+ private static double getScreenSize(@NonNull Device device) {
+ Hardware hardware = device.getDefaultHardware();
+ if (hardware != null) {
+ Screen screen = hardware.getScreen();
+ if (screen != null) {
+ return screen.getDiagonalLength();
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ @Nullable
+ public State getDeviceState() {
+ if (isOverridingDeviceState()) {
+ return super.getDeviceState();
+ }
+ State state = mParent.getDeviceState();
+ if (isAlternatingDeviceState() && state != null) {
+ State alternate = getNextDeviceState(state);
+
+ return alternate;
+ } else {
+ if ((isAlternatingDevice() || isOverridingDevice()) && state != null) {
+ // If the device differs, I need to look up a suitable equivalent state
+ // on our device
+ Device device = getDevice();
+ if (device != null) {
+ return device.getState(state.getName());
+ }
+ }
+
+ return state;
+ }
+ }
+
+ @Override
+ @NonNull
+ public NightMode getNightMode() {
+ if (isOverridingNightMode()) {
+ return super.getNightMode();
+ }
+ NightMode nightMode = mParent.getNightMode();
+ if (isAlternatingNightMode() && nightMode != null) {
+ nightMode = nightMode == NightMode.NIGHT ? NightMode.NOTNIGHT : NightMode.NIGHT;
+ return nightMode;
+ } else {
+ return nightMode;
+ }
+ }
+
+ @Override
+ @NonNull
+ public UiMode getUiMode() {
+ if (isOverridingUiMode()) {
+ return super.getUiMode();
+ }
+ UiMode uiMode = mParent.getUiMode();
+ if (isAlternatingUiMode() && uiMode != null) {
+ // TODO: Use manifest's supports screen to decide which are most relevant
+ // (as well as which available configuration qualifiers are present in the
+ // layout)
+ UiMode[] values = UiMode.values();
+ uiMode = values[(uiMode.ordinal() + 1) % values.length];
+ return uiMode;
+ } else {
+ return uiMode;
+ }
+ }
+
+ @Override
+ @Nullable
+ public String computeDisplayName() {
+ return computeDisplayName(getOverrideFlags() | mAlternate, this);
+ }
+
+ /**
+ * Sets whether the locale should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateLocale(boolean alternate) {
+ mAlternate |= CFG_LOCALE;
+ }
+
+ /**
+ * Returns true if the locale is alternated
+ *
+ * @return true if the locale is alternated
+ */
+ public final boolean isAlternatingLocale() {
+ return (mAlternate & CFG_LOCALE) != 0;
+ }
+
+ /**
+ * Sets whether the rendering target should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateTarget(boolean alternate) {
+ mAlternate |= CFG_TARGET;
+ }
+
+ /**
+ * Returns true if the target is alternated
+ *
+ * @return true if the target is alternated
+ */
+ public final boolean isAlternatingTarget() {
+ return (mAlternate & CFG_TARGET) != 0;
+ }
+
+ /**
+ * Sets whether the device should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateDevice(boolean alternate) {
+ mAlternate |= CFG_DEVICE;
+ }
+
+ /**
+ * Returns true if the device is alternated
+ *
+ * @return true if the device is alternated
+ */
+ public final boolean isAlternatingDevice() {
+ return (mAlternate & CFG_DEVICE) != 0;
+ }
+
+ /**
+ * Sets whether the device state should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateDeviceState(boolean alternate) {
+ mAlternate |= CFG_DEVICE_STATE;
+ }
+
+ /**
+ * Returns true if the device state is alternated
+ *
+ * @return true if the device state is alternated
+ */
+ public final boolean isAlternatingDeviceState() {
+ return (mAlternate & CFG_DEVICE_STATE) != 0;
+ }
+
+ /**
+ * Sets whether the night mode should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateNightMode(boolean alternate) {
+ mAlternate |= CFG_NIGHT_MODE;
+ }
+
+ /**
+ * Returns true if the night mode is alternated
+ *
+ * @return true if the night mode is alternated
+ */
+ public final boolean isAlternatingNightMode() {
+ return (mAlternate & CFG_NIGHT_MODE) != 0;
+ }
+
+ /**
+ * Sets whether the UI mode should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateUiMode(boolean alternate) {
+ mAlternate |= CFG_UI_MODE;
+ }
+
+ /**
+ * Returns true if the UI mode is alternated
+ *
+ * @return true if the UI mode is alternated
+ */
+ public final boolean isAlternatingUiMode() {
+ return (mAlternate & CFG_UI_MODE) != 0;
+ }
+
+} \ No newline at end of file