| /* |
| * 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; |
| } |
| } |