aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java
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/ConfigurationChooser.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java2096
1 files changed, 2096 insertions, 0 deletions
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;
+ }
+}