diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration')
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 |