diff options
42 files changed, 4734 insertions, 233 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index d678c2d..d84f60d 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -348,6 +348,7 @@ wildcard workflow xdpi xhdpi +xlarge xml xmlns ydpi diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/editPreview.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/editPreview.png Binary files differnew file mode 100644 index 0000000..fd36133 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/editPreview.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/minimizePreview.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/minimizePreview.png Binary files differnew file mode 100644 index 0000000..4ddc540 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/minimizePreview.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/refreshPreview.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/refreshPreview.png Binary files differnew file mode 100644 index 0000000..d103763 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/refreshPreview.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/renderError.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/renderError.png Binary files differnew file mode 100644 index 0000000..95be641 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/renderError.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/restorePreview.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/restorePreview.png Binary files differnew file mode 100644 index 0000000..d6b3f32 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/restorePreview.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-b.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-b.png Binary files differnew file mode 100644 index 0000000..963973e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-b.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-bl.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-bl.png Binary files differnew file mode 100644 index 0000000..7612487 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-bl.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-br.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-br.png Binary files differnew file mode 100644 index 0000000..8e20252 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-br.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-r.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-r.png Binary files differnew file mode 100644 index 0000000..8e026f1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-r.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-tr.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-tr.png Binary files differnew file mode 100644 index 0000000..590373c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/shadow2-tr.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java index d5fa567..4ef469d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java @@ -80,6 +80,7 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -1236,6 +1237,62 @@ public class AdtUtils { } /** + * Returns all resource variations for the given file + * + * @param file resource file, which should be an XML file in one of the + * various resource folders, e.g. res/layout, res/values-xlarge, etc. + * @param includeSelf if true, include the file itself in the list, + * otherwise exclude it + * @return a list of all the resource variations + */ + public static List<IFile> getResourceVariations(@Nullable IFile file, boolean includeSelf) { + if (file == null) { + return Collections.emptyList(); + } + + // Compute the set of layout files defining this layout resource + List<IFile> variations = new ArrayList<IFile>(); + String name = file.getName(); + IContainer parent = file.getParent(); + if (parent != null) { + IContainer resFolder = parent.getParent(); + if (resFolder != null) { + String parentName = parent.getName(); + String prefix = parentName; + int qualifiers = prefix.indexOf('-'); + + if (qualifiers != -1) { + parentName = prefix.substring(0, qualifiers); + prefix = prefix.substring(0, qualifiers + 1); + } else { + prefix = prefix + '-'; + } + try { + for (IResource resource : resFolder.members()) { + String n = resource.getName(); + if ((n.startsWith(prefix) || n.equals(parentName)) + && resource instanceof IContainer) { + IContainer layoutFolder = (IContainer) resource; + IResource r = layoutFolder.findMember(name); + if (r instanceof IFile) { + IFile variation = (IFile) r; + if (!includeSelf && file.equals(variation)) { + continue; + } + variations.add(variation); + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + } + + return variations; + } + + /** * Returns whether the current thread is the UI thread * * @return true if the current thread is the UI thread diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java index 1dd32c7..969d45a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java @@ -133,6 +133,44 @@ public class XmlPrettyPrinter { } /** + * Pretty prints the given node + * + * @param node the node, usually a document, to be printed + * @param prefs the formatting preferences + * @param style the formatting style to use + * @param lineSeparator the line separator to use, or null to use the + * default + * @return a formatted string + */ + @NonNull + public static String prettyPrint( + @NonNull Node node, + @NonNull XmlFormatPreferences prefs, + @NonNull XmlFormatStyle style, + @Nullable String lineSeparator) { + XmlPrettyPrinter printer = new XmlPrettyPrinter(prefs, style, lineSeparator); + StringBuilder sb = new StringBuilder(1000); + printer.prettyPrint(-1, node, null, null, sb, false /*openTagOnly*/); + String xml = sb.toString(); + if (node.getNodeType() == Node.DOCUMENT_NODE && !xml.startsWith("<?")) { //$NON-NLS-1$ + xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + xml; //$NON-NLS-1$ + } + return xml; + } + + /** + * Pretty prints the given node using default styles + * + * @param node the node, usually a document, to be printed + * @return the resulting formatted string + */ + @NonNull + public static String prettyPrint(@NonNull Node node) { + return prettyPrint(node, XmlFormatPreferences.create(), XmlFormatStyle.FILE, + SdkUtils.getLineSeparator()); + } + + /** * Start pretty-printing at the given node, which must either be the * startNode or contain it as a descendant. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ComplementingConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ComplementingConfiguration.java new file mode 100644 index 0000000..cce5bc9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ComplementingConfiguration.java @@ -0,0 +1,346 @@ +/* + * 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.manifest.ManifestInfo; +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.List; + +/** + * An {@linkplain ComplementingConfiguration} 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 ComplementingConfiguration} may state that it + * overrides the locale, and if the inherited locale is "en", then the returned + * locale from the {@linkplain ComplementingConfiguration} 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 ComplementingConfiguration extends NestedConfiguration { + /** + * If non zero, keep the display name up to date with the label for the + * given overridden attribute, according to the flag constants in + * {@link ConfigurationClient} + */ + private int mUpdateDisplayName; + + /** Variation version; see {@link #setVariation(int)} */ + private int mVariation; + + /** Variation version count; see {@link #setVariationCount(int)} */ + private int mVariationCount; + + /** + * Constructs a new {@linkplain ComplementingConfiguration}. + * Construct via + * + * @param chooser the associated chooser + * @param configuration the configuration to inherit from + */ + private ComplementingConfiguration( + @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 ComplementingConfiguration create(@NonNull ConfigurationChooser chooser, + @NonNull Configuration parent) { + return new ComplementingConfiguration(chooser, parent); + } + + /** + * Sets the variation version for this + * {@linkplain ComplementingConfiguration}. There might be multiple + * {@linkplain ComplementingConfiguration} 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 ComplementingConfiguration} 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; + } + + @Override + public void setOverrideDevice(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_DEVICE; + super.setOverrideDevice(override); + } + + @Override + public void setOverrideDeviceState(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_DEVICE_CONFIG; + super.setOverrideDeviceState(override); + } + + @Override + public void setOverrideLocale(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_LOCALE; + super.setOverrideLocale(override); + } + + @Override + public void setOverrideTarget(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_RENDER_TARGET; + super.setOverrideTarget(override); + } + + @Override + public void setOverrideNightMode(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_NIGHT_MODE; + super.setOverrideNightMode(override); + } + + @Override + public void setOverrideUiMode(boolean override) { + mUpdateDisplayName |= ConfigurationClient.CHANGED_UI_MODE; + super.setOverrideUiMode(override); + } + + @Override + @NonNull + public Locale getLocale() { + Locale locale = mParent.getLocale(); + if (mOverrideLocale && 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; + } + } + + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_LOCALE) != 0) { + setDisplayName(ConfigurationChooser.getLocaleLabel(mConfigChooser, locale, false)); + } + } + + return locale; + } + + @Override + @Nullable + public IAndroidTarget getTarget() { + IAndroidTarget target = mParent.getTarget(); + if (mOverrideTarget && 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; + } + } + + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_RENDER_TARGET) != 0) { + setDisplayName(ConfigurationChooser.getRenderingTargetLabel(target, false)); + } + } + + return target; + } + + @Override + @Nullable + public Device getDevice() { + Device device = mParent.getDevice(); + if (mOverrideDevice && device != null) { + // Pick a different device + List<Device> devices = mConfigChooser.getDeviceList(); + + + // 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; + } + + for (Device d : devices) { + double size = getScreenSize(d); + if (size >= from && size < to) { + device = d; + break; + } + } + + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_DEVICE) != 0) { + setDisplayName(ConfigurationChooser.getDeviceLabel(device, true)); + } + } + + return device; + } + 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() { + State state = mParent.getDeviceState(); + if (mOverrideDeviceState && state != null) { + State alternate = getNextDeviceState(state); + + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_DEVICE_CONFIG) != 0) { + if (alternate != null) { + setDisplayName(alternate.getName()); + } + } + + return alternate; + } else { + if (mOverrideDevice && 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() { + NightMode nightMode = mParent.getNightMode(); + if (mOverrideNightMode && nightMode != null) { + nightMode = nightMode == NightMode.NIGHT ? NightMode.NOTNIGHT : NightMode.NIGHT; + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_NIGHT_MODE) != 0) { + setDisplayName(nightMode.getLongDisplayValue()); + } + return nightMode; + } else { + return nightMode; + } + } + + @Override + @NonNull + public UiMode getUiMode() { + UiMode uiMode = mParent.getUiMode(); + if (mOverrideUiMode && 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]; + if ((mUpdateDisplayName & ConfigurationClient.CHANGED_UI_MODE) != 0) { + setDisplayName(uiMode.getLongDisplayValue()); + } + return uiMode; + } else { + return uiMode; + } + } +}
\ No newline at end of file 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 index 2b5589b..09adc64 100644 --- 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 @@ -19,6 +19,7 @@ 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 static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser.NAME_CONFIG_STATE; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -38,6 +39,7 @@ import com.android.ide.common.resources.configuration.VersionQualifier; 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 com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.Density; import com.android.resources.NightMode; @@ -50,6 +52,7 @@ import com.android.sdklib.devices.Device; import com.android.sdklib.devices.State; import com.android.utils.Pair; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; @@ -120,6 +123,9 @@ public class Configuration { @NonNull private NightMode mNightMode = NightMode.NOTNIGHT; + /** The display name */ + private String mDisplayName; + /** * Creates a new {@linkplain Configuration} * @@ -141,6 +147,58 @@ public class Configuration { } /** + * Creates a configuration + * @param chooser the configuration chooser with device info, render target info, etc + * @param file the file to look up a configuration for + * @return a suitable configuration + */ + @NonNull + public static Configuration create(@NonNull ConfigurationChooser chooser, + @NonNull IFile file) { + Configuration configuration = copy(chooser.getConfiguration()); + String data = AdtPlugin.getFileProperty(file, NAME_CONFIG_STATE); + if (data != null) { + configuration.initialize(data); + } else { + ProjectResources resources = chooser.getResources(); + ConfigurationMatcher matcher = new ConfigurationMatcher(chooser, configuration, file, + resources, false); + if (configuration.mEditedConfig == null) { + configuration.mEditedConfig = new FolderConfiguration(); + } + matcher.adaptConfigSelection(true /*needBestMatch*/); + } + + 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(); + + return copy; + } + + /** * Returns the associated activity * * @return the activity @@ -221,6 +279,16 @@ public class Configuration { } /** + * 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 @@ -366,6 +434,15 @@ public class Configuration { } /** + * 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 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 index 7412bf1..38f2e67 100644 --- 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 @@ -37,6 +37,7 @@ 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.ResourceFile; import com.android.ide.common.resources.ResourceFolder; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.configuration.DeviceConfigHelper; @@ -55,6 +56,7 @@ 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.ResourceFolderType; import com.android.resources.ResourceType; import com.android.resources.ScreenOrientation; import com.android.sdklib.AndroidVersion; @@ -203,8 +205,8 @@ public class ConfigurationChooser extends Composite 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 | SWT.BOLD); - mConfigCombo.setImage(null); + 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") @@ -294,6 +296,9 @@ public class ConfigurationChooser extends Composite mOrientationCombo.addSelectionListener(listener); addDisposeListener(this); + + initDevices(); + initTargets(); } /** @@ -483,6 +488,7 @@ public class ConfigurationChooser extends Composite */ public void setFile(IFile file) { mEditedFile = file; + initializeConfiguration(); } /** @@ -499,7 +505,7 @@ public class ConfigurationChooser extends Composite return; } - mEditedFile = file; + setFile(file); IProject project = mEditedFile.getProject(); mResources = ResourceManager.getInstance().getProjectResources(project); @@ -543,7 +549,7 @@ public class ConfigurationChooser extends Composite * @see #replaceFile(IFile) */ public void changeFileOnNewConfig(IFile file) { - mEditedFile = file; + setFile(file); IProject project = mEditedFile.getProject(); mResources = ResourceManager.getInstance().getProjectResources(project); @@ -648,10 +654,8 @@ public class ConfigurationChooser extends Composite mDisableUpdates++; // we do not want to trigger onXXXChange when setting // new values in the widgets. try { - // this is going to be followed by a call to onTargetLoaded. - // So we can only care about the layout devices in this case. - initDevices(); - initTargets(); + updateDevices(); + updateTargets(); } finally { mDisableUpdates--; } @@ -689,8 +693,8 @@ public class ConfigurationChooser extends Composite try { // init the devices if needed (new SDK or first time going through here) if (mSdkChanged) { - initDevices(); - initTargets(); + updateDevices(); + updateTargets(); mSdkChanged = false; } @@ -704,7 +708,7 @@ public class ConfigurationChooser extends Composite LoadStatus targetStatus = LoadStatus.FAILED; if (mProjectTarget != null) { targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); - initTargets(); + updateTargets(); } if (targetStatus == LoadStatus.LOADED) { @@ -726,16 +730,9 @@ public class ConfigurationChooser extends Composite targetData = Sdk.getCurrent().getTargetData(mProjectTarget); // get the file stored state - boolean loadedConfigData = false; - String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE); - if (mInitialState != null) { - data = mInitialState; - mInitialState = null; - } - - if (data != null) { - loadedConfigData = mConfiguration.initialize(data); - } + initializeConfiguration(); + 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 @@ -831,59 +828,81 @@ public class ConfigurationChooser extends Composite } else { mDeviceList = new ArrayList<Device>(); } - - // fill with the devices - if (!mDeviceList.isEmpty()) { - Device first = mDeviceList.get(0); - selectDevice(first); - List<State> states = first.getAllStates(); - selectDeviceState(states.get(0)); - } else { - selectDevice(null); - } } /** * Loads the list of {@link IAndroidTarget} and inits the UI with it. */ - private void initTargets() { + private boolean initTargets() { mTargetList.clear(); - IAndroidTarget renderingTarget = mConfiguration.getTarget(); - Sdk currentSdk = Sdk.getCurrent(); if (currentSdk != null) { IAndroidTarget[] targets = currentSdk.getTargets(); - IAndroidTarget match = null; for (int i = 0 ; i < targets.length; i++) { - // FIXME: add check based on project minSdkVersion if (targets[i].hasRenderingLibrary()) { mTargetList.add(targets[i]); - - 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(targets[i])) { - match = targets[i]; - } - } else if (mProjectTarget == targets[i]) { - match = targets[i]; - } } } - if (match == null) { - selectTarget(null); + return true; + } - // the rendering target is the same as the project. - renderingTarget = mProjectTarget; - } else { - selectTarget(match); + return false; + } + + private void initializeConfiguration() { + if (mConfiguration.getDevice() == null) { + String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE); + if (mInitialState != null) { + data = mInitialState; + mInitialState = null; + } + if (data != null) { + mConfiguration.initialize(data); + } + } + } - // set the rendering target to the new object. - renderingTarget = match; + private void updateDevices() { + if (mDeviceList.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 @@ -947,7 +966,9 @@ public class ConfigurationChooser extends Composite */ public void saveConstraints() { String description = mConfiguration.toPersistentString(); - AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, description); + if (description != null && !description.isEmpty()) { + AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, description); + } } // ---- Setting the current UI state ---- @@ -1923,4 +1944,22 @@ public class ConfigurationChooser extends Composite 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(), + ResourceFolderType.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/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 0000000..47c139f --- /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,359 @@ +/* + * 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.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +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.IProject; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** 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$ + + /** 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; + } + + /** + * 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 List<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. + LanguageQualifier language = Locale.ANY_LANGUAGE; + RegionQualifier region = Locale.ANY_REGION; + String locales[] = localeString.split(SEP_LOCALE); + if (locales[0].length() > 0) { + language = new LanguageQualifier(locales[0]); + } + if (locales.length > 1 && locales[1].length() > 0) { + region = new RegionQualifier(locales[1]); + } + description.locale = Locale.create(language, region); + } + + 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.language.getValue() + SEP_LOCALE + locale.region.getValue(); + } else { + value = locale.language.getValue(); + } + 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) { + Map<String, String> activityThemes = manifest.getActivityThemes(); + preferred = activityThemes.get(activity); + } + 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 index dc64b36..a210bf7 100644 --- 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 @@ -59,12 +59,35 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -/** Produces matches for configurations */ +/** + * Produces matches for configurations + * <p> + * See algorithm described here: + * http://developer.android.com/guide/topics/resources/providing-resources.html + */ public class ConfigurationMatcher { 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 ---- @@ -118,14 +141,12 @@ public class ConfigurationMatcher { * @return true if the current edited file is the best match in the project for the * given config. */ - boolean isCurrentFileBestMatchFor(FolderConfiguration config) { - ProjectResources resources = mConfigChooser.getResources(); - IFile editedFile = mConfigChooser.getEditedFile(); - ResourceFile match = resources.getMatchingFile(editedFile.getName(), + public boolean isCurrentFileBestMatchFor(FolderConfiguration config) { + ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), ResourceFolderType.LAYOUT, config); if (match != null) { - return match.getFile().equals(editedFile); + 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."); @@ -149,9 +170,8 @@ public class ConfigurationMatcher { // check the device config (ie sans locale) boolean needConfigChange = true; // if still true, we need to find another config. boolean currentConfigIsCompatible = false; - Configuration configuration = mConfigChooser.getConfiguration(); - State selectedState = configuration.getDeviceState(); - FolderConfiguration editedConfig = configuration.getEditedConfig(); + State selectedState = mConfiguration.getDeviceState(); + FolderConfiguration editedConfig = mConfiguration.getEditedConfig(); if (selectedState != null) { FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState); if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) { @@ -172,7 +192,7 @@ public class ConfigurationMatcher { // first look in the current device. State matchState = null; int localeIndex = -1; - Device device = configuration.getDevice(); + Device device = mConfiguration.getDevice(); if (device != null) { mainloop: for (State state : device.getAllStates()) { testConfig.set(DeviceConfigHelper.getFolderConfig(state)); @@ -196,12 +216,14 @@ public class ConfigurationMatcher { } if (matchState != null) { - configuration.setDeviceState(matchState, true); + mConfiguration.setDeviceState(matchState, true); Locale locale = localeList.get(localeIndex); - configuration.setLocale(locale, true); - mConfigChooser.selectDeviceState(matchState); - mConfigChooser.selectLocale(locale); - configuration.syncFolderConfig(); + 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 @@ -225,9 +247,8 @@ public class ConfigurationMatcher { void findAndSetCompatibleConfig(boolean favorCurrentConfig) { List<Locale> localeList = mConfigChooser.getLocaleList(); List<Device> deviceList = mConfigChooser.getDeviceList(); - Configuration configuration = mConfigChooser.getConfiguration(); - FolderConfiguration editedConfig = configuration.getEditedConfig(); - FolderConfiguration currentConfig = configuration.getFullConfig(); + FolderConfiguration editedConfig = mConfiguration.getEditedConfig(); + FolderConfiguration currentConfig = mConfiguration.getFullConfig(); // list of compatible device/state/locale List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>(); @@ -316,7 +337,7 @@ public class ConfigurationMatcher { } // just display the warning - AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + AdtPlugin.printErrorToConsole(mEditedFile.getProject(), String.format( "'%1$s' is not a best match for any device/locale combination.", editedConfig.toDisplayString()), @@ -326,21 +347,23 @@ public class ConfigurationMatcher { } else if (anyMatches.size() > 0) { // select the best device anyway. ConfigMatch match = selectConfigMatch(anyMatches); - configuration.setDevice(match.device, true); - configuration.setDeviceState(match.state, true); - configuration.setLocale(localeList.get(match.bundle.localeIndex), true); - configuration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); - configuration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), + 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); - mConfigChooser.selectDevice(configuration.getDevice()); - mConfigChooser.selectDeviceState(configuration.getDeviceState()); - mConfigChooser.selectLocale(configuration.getLocale()); + if (mUpdateUi) { + mConfigChooser.selectDevice(mConfiguration.getDevice()); + mConfigChooser.selectDeviceState(mConfiguration.getDeviceState()); + mConfigChooser.selectLocale(mConfiguration.getLocale()); + } - configuration.syncFolderConfig(); + mConfiguration.syncFolderConfig(); // TODO: display a better warning! - AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + AdtPlugin.printErrorToConsole(mEditedFile.getProject(), String.format( "'%1$s' is not a best match for any device/locale combination.", editedConfig.toDisplayString()), @@ -357,17 +380,19 @@ public class ConfigurationMatcher { } } else { ConfigMatch match = selectConfigMatch(bestMatches); - configuration.setDevice(match.device, true); - configuration.setDeviceState(match.state, true); - configuration.setLocale(localeList.get(match.bundle.localeIndex), true); - configuration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); - configuration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true); - - configuration.syncFolderConfig(); - - mConfigChooser.selectDevice(configuration.getDevice()); - mConfigChooser.selectDeviceState(configuration.getDeviceState()); - mConfigChooser.selectLocale(configuration.getLocale()); + 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()); + } } } @@ -455,15 +480,24 @@ public class ConfigurationMatcher { private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { // API 11-13: look for a x-large device - int apiLevel = mConfigChooser.getProjectTarget().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. - Collections.sort(matches, new TabletConfigComparator()); - } else { + 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 - Collections.sort(matches, new PhoneConfigComparator()); + 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, @@ -473,7 +507,7 @@ public class ConfigurationMatcher { LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); if (delegate != null // (Only do this when the two files are in the same project) - && delegate.getEditor().getProject() == mConfigChooser.getProject()) { + && delegate.getEditor().getProject() == mEditedFile.getProject()) { FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); if (configuration != null) { for (ConfigMatch match : matches) { @@ -636,7 +670,11 @@ public class ConfigurationMatcher { } // From the resources, look for a matching file - String name = chooser.getEditedFile().getName(); + IFile editedFile = chooser.getEditedFile(); + if (editedFile == null) { + return null; + } + String name = editedFile.getName(); FolderConfiguration config = chooser.getConfiguration().getFullConfig(); ResourceFile match = resources.getMatchingFile(name, ResourceFolderType.LAYOUT, config); 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 index 30f7dc2..81b4cd0 100644 --- 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 @@ -16,21 +16,25 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; -import static com.android.SdkConstants.FD_RES_LAYOUT; - 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.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.resources.IProject; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; @@ -39,9 +43,9 @@ 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.ArrayList; import java.util.List; /** @@ -52,6 +56,15 @@ 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_GENERATE_DEFAULT = 4; + private static final int ACTION_DELETE_ALL = 5; + private static final int ACTION_PREVIEW_LOCALES = 6; + private static final int ACTION_PREVIEW_SCREENS = 7; + private static final int ACTION_PREVIEW_INCLUDED_IN = 8; + private static final int ACTION_PREVIEW_VARIATIONS = 9; + private static final int ACTION_PREVIEW_CUSTOM = 10; + private static final int ACTION_PREVIEW_NONE = 11; private final ConfigurationChooser mConfigChooser; private final int mAction; @@ -75,77 +88,177 @@ class ConfigurationMenuListener extends SelectionAdapter { } catch (PartInitException ex) { AdtPlugin.log(ex, null); } - break; + 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_GENERATE_DEFAULT: { + previewManager.selectMode(RenderPreviewMode.DEFAULT); + break; + } + case ACTION_DELETE_ALL: { + previewManager.deleteManualPreviews(); + break; + } + case ACTION_PREVIEW_LOCALES: { + previewManager.selectMode(RenderPreviewMode.LOCALES); + break; + } + case ACTION_PREVIEW_SCREENS: { + previewManager.selectMode(RenderPreviewMode.SCREENS); + break; + } + case ACTION_PREVIEW_INCLUDED_IN: { + previewManager.selectMode(RenderPreviewMode.INCLUDES); + break; + } + case ACTION_PREVIEW_VARIATIONS: { + previewManager.selectMode(RenderPreviewMode.VARIATIONS); + break; + } + case ACTION_PREVIEW_CUSTOM: { + previewManager.selectMode(RenderPreviewMode.CUSTOM); + break; + } + case ACTION_PREVIEW_NONE: { + previewManager.selectMode(RenderPreviewMode.NONE); 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), SWT.PUSH, false); + if (mode == RenderPreviewMode.CUSTOM) { + create(menu, "Delete All Thumbnails", + new ConfigurationMenuListener(chooser, ACTION_DELETE_ALL, null), SWT.PUSH, false); + } + + @SuppressWarnings("unused") + MenuItem configSeparator = new MenuItem(menu, SWT.SEPARATOR); + + //create(menu, "Generate Default Thumbnails", + create(menu, "Preview Sample", + new ConfigurationMenuListener(chooser, ACTION_GENERATE_DEFAULT, null), SWT.RADIO, + mode == RenderPreviewMode.DEFAULT); + create(menu, "Preview All Screen Sizes", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_SCREENS, null), SWT.RADIO, + mode == RenderPreviewMode.SCREENS); + + MenuItem localeItem = create(menu, "Preview All Locales", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_LOCALES, null), SWT.RADIO, + mode == RenderPreviewMode.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_INCLUDED_IN, null), + SWT.RADIO, mode == RenderPreviewMode.INCLUDES); + if (!canPreviewIncluded) { + includedItem.setEnabled(false); + } - // Compute the set of layout files defining this layout resource IFile file = chooser.getEditedFile(); - String name = file.getName(); - IContainer resFolder = file.getParent().getParent(); - List<IFile> variations = new ArrayList<IFile>(); - try { - for (IResource resource : resFolder.members()) { - if (resource.getName().startsWith(FD_RES_LAYOUT) - && resource instanceof IContainer) { - IContainer layoutFolder = (IContainer) resource; - IResource variation = layoutFolder.findMember(name); - if (variation instanceof IFile) { - variations.add((IFile) variation); - } - } - } - } catch (CoreException e1) { - AdtPlugin.log(e1, null); + List<IFile> variations = AdtUtils.getResourceVariations(file, true); + MenuItem variationsItem = create(menu, "Preview Layout Versions", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_VARIATIONS, null), + SWT.RADIO, mode == RenderPreviewMode.VARIATIONS); + if (variations.size() <= 1) { + variationsItem.setEnabled(false); } - ResourceManager manager = ResourceManager.getInstance(); - for (final IFile resource : variations) { - MenuItem item = new MenuItem(menu, SWT.CHECK); + create(menu, "Manual Previews", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_CUSTOM, null), + SWT.RADIO, mode == RenderPreviewMode.CUSTOM); + create(menu, "None", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_NONE, null), + SWT.RADIO, mode == RenderPreviewMode.NONE); - IFolder parent = (IFolder) resource.getParent(); - ResourceFolder parentResource = manager.getResourceFolder(parent); - FolderConfiguration configuration = parentResource.getConfiguration(); - String title = configuration.toDisplayString(); - item.setText(title); + if (variations.size() > 1) { + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); - boolean selected = file.equals(resource); - if (selected) { - item.setSelection(true); - item.setEnabled(false); - } + 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(); - item.addSelectionListener(new ConfigurationMenuListener(chooser, - ACTION_SELECT_CONFIG, resource)); + MenuItem item = create(menu, title, + new ConfigurationMenuListener(chooser, ACTION_SELECT_CONFIG, resource), + 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().equals(configuration.getFullConfig())) { + 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 = new MenuItem(menu, SWT.PUSH); - item.setText("Create New..."); + MenuItem item = create(menu, "Create New...", + new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE, null), + SWT.PUSH, false); item.setImage(IconFactory.getInstance().getIcon(ICON_NEW_CONFIG)); - //item.setToolTipText("Duplicate: Create new configuration for this layout"); - - item.addSelectionListener( - new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE, null)); } Rectangle bounds = combo.getBounds(); @@ -154,4 +267,16 @@ class ConfigurationMenuListener extends SelectionAdapter { 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; + } } 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 0000000..73d08f8 --- /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,305 @@ +/* + * 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; + +/** + * 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 ComplementingConfiguration}, 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 { + protected final Configuration mParent; + protected boolean mOverrideLocale; + protected boolean mOverrideTarget; + protected boolean mOverrideDevice; + protected boolean mOverrideDeviceState; + protected boolean mOverrideNightMode; + protected boolean mOverrideUiMode; + + /** + * 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); + } + } + + /** + * 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) { + mOverrideLocale = override; + } + + /** + * Returns true if the locale is overridden + * + * @return true if the locale is overridden + */ + public boolean isOverridingLocale() { + return mOverrideLocale; + } + + @Override + @NonNull + public Locale getLocale() { + if (mOverrideLocale) { + return super.getLocale(); + } else { + return mParent.getLocale(); + } + } + + @Override + public void setLocale(@NonNull Locale locale, boolean skipSync) { + if (mOverrideLocale) { + 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) { + mOverrideTarget = override; + } + + @Override + @Nullable + public IAndroidTarget getTarget() { + if (mOverrideTarget) { + return super.getTarget(); + } else { + return mParent.getTarget(); + } + } + + @Override + public void setTarget(IAndroidTarget target, boolean skipSync) { + if (mOverrideTarget) { + 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) { + mOverrideDevice = override; + } + + @Override + @Nullable + public Device getDevice() { + if (mOverrideDevice) { + return super.getDevice(); + } else { + return mParent.getDevice(); + } + } + + @Override + public void setDevice(Device device, boolean skipSync) { + if (mOverrideDevice) { + 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) { + mOverrideDeviceState = override; + } + + @Override + @Nullable + public State getDeviceState() { + if (mOverrideDeviceState) { + return super.getDeviceState(); + } else { + State state = mParent.getDeviceState(); + if (mOverrideDevice) { + // 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 (mOverrideDeviceState) { + super.setDeviceState(state, skipSync); + } else { + if (mOverrideDevice) { + 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) { + mOverrideNightMode = override; + } + + @Override + @NonNull + public NightMode getNightMode() { + if (mOverrideNightMode) { + return super.getNightMode(); + } else { + return mParent.getNightMode(); + } + } + + @Override + public void setNightMode(@NonNull NightMode night, boolean skipSync) { + if (mOverrideNightMode) { + 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) { + mOverrideUiMode = override; + } + + @Override + @NonNull + public UiMode getUiMode() { + if (mOverrideUiMode) { + return super.getUiMode(); + } else { + return mParent.getUiMode(); + } + } + + @Override + public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) { + if (mOverrideUiMode) { + 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; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java index 5650772..717347f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java @@ -40,6 +40,9 @@ public class CanvasTransform { /** Canvas image size (original, before zoom), in pixels. */ private int mImgSize; + /** Full size being scrolled (after zoom), in pixels */ + private int mFullSize;; + /** Client size, in pixels. */ private int mClientSize; @@ -83,6 +86,11 @@ public class CanvasTransform { } } + /** Recomputes the scrollbar and view port settings */ + public void refresh() { + resizeScrollbar(); + } + /** * Returns current scaling factor. * @@ -110,13 +118,22 @@ public class CanvasTransform { return (int) (mImgSize * mScale); } - /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */ - public void setSize(int imgSize, int clientSize) { + /** + * Changes the size of the canvas image and the client size. Recomputes + * scrollbars. + * + * @param imgSize the size of the image being scaled + * @param fullSize the size of the full view area being scrolled + * @param clientSize the size of the view port + */ + public void setSize(int imgSize, int fullSize, int clientSize) { mImgSize = imgSize; - setClientSize(clientSize); + mFullSize = fullSize; + mClientSize = clientSize; + mScrollbar.setPageIncrement(clientSize); + resizeScrollbar(); } - /** Changes the size of the client size. Recomputes scrollbars. */ public void setClientSize(int clientSize) { mClientSize = clientSize; mScrollbar.setPageIncrement(clientSize); @@ -125,7 +142,7 @@ public class CanvasTransform { private void resizeScrollbar() { // scaled image size - int sx = (int) (mImgSize * mScale); + int sx = (int) (mScale * mFullSize); // Adjust margin such that for zoomed out views // we don't waste space (unless the viewport is @@ -150,6 +167,11 @@ public class CanvasTransform { mMargin = DEFAULT_MARGIN; } + if (mCanvas.getPreviewManager().hasPreviews()) { + // Make more room for the previews + mMargin = 2; + } + // actual client area is always reduced by the margins int cx = mClientSize - 2 * mMargin; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java index 1625195..b364f57 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java @@ -15,16 +15,13 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.TOOLS_URI; - - import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML; -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; @@ -61,6 +58,7 @@ import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; /** * Various utility methods for manipulating DOM nodes. @@ -828,6 +826,33 @@ public class DomUtilities { } /** + * Creates an empty non-Eclipse XML document. + * This is used when you need to use XML operations not supported by + * the Eclipse XML model (such as serialization). + * <p> + * The new document will not validate, will ignore comments, and will + * support namespace. + * + * @return the new document + */ + @Nullable + public static Document createEmptyPlainDocument() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setIgnoringComments(true); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + return builder.newDocument(); + } catch (ParserConfigurationException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** * Parses the given XML string as a DOM document, using the JDK parser. * The parser does not validate, and is namespace aware. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java index 468d159..98bc25e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java @@ -39,6 +39,7 @@ import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.events.TypedEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Device; @@ -433,7 +434,8 @@ public class GestureManager { * Helper class which implements the {@link MouseMoveListener}, * {@link MouseListener} and {@link KeyListener} interfaces. */ - private class Listener implements MouseMoveListener, MouseListener, KeyListener { + private class Listener implements MouseMoveListener, MouseListener, MouseTrackListener, + KeyListener { // --- MouseMoveListener --- @@ -443,15 +445,16 @@ public class GestureManager { mLastMouseY = e.y; mLastStateMask = e.stateMask; + ControlPoint controlPoint = ControlPoint.create(mCanvas, e); if ((e.stateMask & SWT.BUTTON_MASK) != 0) { if (mCurrentGesture != null) { - ControlPoint controlPoint = ControlPoint.create(mCanvas, e); updateMouse(controlPoint, e); mCanvas.redraw(); } } else { - updateCursor(ControlPoint.create(mCanvas, e)); + updateCursor(controlPoint); mCanvas.hover(e); + mCanvas.getPreviewManager().moved(controlPoint); } } @@ -460,7 +463,13 @@ public class GestureManager { @Override public void mouseUp(MouseEvent e) { ControlPoint mousePos = ControlPoint.create(mCanvas, e); + if (mCurrentGesture == null) { + // If clicking on a configuration preview, just process it there + if (mCanvas.getPreviewManager().click(mousePos)) { + return; + } + // Just a click, select Pair<SelectionItem, SelectionHandle> handlePair = mCanvas.getSelectionManager().findHandle(mousePos); @@ -507,6 +516,24 @@ public class GestureManager { } } + // --- MouseTrackListener --- + + @Override + public void mouseEnter(MouseEvent e) { + ControlPoint mousePos = ControlPoint.create(mCanvas, e); + mCanvas.getPreviewManager().enter(mousePos); + } + + @Override + public void mouseExit(MouseEvent e) { + ControlPoint mousePos = ControlPoint.create(mCanvas, e); + mCanvas.getPreviewManager().exit(mousePos); + } + + @Override + public void mouseHover(MouseEvent e) { + } + // --- KeyListener --- @Override diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index 726824f..891feea 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -30,7 +30,6 @@ import static com.android.SdkConstants.STRING_PREFIX; import static com.android.SdkConstants.VALUE_FILL_PARENT; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; -import static com.android.ide.eclipse.adt.AdtUtils.isUiThread; import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser.NAME_CONFIG_STATE; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST; @@ -658,6 +657,8 @@ public class GraphicalEditorPart extends EditorPart return true; } + getCanvasControl().getPreviewManager().configurationChanged(flags); + // Before doing the normal process, test for the following case. // - the editor is being opened (or reset for a new input) // - the file being opened is not the best match for any possible configuration @@ -746,7 +747,7 @@ public class GraphicalEditorPart extends EditorPart // out to fit the content, or zoom back in if we were zoomed out more from the // previous view, but only up to 100% such that we never blow up pixels if (mActionBar.isZoomingAllowed()) { - getCanvasControl().setFitScale(true); + getCanvasControl().setFitScale(true, true /*allowZoomIn*/); } } @@ -1039,6 +1040,8 @@ public class GraphicalEditorPart extends EditorPart if (mNeedsRecompute) { recomputeLayout(); } + + mCanvasViewer.getCanvas().syncPreviewMode(); } } @@ -1253,6 +1256,7 @@ public class GraphicalEditorPart extends EditorPart } UiDocumentNode model = getModel(); + LayoutCanvas canvas = mCanvasViewer.getCanvas(); if (!ensureModelValid(model)) { // Although we display an error, we still treat an empty document as a // successful layout result so that we can drop new elements in it. @@ -1260,7 +1264,7 @@ public class GraphicalEditorPart extends EditorPart // For that purpose, create a special LayoutScene that has no image, // no root view yet indicates success and then update the canvas with it. - mCanvasViewer.getCanvas().setSession( + canvas.setSession( new StaticRenderSession( Result.Status.SUCCESS.createResult(), null /*rootViewInfo*/, null /*image*/), @@ -1278,6 +1282,8 @@ public class GraphicalEditorPart extends EditorPart IProject project = mEditedFile.getProject(); renderWithBridge(project, model, layoutLib); + + canvas.getPreviewManager().renderPreviews(); } } finally { // no matter the result, we are done doing the recompute based on the latest @@ -1462,11 +1468,6 @@ public class GraphicalEditorPart extends EditorPart return null; } - if (mConfigChooser.isDisposed()) { - return null; - } - assert isUiThread(); - // attempt to get a target from the configuration selector. IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget(); if (renderingTarget != null) { @@ -1501,6 +1502,10 @@ public class GraphicalEditorPart extends EditorPart private boolean ensureModelValid(UiDocumentNode model) { // check there is actually a model (maybe the file is empty). if (model.getUiChildren().size() == 0) { + if (mEditorDelegate.getEditor().isCreatingPages()) { + displayError("Loading editor"); + return false; + } displayError( "No XML content. Please add a root view or layout to your document."); return false; @@ -1565,7 +1570,7 @@ public class GraphicalEditorPart extends EditorPart } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) { displayFailingClasses(missingClasses, brokenClasses, false); displayUserStackTrace(logger, true); - } else { + } else if (session != null) { // Nope, no missing or broken classes. Clear success, congrats! hideError(); @@ -2798,7 +2803,7 @@ public class GraphicalEditorPart extends EditorPart // Auto zoom the surface if you open or close flyout windows such as the palette // or the property/outline views if (newState == STATE_OPEN || newState == STATE_COLLAPSED && oldState == STATE_OPEN) { - getCanvasControl().setFitScale(true /*onlyZoomOut*/); + getCanvasControl().setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); } sDockingStateVersion++; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java index c55d0d8..8f7172e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java @@ -21,6 +21,12 @@ import static com.android.SdkConstants.DOT_GIF; import static com.android.SdkConstants.DOT_JPG; import static com.android.SdkConstants.DOT_PNG; import static com.android.utils.SdkUtils.endsWithIgnoreCase; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_INTERPOLATION; +import static java.awt.RenderingHints.KEY_RENDERING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; +import static java.awt.RenderingHints.VALUE_RENDER_QUALITY; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -34,7 +40,6 @@ import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.IOException; @@ -471,6 +476,7 @@ public class ImageUtils { * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. + * The size of the shadow is {@link #SHADOW_SIZE}. * * @param image the image to draw the shadow into * @param x the left coordinate of the left hand side of the rectangle @@ -489,12 +495,40 @@ public class ImageUtils { } /** + * Draws a small drop shadow for the given rectangle into the given context. It + * will not draw anything if the rectangle is smaller than a minimum + * determined by the assets used to draw the shadow graphics. + * The size of the shadow is {@link #SMALL_SHADOW_SIZE}. + * + * @param image the image to draw the shadow into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static final void drawSmallRectangleShadow(BufferedImage image, + int x, int y, int width, int height) { + Graphics gc = image.getGraphics(); + try { + drawSmallRectangleShadow(gc, x, y, width, height); + } finally { + gc.dispose(); + } + } + + /** * The width and height of the drop shadow painted by * {@link #drawRectangleShadow(Graphics, int, int, int, int)} */ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics /** + * The width and height of the drop shadow painted by + * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)} + */ + public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics + + /** * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. @@ -569,6 +603,73 @@ public class ImageUtils { null); } + /** + * Draws a small drop shadow for the given rectangle into the given context. It + * will not draw anything if the rectangle is smaller than a minimum + * determined by the assets used to draw the shadow graphics. + * <p> + * + * @param gc the graphics context to draw into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static final void drawSmallRectangleShadow(Graphics gc, + int x, int y, int width, int height) { + if (sShadow2BottomLeft == null) { + // Shadow graphics. This was generated by creating a drop shadow in + // Gimp, using the parameters x offset=5, y offset=%, blur radius=5, + // color=black, and opacity=51. These values attempt to make a shadow + // that is legible both for dark and light themes, on top of the + // canvas background (rgb(150,150,150). Darker shadows would tend to + // blend into the foreground for a dark holo screen, and lighter shadows + // would be hard to spot on the canvas background. If you make adjustments, + // make sure to check the shadow with both dark and light themes. + // + // After making the graphics, I cut out the top right, bottom left + // and bottom right corners as 20x20 images, and these are reproduced by + // painting them in the corresponding places in the target graphics context. + // I then grabbed a single horizontal gradient line from the middle of the + // right edge,and a single vertical gradient line from the bottom. These + // are then painted scaled/stretched in the target to fill the gaps between + // the three corner images. + // + // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right + sShadow2BottomLeft = readImage("shadow2-bl.png"); //$NON-NLS-1$ + sShadow2Bottom = readImage("shadow2-b.png"); //$NON-NLS-1$ + sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$ + sShadow2Right = readImage("shadow2-r.png"); //$NON-NLS-1$ + sShadow2TopRight = readImage("shadow2-tr.png"); //$NON-NLS-1$ + assert sShadow2BottomLeft != null; + assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE; + assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE; + } + + int blWidth = sShadow2BottomLeft.getWidth(); + int trHeight = sShadow2TopRight.getHeight(); + if (width < blWidth) { + return; + } + if (height < trHeight) { + return; + } + + gc.drawImage(sShadow2BottomLeft, x, y + height, null); + gc.drawImage(sShadow2BottomRight, x + width, y + height, null); + gc.drawImage(sShadow2TopRight, x + width, y, null); + gc.drawImage(sShadow2Bottom, + x + sShadow2BottomLeft.getWidth(), y + height, + x + width, y + height + sShadow2Bottom.getHeight(), + 0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(), + null); + gc.drawImage(sShadow2Right, + x + width, y + sShadow2TopRight.getHeight(), + x + width + sShadow2Right.getWidth(), y + height, + 0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(), + null); + } + @Nullable private static BufferedImage readImage(@NonNull String name) { InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$ @@ -589,12 +690,19 @@ public class ImageUtils { return null; } + // Normal drop shadow private static BufferedImage sShadowBottomLeft; private static BufferedImage sShadowBottom; private static BufferedImage sShadowBottomRight; private static BufferedImage sShadowRight; private static BufferedImage sShadowTopRight; + // Small drop shadow + private static BufferedImage sShadow2BottomLeft; + private static BufferedImage sShadow2Bottom; + private static BufferedImage sShadow2BottomRight; + private static BufferedImage sShadow2Right; + private static BufferedImage sShadow2TopRight; /** * Returns a bounding rectangle for the given list of rectangles. If the list is @@ -731,20 +839,102 @@ public class ImageUtils { if (imageType == BufferedImage.TYPE_CUSTOM) { imageType = BufferedImage.TYPE_INT_ARGB; } - BufferedImage scaled = new BufferedImage( - destWidth + rightMargin, destHeight + bottomMargin, imageType); - Graphics2D g2 = scaled.createGraphics(); - g2.setComposite(AlphaComposite.Src); - g2.setColor(new Color(0, true)); - g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, null); - g2.dispose(); + if (xScale > 0.5 && yScale > 0.5) { + BufferedImage scaled = + new BufferedImage(destWidth + rightMargin, destHeight + bottomMargin, imageType); + Graphics2D g2 = scaled.createGraphics(); + g2.setComposite(AlphaComposite.Src); + g2.setColor(new Color(0, true)); + g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin); + g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, + null); + g2.dispose(); + return scaled; + } else { + // When creating a thumbnail, using the above code doesn't work very well; + // you get some visible artifacts, especially for text. Instead use the + // technique of repeatedly scaling the image into half; this will cause + // proper averaging of neighboring pixels, and will typically (for the kinds + // of screen sizes used by this utility method in the layout editor) take + // about 3-4 iterations to get the result since we are logarithmically reducing + // the size. Besides, each successive pass in operating on much fewer pixels + // (a reduction of 4 in each pass). + // + // However, we may not be resizing to a size that can be reached exactly by + // successively diving in half. Therefore, once we're within a factor of 2 of + // the final size, we can do a resize to the exact target size. + // However, we can get even better results if we perform this final resize + // up front. Let's say we're going from width 1000 to a destination width of 85. + // The first approach would cause a resize from 1000 to 500 to 250 to 125, and + // then a resize from 125 to 85. That last resize can distort/blur a lot. + // Instead, we can start with the destination width, 85, and double it + // successfully until we're close to the initial size: 85, then 170, + // then 340, and finally 680. (The next one, 1360, is larger than 1000). + // So, now we *start* the thumbnail operation by resizing from width 1000 to + // width 680, which will preserve a lot of visual details such as text. + // Then we can successively resize the image in half, 680 to 340 to 170 to 85. + // We end up with the expected final size, but we've been doing an exact + // divide-in-half resizing operation at the end so there is less distortion. + + + int iterations = 0; // Number of halving operations to perform after the initial resize + int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer + int nearestHeight = destHeight; + while (nearestWidth < sourceWidth / 2) { + nearestWidth *= 2; + nearestHeight *= 2; + iterations++; + } - return scaled; + // If we're supposed to add in margins, we need to do it in the initial resizing + // operation if we don't have any subsequent resizing operations. + if (iterations == 0) { + nearestWidth += rightMargin; + nearestHeight += bottomMargin; + } + + BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType); + Graphics2D g2 = scaled.createGraphics(); + g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, nearestWidth, nearestHeight, + 0, 0, sourceWidth, sourceHeight, null); + g2.dispose(); + + sourceWidth = nearestWidth; + sourceHeight = nearestHeight; + source = scaled; + + for (int iteration = iterations - 1; iteration >= 0; iteration--) { + int halfWidth = sourceWidth / 2; + int halfHeight = sourceHeight / 2; + if (iteration == 0) { // Last iteration: Add margins in final image + scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin, + imageType); + } else { + scaled = new BufferedImage(halfWidth, halfHeight, imageType); + } + g2 = scaled.createGraphics(); + g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, + halfWidth, halfHeight, 0, 0, + sourceWidth, sourceHeight, + null); + g2.dispose(); + + sourceWidth = halfWidth; + sourceHeight = halfHeight; + source = scaled; + iterations--; + } + return scaled; + } } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java index 0b8f784..7bab914 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java @@ -20,6 +20,8 @@ import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.EXT_XML; import static com.android.SdkConstants.FD_RESOURCES; import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; import static com.android.ide.eclipse.adt.AdtConstants.WS_LAYOUTS; import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; @@ -29,6 +31,8 @@ import static org.eclipse.core.resources.IResourceDelta.CHANGED; import static org.eclipse.core.resources.IResourceDelta.CONTENT; import static org.eclipse.core.resources.IResourceDelta.REMOVED; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.ide.common.resources.ResourceFile; import com.android.ide.common.resources.ResourceFolder; @@ -56,6 +60,7 @@ import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; @@ -117,8 +122,9 @@ public class IncludeFinder { * Returns the {@link IncludeFinder} for the given project * * @param project the project the finder is associated with - * @return an {@IncludeFinder} for the given project, never null + * @return an {@link IncludeFinder} for the given project, never null */ + @NonNull public static IncludeFinder get(IProject project) { IncludeFinder finder = null; try { @@ -157,6 +163,7 @@ public class IncludeFinder { * @param included the file that is included * @return the files that are including the given file, or null or empty */ + @Nullable public List<Reference> getIncludedBy(IResource included) { ensureInitialized(); String mapKey = getMapKey(included); @@ -503,8 +510,10 @@ public class IncludeFinder { * empty if the file does not include any include tags; it does this by only parsing * if it detects the string <include in the file. */ - private List<String> findIncludes(String xml) { - int index = xml.indexOf("<include"); //$NON-NLS-1$ + @VisibleForTesting + @NonNull + static List<String> findIncludes(@NonNull String xml) { + int index = xml.indexOf(ATTR_LAYOUT); if (index != -1) { return findIncludesInXml(xml); } @@ -518,7 +527,9 @@ public class IncludeFinder { * @param xml layout XML content to be parsed for includes * @return a list of included urls, or null */ - private List<String> findIncludesInXml(String xml) { + @VisibleForTesting + @NonNull + static List<String> findIncludesInXml(@NonNull String xml) { Document document = DomUtilities.parseDocument(xml, false /*logParserErrors*/); if (document != null) { return findIncludesInDocument(document); @@ -528,27 +539,52 @@ public class IncludeFinder { } /** Searches the given DOM document and returns the list of includes, if any */ - private List<String> findIncludesInDocument(Document document) { - NodeList includes = document.getElementsByTagName(VIEW_INCLUDE); - if (includes.getLength() > 0) { - List<String> urls = new ArrayList<String>(); - for (int i = 0; i < includes.getLength(); i++) { - Element element = (Element) includes.item(i); - String url = element.getAttribute(ATTR_LAYOUT); + @NonNull + private static List<String> findIncludesInDocument(@NonNull Document document) { + List<String> includes = findIncludesInDocument(document, null); + if (includes == null) { + includes = Collections.emptyList(); + } + return includes; + } + + @Nullable + private static List<String> findIncludesInDocument(@NonNull Node node, + @Nullable List<String> urls) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + String tag = node.getNodeName(); + boolean isInclude = tag.equals(VIEW_INCLUDE); + boolean isFragment = tag.equals(VIEW_FRAGMENT); + if (isInclude || isFragment) { + Element element = (Element) node; + String url; + if (isInclude) { + url = element.getAttribute(ATTR_LAYOUT); + } else { + url = element.getAttributeNS(TOOLS_URI, ATTR_LAYOUT); + } if (url.length() > 0) { String resourceName = urlToLocalResource(url); if (resourceName != null) { + if (urls == null) { + urls = new ArrayList<String>(); + } urls.add(resourceName); } } + } + } - return urls; + NodeList children = node.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + urls = findIncludesInDocument(children.item(i), urls); } - return Collections.emptyList(); + return urls; } + /** * Returns the layout URL to a local resource name (provided the URL is a local * resource, not something in @android etc.) Returns null otherwise. @@ -628,6 +664,7 @@ public class IncludeFinder { ResourceManager.getInstance().addListener(sListener); } + /** Stop listening on project resources */ public static void stop() { assert sListener != null; ResourceManager.getInstance().addListener(sListener); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java index 4368db4..d5b46b4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java @@ -691,7 +691,7 @@ public class LayoutActionBar extends Composite { * Reset the canvas scale to best fit (so content is as large as possible without scrollbars) */ void rescaleToFit(boolean onlyZoomOut) { - mEditor.getCanvasControl().setFitScale(onlyZoomOut); + mEditor.getCanvasControl().setFitScale(onlyZoomOut, true /*allowZoomIn*/); } boolean rescaleToReal(boolean real) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 86878ac..81fd8d2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.SdkConstants; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.api.INode; import com.android.ide.common.api.Margins; import com.android.ide.common.api.Point; @@ -150,10 +151,10 @@ public class LayoutCanvas extends Canvas { private final NodeFactory mNodeFactory = new NodeFactory(this); /** Vertical scaling & scrollbar information. */ - private CanvasTransform mVScale; + private final CanvasTransform mVScale; /** Horizontal scaling & scrollbar information. */ - private CanvasTransform mHScale; + private final CanvasTransform mHScale; /** Drag source associated with this canvas. */ private DragSource mDragSource; @@ -218,6 +219,9 @@ public class LayoutCanvas extends Canvas { /** The overlay which paints masks hiding everything but included content. */ private IncludeOverlay mIncludeOverlay; + /** Configuration previews shown next to the layout */ + private final RenderPreviewManager mPreviewManager; + /** * Gesture Manager responsible for identifying mouse, keyboard and drag and * drop events. @@ -239,6 +243,14 @@ public class LayoutCanvas extends Canvas { private Color mBackgroundColor; + /** + * Creates a new {@link LayoutCanvas} widget + * + * @param editorDelegate the associated editor delegate + * @param rulesEngine the rules engine + * @param parent parent SWT widget + * @param style the SWT style + */ public LayoutCanvas(LayoutEditorDelegate editorDelegate, RulesEngine rulesEngine, Composite parent, @@ -253,6 +265,7 @@ public class LayoutCanvas extends Canvas { mClipboardSupport = new ClipboardSupport(this, parent); mHScale = new CanvasTransform(this, getHorizontalBar()); mVScale = new CanvasTransform(this, getVerticalBar()); + mPreviewManager = new RenderPreviewManager(this); // Unit test suite passes a null here; TODO: Replace with mocking IFile file = editorDelegate != null ? editorDelegate.getEditor().getInputFile() : null; @@ -314,9 +327,7 @@ public class LayoutCanvas extends Canvas { } } - Rectangle clientArea = getClientArea(); - mHScale.setClientSize(clientArea.width); - mVScale.setClientSize(clientArea.height); + updateScrollBars(); // Update the zoom level in the canvas when you toggle the zoom if (coordinator != null) { @@ -355,6 +366,37 @@ public class LayoutCanvas extends Canvas { mLintTooltipManager.register(); } + void updateScrollBars() { + Rectangle clientArea = getClientArea(); + Image image = mImageOverlay.getImage(); + if (image != null) { + ImageData imageData = image.getImageData(); + int clientWidth = clientArea.width; + int clientHeight = clientArea.height; + + int imageWidth = imageData.width; + int imageHeight = imageData.height; + + int fullWidth = imageWidth; + int fullHeight = imageHeight; + + if (mPreviewManager.hasPreviews()) { + fullHeight = Math.max(fullHeight, + (int) (mPreviewManager.getHeight() / mHScale.getScale())); + } + + if (clientWidth == 0) { + clientWidth = imageWidth; + } + if (clientHeight == 0) { + clientHeight = imageHeight; + } + + mHScale.setSize(imageWidth, fullWidth, clientWidth); + mVScale.setSize(imageHeight, fullHeight, clientHeight); + } + } + private Runnable mZoomCheck = new Runnable() { private Boolean mWasZoomed; @@ -375,7 +417,7 @@ public class LayoutCanvas extends Canvas { LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor() .getLayoutActionBar(); if (actionBar.isZoomingAllowed()) { - setFitScale(true /*onlyZoomOut*/); + setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); } } mWasZoomed = zoomed; @@ -419,14 +461,22 @@ public class LayoutCanvas extends Canvas { if (c == '1' && actionBar.isZoomingAllowed()) { setScale(1, true); } else if (c == '0' && actionBar.isZoomingAllowed()) { - setFitScale(true); + setFitScale(true, true /*allowZoomIn*/); } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 && actionBar.isZoomingAllowed()) { - setFitScale(false); - } else if (c == '+' && actionBar.isZoomingAllowed()) { - actionBar.rescale(1); + setFitScale(false, true /*allowZoomIn*/); + } else if ((c == '+' || c == '=') && actionBar.isZoomingAllowed()) { + if ((e.stateMask & SWT.MOD1) != 0) { + mPreviewManager.zoomIn(); + } else { + actionBar.rescale(1); + } } else if (c == '-' && actionBar.isZoomingAllowed()) { - actionBar.rescale(-1); + if ((e.stateMask & SWT.MOD1) != 0) { + mPreviewManager.zoomOut(); + } else { + actionBar.rescale(-1); + } } } } @@ -507,9 +557,20 @@ public class LayoutCanvas extends Canvas { mBackgroundColor = null; } + mPreviewManager.disposePreviews(); mViewHierarchy.dispose(); } + /** + * Returns the configuration preview manager for this canvas + * + * @return the configuration preview manager for this canvas + */ + @NonNull + public RenderPreviewManager getPreviewManager() { + return mPreviewManager; + } + /** Returns the Rules Engine, associated with the current project. */ /* package */ RulesEngine getRulesEngine() { return mRulesEngine; @@ -539,6 +600,8 @@ public class LayoutCanvas extends Canvas { /** * Returns the {@link LayoutEditorDelegate} associated with this canvas. + * + * @return the delegate */ public LayoutEditorDelegate getEditorDelegate() { return mEditorDelegate; @@ -670,15 +733,14 @@ public class LayoutCanvas extends Canvas { mViewHierarchy.setSession(session, explodedNodes, layoutlib5); if (mViewHierarchy.isValid() && session != null) { - Image image = mImageOverlay.setImage(session.getImage(), session.isAlphaChannelImage()); + Image image = mImageOverlay.setImage(session.getImage(), + session.isAlphaChannelImage()); mOutlinePage.setModel(mViewHierarchy.getRoot()); mEditorDelegate.getGraphicalEditor().setModel(mViewHierarchy.getRoot()); if (image != null) { - Rectangle clientArea = getClientArea(); - mHScale.setSize(image.getImageData().width, clientArea.width); - mVScale.setSize(image.getImageData().height, clientArea.height); + updateScrollBars(); if (mZoomFitNextImage) { // Must be run asynchronously because getClientArea() returns 0 bounds // when the editor is being initialized @@ -691,6 +753,9 @@ public class LayoutCanvas extends Canvas { } }); } + + // Ensure that if we have a a preview mode enabled, it's shown + syncPreviewMode(); } } @@ -703,7 +768,7 @@ public class LayoutCanvas extends Canvas { LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor() .getLayoutActionBar(); if (actionBar.isZoomingAllowed()) { - setFitScale(true); + setFitScale(true, true /*allowZoomIn*/); } } } @@ -713,6 +778,13 @@ public class LayoutCanvas extends Canvas { redraw(); } + /** + * Returns the zoom scale factor of the canvas (the amount the full + * resolution render of the device is zoomed before being shown on the + * canvas) + * + * @return the image scale + */ public double getScale() { return mHScale.getScale(); } @@ -746,8 +818,13 @@ public class LayoutCanvas extends Canvas { * @param onlyZoomOut if true, then the zooming factor will never be larger than 1, * which means that this function will zoom out if necessary to show the * rendered image, but it will never zoom in. + * TODO: Rename this, it sounds like it conflicts with allowZoomIn, + * even though one is referring to the zoom level and one is referring + * to the overall act of scaling above/below 1. + * @param allowZoomIn if false, then if the computed zoom factor is smaller than + * the current zoom factor, it will be ignored. */ - void setFitScale(boolean onlyZoomOut) { + public void setFitScale(boolean onlyZoomOut, boolean allowZoomIn) { ImageOverlay imageOverlay = getImageOverlay(); if (imageOverlay == null) { return; @@ -758,6 +835,13 @@ public class LayoutCanvas extends Canvas { int canvasWidth = canvasSize.width; int canvasHeight = canvasSize.height; + if (mPreviewManager.hasPreviews()) { + canvasWidth = 2 * canvasWidth / 3; + } else { + canvasWidth -= 4; + canvasHeight -= 4; + } + ImageData imageData = image.getImageData(); int sceneWidth = imageData.width; int sceneHeight = imageData.height; @@ -796,6 +880,10 @@ public class LayoutCanvas extends Canvas { scale = Math.min(1.0, scale); } + if (!allowZoomIn && scale > getScale()) { + return; + } + setScale(scale, true); } } @@ -857,6 +945,8 @@ public class LayoutCanvas extends Canvas { mImageOverlay.paint(gc); } + mPreviewManager.paint(gc); + if (mShowOutline) { if (mOutlineOverlay == null) { mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale); @@ -1552,4 +1642,46 @@ public class LayoutCanvas extends Canvas { mLintTooltipManager.hide(); } } + + /** @see #setPreview(RenderPreview) */ + private RenderPreview mPreview; + + /** + * Sets the {@link RenderPreview} associated with the currently rendering + * configuration. + * <p> + * A {@link RenderPreview} has various additional state beyond its rendering, + * such as its display name (which can be edited by the user). When you click on + * previews, the layout editor switches to show the given configuration preview. + * The preview is then no longer shown in the list of previews and is instead rendered + * in the main editor. However, when you then switch away to some other preview, we + * want to be able to restore the preview with all its state. + * + * @param preview the preview associated with the current canvas + */ + public void setPreview(@Nullable RenderPreview preview) { + mPreview = preview; + } + + /** + * Returns the {@link RenderPreview} associated with this layout canvas. + * + * @see #setPreview(RenderPreview) + * @return the {@link RenderPreview} + */ + @Nullable + public RenderPreview getPreview() { + return mPreview; + } + + /** Ensures that the configuration previews are up to date for this canvas */ + public void syncPreviewMode() { + if (mImageOverlay != null && mImageOverlay.getImage() != null) { + if (mPreviewManager.recomputePreviews(false)) { + // Zoom when syncing modes + mZoomFitNextImage = true; + ensureZoomed(); + } + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java index 119506b..ad4b94d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java @@ -378,6 +378,9 @@ public class PaletteControl extends Composite { ConfigurationChooser configChooser = mEditor.getConfigurationChooser(); String theme = configChooser.getThemeName(); String device = configChooser.getDeviceName(); + if (device == null) { + return; + } AndroidTargetData targetData = target != null ? Sdk.getCurrent().getTargetData(target) : null; if (target == mCurrentTarget && targetData == mCurrentTargetData diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java new file mode 100644 index 0000000..aafa69d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java @@ -0,0 +1,1015 @@ +/* + * 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.gle2; + +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 static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_DEVICE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_FOLDER; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_LOCALE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_RENDER_TARGET; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_THEME; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +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.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +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.ide.eclipse.adt.io.IFileWrapper; +import com.android.io.IAbstractFile; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.progress.UIJob; +import org.w3c.dom.Document; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Map; + +/** + * Represents a preview rendering of a given configuration + */ +public class RenderPreview implements IJobChangeListener { + private static final int HEADER_HEIGHT = 20; + static final boolean LARGE_SHADOWS = false; + private static final boolean DUMP_RENDER_DIAGNOSTICS = false; + private static final Image EDIT_ICON; + private static final Image ZOOM_IN_ICON; + private static final Image ZOOM_OUT_ICON; + private static final Image CLOSE_ICON; + private static final int EDIT_ICON_WIDTH; + private static final int ZOOM_IN_ICON_WIDTH; + private static final int ZOOM_OUT_ICON_WIDTH; + private static final int CLOSE_ICON_WIDTH; + static { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + IconFactory icons = IconFactory.getInstance(); + CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE); + EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$ + ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$ + ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$ + CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width; + EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width; + ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width; + ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width; + } + + /** The configuration being previewed */ + private final @NonNull Configuration mConfiguration; + /** The associated manager */ + private final @NonNull RenderPreviewManager mManager; + private final @NonNull LayoutCanvas mCanvas; + private @Nullable ResourceResolver mResourceResolver; + private @Nullable RenderJob mJob; + private @Nullable Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes; + private @Nullable Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes; + private @Nullable Image mThumbnail; + private @Nullable String mDisplayName; + private int mWidth; + private int mHeight; + private int mX; + private int mY; + private double mScale = 1.0; + /** If non null, points to a separate file containing the source */ + private @Nullable IFile mInput; + /** If included within another layout, the name of that outer layout */ + private @Nullable Reference mIncludedWithin; + /** Whether the mouse is actively hovering over this preview */ + private boolean mActive; + /** Whether this preview cannot be rendered because of a model error - such as + * an invalid configuration, a missing resource, an error in the XML markup, etc */ + private boolean mError; + /** + * Whether this preview presents a file that has been "forked" (separate, + * not linked) from the primary layout. + * <p> + * TODO: Decide if this is redundant and I can just use {@link #mInput} != null + * instead. + */ + private boolean mForked; + /** Whether in the current layout, this preview is visible */ + private boolean mVisible; + + /** + * Creates a new {@linkplain RenderPreview} + * + * @param manager the manager + * @param canvas canvas where preview is painted + * @param configuration the associated configuration + * @param width the initial width to use for the preview + * @param height the initial height to use for the preview + */ + private RenderPreview( + @NonNull RenderPreviewManager manager, + @NonNull LayoutCanvas canvas, + @NonNull Configuration configuration, + int width, + int height) { + mManager = manager; + mCanvas = canvas; + mConfiguration = configuration; + mWidth = width; + mHeight = height; + + updateForkStatus(); + } + + /** + * Gets the scale being applied to the thumbnail + * + * @return the scale being applied to the thumbnail + */ + public double getScale() { + return mScale; + } + + /** + * Sets the scale to apply to the thumbnail + * + * @param scale the factor to scale the thumbnail picture by + */ + public void setScale(double scale) { + Image thumbnail = mThumbnail; + mThumbnail = null; + if (thumbnail != null) { + thumbnail.dispose(); + } + mScale = scale; + } + + /** + * Returns whether the preview is actively hovered + * + * @return whether the mouse is hovering over the preview + */ + public boolean isActive() { + return mActive; + } + + /** + * Sets whether the preview is actively hovered + * + * @param active if the mouse is hovering over the preview + */ + public void setActive(boolean active) { + mActive = active; + } + + /** + * Returns whether the preview is visible. Previews that are off + * screen are typically marked invisible during layout, which means we don't + * have to expend effort computing preview thumbnails etc + * + * @return true if the preview is visible + */ + public boolean isVisible() { + return mVisible; + } + + /** + * Sets whether this preview represents a forked layout (e.g. a layout which lives + * in a separate file and is not connected to the main layout) + * + * @param forked true if this preview represents a separate file + */ + public void setForked(boolean forked) { + mForked = forked; + } + + /** + * Returns whether this preview represents a forked layout + * + * @return true if this preview represents a separate file + */ + public boolean isForked() { + return mForked; + } + + /** + * Sets whether the preview is visible. Previews that are off + * screen are typically marked invisible during layout, which means we don't + * have to expend effort computing preview thumbnails etc + * + * @param visible whether this preview is visible + */ + public void setVisible(boolean visible) { + mVisible = visible; + } + + /** + * Sets the layout position relative to the top left corner of the preview + * area, in control coordinates + */ + void setPosition(int x, int y) { + mX = x; + mY = y; + } + + /** + * Gets the layout X position relative to the top left corner of the preview + * area, in control coordinates + */ + int getX() { + return mX; + } + + /** + * Gets the layout Y position relative to the top left corner of the preview + * area, in control coordinates + */ + int getY() { + return mY; + } + + /** Determine whether this configuration has a better match in a different layout file */ + private void updateForkStatus() { + mForked = false; + mInput = null; + ConfigurationChooser chooser = mManager.getChooser(); + IFile editedFile = chooser.getEditedFile(); + if (editedFile != null) { + FolderConfiguration config = mConfiguration.getFullConfig(); + if (!chooser.isBestMatchFor(editedFile, config)) { + ProjectResources resources = chooser.getResources(); + if (resources != null) { + ResourceFile best = resources.getMatchingFile(editedFile.getName(), + ResourceFolderType.LAYOUT, config); + if (best != null) { + IAbstractFile file = best.getFile(); + if (file instanceof IFileWrapper) { + mInput = ((IFileWrapper) file).getIFile(); + } else if (file instanceof File) { + mInput = AdtUtils.fileToIFile(((File) file)); + } + } + } + mForked = true; + } + } + } + + /** + * Creates a new {@linkplain RenderPreview} + * + * @param manager the manager + * @param configuration the associated configuration + * @return a new configuration + */ + @NonNull + public static RenderPreview create( + @NonNull RenderPreviewManager manager, + @NonNull Configuration configuration) { + LayoutCanvas canvas = manager.getCanvas(); + + Image image = canvas.getImageOverlay().getImage(); + + // Image size + int screenWidth = 0; + int screenHeight = 0; + FolderConfiguration myconfig = configuration.getFullConfig(); + ScreenDimensionQualifier dimension = myconfig.getScreenDimensionQualifier(); + if (dimension != null) { + screenWidth = dimension.getValue1(); + screenHeight = dimension.getValue2(); + ScreenOrientationQualifier orientation = myconfig.getScreenOrientationQualifier(); + if (orientation != null) { + ScreenOrientation value = orientation.getValue(); + if (value == ScreenOrientation.PORTRAIT) { + int temp = screenWidth; + screenWidth = screenHeight; + screenHeight = temp; + } + } + } else { + if (image != null) { + screenWidth = image.getImageData().width; + screenHeight = image.getImageData().height; + } + } + int width = RenderPreviewManager.getMaxWidth(); + int height = RenderPreviewManager.getMaxHeight(); + if (screenWidth > 0) { + double scale = getScale(screenWidth, screenHeight); + width = (int) (screenWidth * scale); + height = (int) (screenHeight * scale); + } + + return new RenderPreview(manager, canvas, + configuration, width, height); + } + + /** + * Throws away this preview: cancels any pending rendering jobs and disposes + * of image resources etc + */ + public void dispose() { + if (mThumbnail != null) { + mThumbnail.dispose(); + mThumbnail = null; + } + + if (mJob != null) { + mJob.cancel(); + mJob = null; + } + } + + /** + * Returns the display name of this preview + * + * @return the name of the preview + */ + @NonNull + public String getDisplayName() { + if (mDisplayName == null) { + String displayName = getConfiguration().getDisplayName(); + if (displayName == null) { + // No display name: this must be the configuration used by default + // for the view which is originally displayed (before adding thumbnails), + // and you've switched away to something else; now we need to display a name + // for this original configuration. For now, just call it "Original" + return "Original"; + } + + return displayName; + } + + return mDisplayName; + } + + /** + * Sets the display name of this preview. By default, the display name is + * the display name of the configuration, but it can be overridden by calling + * this setter (which only sets the preview name, without editing the configuration.) + * + * @param displayName the new display name + */ + public void setDisplayName(@NonNull String displayName) { + mDisplayName = displayName; + } + + /** + * Sets an inclusion context to use for this layout, if any. This will render + * the configuration preview as the outer layout with the current layout + * embedded within. + * + * @param includedWithin a reference to a layout which includes this one + */ + public void setIncludedWithin(Reference includedWithin) { + mIncludedWithin = includedWithin; + } + + /** + * Request a new render after the given delay + * + * @param delay the delay to wait before starting the render job + */ + public void render(long delay) { + RenderJob job = mJob; + if (job != null) { + job.cancel(); + } + job = new RenderJob(); + job.schedule(delay); + job.addJobChangeListener(this); + mJob = job; + } + + /** Render immediately */ + private void renderSync() { + if (mThumbnail != null) { + mThumbnail.dispose(); + mThumbnail = null; + } + + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + ResourceResolver resolver = getResourceResolver(); + FolderConfiguration config = mConfiguration.getFullConfig(); + RenderService renderService = RenderService.create(editor, config, resolver); + ScreenSizeQualifier screenSize = config.getScreenSizeQualifier(); + renderService.setScreen(screenSize, mConfiguration.getXDpi(), mConfiguration.getYDpi()); + + if (mIncludedWithin != null) { + renderService.setIncludedWithin(mIncludedWithin); + } + + if (mInput != null) { + IAndroidTarget target = editor.getRenderingTarget(); + AndroidTargetData data = null; + if (target != null) { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + data = sdk.getTargetData(target); + } + } + + // Construct UI model from XML + DocumentDescriptor documentDescriptor; + if (data == null) { + documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$ + } else { + documentDescriptor = data.getLayoutDescriptors().getDescriptor(); + } + UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); + model.setEditor(mCanvas.getEditorDelegate().getEditor()); + model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); + + Document document = DomUtilities.getDocument(mInput); + if (document == null) { + mError = true; + return; + } + model.loadFromXmlNode(document); + renderService.setModel(model); + } else { + renderService.setModel(editor.getModel()); + } + Rect rect = Configuration.getScreenBounds(config); + renderService.setSize(rect.w, rect.h); + RenderLogger log = new RenderLogger(getDisplayName()); + renderService.setLog(log); + RenderSession session = renderService.createRenderSession(); + Result render = session.render(1000); + + if (DUMP_RENDER_DIAGNOSTICS) { + if (log.hasProblems() || !render.isSuccess()) { + Throwable exception = render.getException(); + System.out.println("Found problems rendering preview " + getDisplayName()); + System.out.println(render.getErrorMessage()); + System.out.println(log.getProblems(false)); + if (exception != null) { + exception.printStackTrace(); + } + } + } + + mError = !render.isSuccess(); + + if (render.getStatus() == Status.ERROR_TIMEOUT) { + // TODO: Special handling? schedule update again later + return; + } + if (render.isSuccess()) { + BufferedImage image = session.getImage(); + if (image != null) { + setFullImage(image); + } + } + } + + private ResourceResolver getResourceResolver() { + if (mResourceResolver != null) { + return mResourceResolver; + } + + GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor(); + String theme = mConfiguration.getTheme(); + if (theme == null) { + return null; + } + + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + mResourceResolver = null; + + FolderConfiguration config = mConfiguration.getFullConfig(); + IAndroidTarget target = graphicalEditor.getRenderingTarget(); + ResourceRepository frameworkRes = null; + if (target != null) { + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return null; + } + AndroidTargetData data = sdk.getTargetData(target); + + if (data != null) { + // TODO: SHARE if possible + frameworkRes = data.getFrameworkResources(); + mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(config); + } else { + return null; + } + } else { + return null; + } + assert mConfiguredFrameworkRes != null; + + + // get the resources of the file's project. + ProjectResources projectRes = ResourceManager.getInstance().getProjectResources( + graphicalEditor.getProject()); + mConfiguredProjectRes = projectRes.getConfiguredResources(config); + + if (!theme.startsWith(PREFIX_RESOURCE_REF)) { + if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { + theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; + } else { + theme = STYLE_RESOURCE_PREFIX + theme; + } + } + + mResourceResolver = ResourceResolver.create( + mConfiguredProjectRes, mConfiguredFrameworkRes, + ResourceHelper.styleToTheme(theme), + ResourceHelper.isProjectStyle(theme)); + + return mResourceResolver; + } + + /** + * Sets the new image of the preview and generates a thumbnail + * + * @param image the full size image + */ + void setFullImage(BufferedImage image) { + if (image == null) { + mThumbnail = null; + return; + } + + //double scale = getScale(image); + double scale = getWidth() / (double) image.getWidth(); + if (scale < 1.0) { + if (LARGE_SHADOWS) { + image = ImageUtils.scale(image, scale, scale, + SHADOW_SIZE, SHADOW_SIZE); + ImageUtils.drawRectangleShadow(image, 0, 0, + image.getWidth() - SHADOW_SIZE, + image.getHeight() - SHADOW_SIZE); + } else { + image = ImageUtils.scale(image, scale, scale, + SMALL_SHADOW_SIZE, SMALL_SHADOW_SIZE); + ImageUtils.drawSmallRectangleShadow(image, 0, 0, + image.getWidth() - SMALL_SHADOW_SIZE, + image.getHeight() - SMALL_SHADOW_SIZE); + } + } + + // Adjust size; for different aspect ratios the height might get adjusted etc + /* + if (LARGE_SHADOWS) { + mWidth = image.getWidth() - SMALL_SHADOW_SIZE; + mHeight = image.getHeight() - SMALL_SHADOW_SIZE; + } else { + mWidth = image.getWidth() - SHADOW_SIZE; + mHeight = image.getHeight() - SHADOW_SIZE; + }*/ + + mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, + true /* transferAlpha */, -1); + } + + private static double getScale(int width, int height) { + int maxWidth = RenderPreviewManager.getMaxWidth(); + int maxHeight = RenderPreviewManager.getMaxHeight(); + if (width > 0 && height > 0 + && (width > maxWidth || height > maxHeight)) { + if (width >= height) { // landscape + return maxWidth / (double) width; + } else { // portrait + return maxHeight / (double) height; + } + } + + return 1.0; + } + + /** + * Returns the width of the preview, in pixels + * + * @return the width in pixels + */ + public int getWidth() { + return (int) (mScale * mWidth); + } + + /** + * Returns the height of the preview, in pixels + * + * @return the height in pixels + */ + public int getHeight() { + return (int) (mScale * mHeight); + } + + /** + * Handles clicks within the preview (x and y are positions relative within the + * preview + * + * @param x the x coordinate within the preview where the click occurred + * @param y the y coordinate within the preview where the click occurred + * @return true if this preview handled (and therefore consumed) the click + */ + public boolean click(int x, int y) { + if (y < RenderPreview.HEADER_HEIGHT) { + int left = 0; + left += CLOSE_ICON_WIDTH; + if (x <= left) { + // Delete + mManager.deletePreview(this); + return true; + } + left += ZOOM_IN_ICON_WIDTH; + if (x <= left) { + // Zoom in + mScale = mScale * (1 / 0.5); + if (Math.abs(mScale-1.0) < 0.0001) { + mScale = 1.0; + } + + render(0); + mManager.layout(true); + mCanvas.redraw(); + return true; + } + left += ZOOM_OUT_ICON_WIDTH; + if (x <= left) { + // Zoom out + mScale = mScale * (0.5 / 1); + if (Math.abs(mScale-1.0) < 0.0001) { + mScale = 1.0; + } + render(0); + + mManager.layout(true); + mCanvas.redraw(); + return true; + } + left += EDIT_ICON_WIDTH; + if (x <= left) { + // Edit. For now, just rename + InputDialog d = new InputDialog( + AdtPlugin.getDisplay().getActiveShell(), + "Rename Preview", // title + "Name:", + getDisplayName(), + null); + if (d.open() == Window.OK) { + String newName = d.getValue(); + mConfiguration.setDisplayName(newName); + mCanvas.redraw(); + } + + return true; + } + + // Clicked anywhere else on header + // Perhaps open Edit dialog here? + } + + mManager.switchTo(this); + return true; + } + + /** + * Paints the preview at the given x/y position + * + * @param gc the graphics context to paint it into + * @param x the x coordinate to paint the preview at + * @param y the y coordinate to paint the preview at + */ + void paint(GC gc, int x, int y) { + if (mThumbnail != null) { + gc.drawImage(mThumbnail, x, y); + + if (mActive) { + int oldWidth = gc.getLineWidth(); + gc.setLineWidth(3); + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION)); + gc.drawRectangle(x - 1, y - 1, getWidth() + 2, getHeight() + 2); + gc.setLineWidth(oldWidth); + } + } else if (mError) { + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); + gc.drawRectangle(x, y, getWidth(), getHeight()); + Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$ + ImageData data = icon.getImageData(); + int prevAlpha = gc.getAlpha(); + gc.setAlpha(128-32); + gc.drawImage(icon, x + (getWidth() - data.width) / 2, + y + (getHeight() - data.height) / 2); + gc.setAlpha(prevAlpha); + } else { + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); + gc.drawRectangle(x, y, getWidth(), getHeight()); + Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$ + ImageData data = icon.getImageData(); + int prevAlpha = gc.getAlpha(); + gc.setAlpha(128-32); + gc.drawImage(icon, x + (getWidth() - data.width) / 2, + y + (getHeight() - data.height) / 2); + gc.setAlpha(prevAlpha); + } + + if (mActive) { + int left = x ; + int prevAlpha = gc.getAlpha(); + gc.setAlpha(128+32); + Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE); + gc.setBackground(bg); + gc.fillRectangle(left, y, x + getWidth() - left, + RenderPreview.HEADER_HEIGHT); + gc.setAlpha(prevAlpha); + + // Paint icons + gc.drawImage(CLOSE_ICON, left, y); + left += CLOSE_ICON_WIDTH; + + gc.drawImage(ZOOM_IN_ICON, left, y); + left += ZOOM_IN_ICON_WIDTH; + + gc.drawImage(ZOOM_OUT_ICON, left, y); + left += ZOOM_OUT_ICON_WIDTH; + + gc.drawImage(EDIT_ICON, left, y); + left += EDIT_ICON_WIDTH; + } + + paintTitle(gc, x, y); + } + + /** + * Paints the preview title at the given position + * + * @param gc the graphics context to paint into + * @param x the left edge of the preview rectangle + * @param y the top edge of the preview rectangle + */ + void paintTitle(GC gc, int x, int y) { + String displayName = getDisplayName(); + if (displayName != null && displayName.length() > 0) { + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE)); + + int width = getWidth(); + int height = getHeight(); + Point extent = gc.textExtent(displayName); + int labelLeft = Math.max(x, x + (width - extent.x) / 2); + int labelTop = y + height + 1; + Image flagImage = null; + Locale locale = mConfiguration.getLocale(); + if (locale != null && (locale.hasLanguage() || locale.hasRegion()) + && (!(mConfiguration instanceof NestedConfiguration) + || ((NestedConfiguration) mConfiguration).isOverridingLocale())) { + flagImage = locale.getFlagImage(); + } + + gc.setClipping(x, y, width, height + 100); + if (flagImage != null) { + int flagWidth = flagImage.getImageData().width; + int flagHeight = flagImage.getImageData().height; + gc.drawImage(flagImage, labelLeft - flagWidth / 2 - 1, labelTop); + labelLeft += flagWidth / 2 + 1; + gc.drawText(displayName, labelLeft, + labelTop - (extent.y - flagHeight) / 2, true); + } else { + gc.drawText(displayName, labelLeft, labelTop, true); + } + + if (mForked && mInput != null) { + // Draw file flag, and parent folder name + labelTop += extent.y; + String fileName = mInput.getParent().getName() + File.separator + mInput.getName(); + extent = gc.textExtent(fileName); + flagImage = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$ + int flagWidth = flagImage.getImageData().width; + int flagHeight = flagImage.getImageData().height; + + labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2); + + gc.drawImage(flagImage, labelLeft, labelTop); + + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); + labelLeft += flagWidth + 1; + labelTop -= (extent.y - flagHeight) / 2; + gc.drawText(fileName, labelLeft, labelTop, true); + } + + gc.setClipping((Region) null); + } + } + + /** + * Notifies that the preview's configuration has changed. + * + * @param flags the change flags, a bitmask corresponding to the + * {@code CHANGE_} constants in {@link ConfigurationClient} + */ + public void configurationChanged(int flags) { + if ((flags & (CHANGED_FOLDER | CHANGED_THEME | CHANGED_DEVICE + | CHANGED_RENDER_TARGET | CHANGED_LOCALE)) != 0) { + mResourceResolver = null; + // Handle inheritance + mConfiguration.syncFolderConfig(); + updateForkStatus(); + } + + FolderConfiguration folderConfig = mConfiguration.getFullConfig(); + ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier(); + ScreenOrientation orientation = qualifier == null + ? ScreenOrientation.PORTRAIT : qualifier.getValue(); + if (orientation == ScreenOrientation.LANDSCAPE + || orientation == ScreenOrientation.SQUARE) { + orientation = ScreenOrientation.PORTRAIT; + } else { + orientation = ScreenOrientation.LANDSCAPE; + } + + if ((mWidth < mHeight && orientation == ScreenOrientation.PORTRAIT) + || (mWidth > mHeight && orientation == ScreenOrientation.LANDSCAPE)) { + Image thumbnail = mThumbnail; + mThumbnail = null; + if (thumbnail != null) { + thumbnail.dispose(); + } + + // Flip icon size + int temp = mHeight; + mHeight = mWidth; + mWidth = temp; + } + } + + /** + * Returns the configuration associated with this preview + * + * @return the configuration + */ + @NonNull + public Configuration getConfiguration() { + return mConfiguration; + } + + // ---- Implements IJobChangeListener ---- + + @Override + public void aboutToRun(IJobChangeEvent event) { + } + + @Override + public void awake(IJobChangeEvent event) { + } + + @Override + public void done(IJobChangeEvent event) { + mJob = null; + } + + @Override + public void running(IJobChangeEvent event) { + } + + @Override + public void scheduled(IJobChangeEvent event) { + } + + @Override + public void sleeping(IJobChangeEvent event) { + } + + // ---- Delayed Rendering ---- + + private final class RenderJob extends UIJob { + public RenderJob() { + super("RenderPreview"); + setSystem(true); + setUser(false); + } + + /* TODO: Make this job work in the background. Need to make the render service + * not read UI thread properties out of the configuration composite. + * SEPTEMBER 2012: The config composite work should be done now, check. + @Override + protected IStatus run(IProgressMonitor monitor) { + if (mCanvas.isDisposed()) { + return org.eclipse.core.runtime.Status.CANCEL_STATUS; + } + + renderSync(); + + // Update display + mCanvas.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + mCanvas.redraw(); + } + }); + return org.eclipse.core.runtime.Status.OK_STATUS; + } + */ + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + mJob = null; + if (!mCanvas.isDisposed()) { + renderSync(); + mCanvas.redraw(); + return org.eclipse.core.runtime.Status.OK_STATUS; + } + + return org.eclipse.core.runtime.Status.CANCEL_STATUS; + } + + @Override + public Display getDisplay() { + if (mCanvas.isDisposed()) { + return null; + } + return mCanvas.getDisplay(); + } + } + + /** + * Sets the input file to use for rendering. If not set, this will just be + * the same file as the configuration chooser. This is used to render other + * layouts, such as variations of the currently edited layout, which are + * not kept in sync with the main layout. + * + * @param file the file to set as input + */ + public void setInput(@Nullable IFile file) { + mInput = file; + } + + /** Corresponding description for this preview if it is a manually added preview */ + private @Nullable ConfigurationDescription mDescription; + + /** + * Sets the description of this preview, if this preview is a manually added preview + * + * @param description the description of this preview + */ + public void setDescription(@Nullable ConfigurationDescription description) { + mDescription = description; + } + + /** + * Returns the description of this preview, if this preview is a manually added preview + * + * @return the description + */ + @Nullable + public ConfigurationDescription getDescription() { + return mDescription; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java new file mode 100644 index 0000000..1d48f7b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java @@ -0,0 +1,203 @@ +/* + * 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.gle2; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.sdklib.devices.Device; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** A list of render previews */ +class RenderPreviewList { + /** Name of file saved in project directory storing previews */ + private static final String PREVIEW_FILE_NAME = "previews.xml"; //$NON-NLS-1$ + + /** Qualified name for the per-project persistent property include-map */ + private final static QualifiedName PREVIEW_LIST = new QualifiedName(AdtPlugin.PLUGIN_ID, + "previewlist");//$NON-NLS-1$ + + private final IProject mProject; + private final List<ConfigurationDescription> mList = Lists.newArrayList(); + + private RenderPreviewList(@NonNull IProject project) { + mProject = project; + } + + /** + * Returns the {@link RenderPreviewList} for the given project + * + * @param project the project the list is associated with + * @return a {@link RenderPreviewList} for the given project, never null + */ + @NonNull + public static RenderPreviewList get(@NonNull IProject project) { + RenderPreviewList list = null; + try { + list = (RenderPreviewList) project.getSessionProperty(PREVIEW_LIST); + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + + if (list == null) { + list = new RenderPreviewList(project); + try { + project.setSessionProperty(PREVIEW_LIST, list); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + return list; + } + + private File getManualFile() { + return new File(AdtUtils.getAbsolutePath(mProject).toFile(), PREVIEW_FILE_NAME); + } + + void load(List<Device> deviceList) throws IOException { + File file = getManualFile(); + if (file.exists()) { + load(file, deviceList); + } + } + + void save() throws IOException { + delete(); + if (!mList.isEmpty()) { + File file = getManualFile(); + save(file); + } + } + + private void save(File file) throws IOException { + //Document document = DomUtilities.createEmptyPlainDocument(); + Document document = DomUtilities.createEmptyDocument(); + if (document != null) { + for (ConfigurationDescription description : mList) { + description.toXml(document); + } + String xml = XmlPrettyPrinter.prettyPrint(document); + Files.write(xml, file, Charsets.UTF_8); + } + } + + void load(File file, List<Device> deviceList) throws IOException { + mList.clear(); + + String xml = Files.toString(file, Charsets.UTF_8); + Document document = DomUtilities.parseDocument(xml, true); + if (document == null || document.getDocumentElement() == null) { + return; + } + List<Element> elements = DomUtilities.getChildren(document.getDocumentElement()); + for (Element element : elements) { + ConfigurationDescription description = ConfigurationDescription.fromXml( + mProject, element, deviceList); + if (description != null) { + mList.add(description); + } + } + } + + /** + * Create a list of previews for the given canvas that matches the internal + * configuration preview list + * + * @param canvas the associated canvas + * @return a new list of previews linked to the given canvas + */ + @NonNull + List<RenderPreview> createPreviews(LayoutCanvas canvas) { + if (mList.isEmpty()) { + return new ArrayList<RenderPreview>(); + } + List<RenderPreview> previews = Lists.newArrayList(); + RenderPreviewManager manager = canvas.getPreviewManager(); + ConfigurationChooser chooser = canvas.getEditorDelegate().getGraphicalEditor() + .getConfigurationChooser(); + + for (ConfigurationDescription description : mList) { + Configuration configuration = Configuration.create(chooser); + configuration.getFullConfig().set(description.folder); + if (description.target != null) { + // TODO: Make sure this layout isn't in some v-folder which is incompatible + // with this target! + configuration.setTarget(description.target, true); + } + + if (description.theme != null) { + configuration.setTheme(description.theme); + } + + RenderPreview preview = RenderPreview.create(manager, configuration); + if (description.displayName != null) { + preview.setDisplayName(description.displayName); + } + + preview.setDescription(description); + previews.add(preview); + } + + return previews; + } + + void remove(@NonNull RenderPreview preview) { + ConfigurationDescription description = preview.getDescription(); + if (description != null) { + mList.remove(description); + } + } + + boolean isEmpty() { + return mList.isEmpty(); + } + + void add(@NonNull RenderPreview preview) { + Configuration configuration = preview.getConfiguration(); + ConfigurationDescription description = + ConfigurationDescription.fromConfiguration(mProject, configuration); + // RenderPreviews can have display names that aren't reflected in the configuration + description.displayName = preview.getDisplayName(); + mList.add(description); + preview.setDescription(description); + } + + void delete() { + mList.clear(); + File file = getManualFile(); + if (file.exists()) { + file.delete(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java new file mode 100644 index 0000000..9fc0681 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java @@ -0,0 +1,1161 @@ +/* + * 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.gle2; + +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreview.LARGE_SHADOWS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +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.LanguageQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ComplementingConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.resources.Density; +import com.android.resources.ScreenSize; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.State; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.ScrollBar; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Manager for the configuration previews, which handles layout computations, + * managing the image buffer cache, etc + */ +public class RenderPreviewManager { + private static double sScale = 1.0; + private static final int RENDER_DELAY = 100; + private static final int PREVIEW_VGAP = 18; + private static final int PREVIEW_HGAP = 12; + private static final int MAX_WIDTH = 200; + private static final int MAX_HEIGHT = MAX_WIDTH; + private @Nullable List<RenderPreview> mPreviews; + private @Nullable RenderPreviewList mManualList; + private final @NonNull LayoutCanvas mCanvas; + private final @NonNull CanvasTransform mVScale; + private final @NonNull CanvasTransform mHScale; + private int mPrevCanvasWidth; + private int mPrevCanvasHeight; + private int mPrevImageWidth; + private int mPrevImageHeight; + private @NonNull RenderPreviewMode mMode = RenderPreviewMode.NONE; + private @Nullable RenderPreview mActivePreview; + private @Nullable ScrollBarListener mListener; + private int mLayoutHeight; + private int mMaxVisibleY; + + /** + * Creates a {@link RenderPreviewManager} associated with the given canvas + * + * @param canvas the canvas to manage previews for + */ + public RenderPreviewManager(@NonNull LayoutCanvas canvas) { + mCanvas = canvas; + mHScale = canvas.getHorizontalTransform(); + mVScale = canvas.getVerticalTransform(); + } + + /** + * Returns the associated chooser + * + * @return the associated chooser + */ + @NonNull + ConfigurationChooser getChooser() { + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + return editor.getConfigurationChooser(); + } + + /** + * Returns the associated canvas + * + * @return the canvas + */ + @NonNull + public LayoutCanvas getCanvas() { + return mCanvas; + } + + /** Zooms in (grows all previews) */ + public void zoomIn() { + sScale = sScale * (1 / 0.9); + if (Math.abs(sScale-1.0) < 0.0001) { + sScale = 1.0; + } + + updatedZoom(); + } + + /** Zooms out (shrinks all previews) */ + public void zoomOut() { + sScale = sScale * (0.9 / 1); + if (Math.abs(sScale-1.0) < 0.0001) { + sScale = 1.0; + } + updatedZoom(); + } + + private void updatedZoom() { + if (hasPreviews()) { + for (RenderPreview preview : mPreviews) { + preview.setScale(sScale); + } + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + preview.setScale(sScale); + } + } + + renderPreviews(); + layout(true); + mCanvas.redraw(); + } + + static int getMaxWidth() { + return (int) sScale * MAX_WIDTH; + } + + static int getMaxHeight() { + return (int) sScale * MAX_HEIGHT; + } + + /** Delete all the previews */ + public void deleteManualPreviews() { + disposePreviews(); + selectMode(RenderPreviewMode.NONE); + mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/); + + if (mManualList != null) { + mManualList.delete(); + } + } + + /** Dispose all the previews */ + public void disposePreviews() { + if (mPreviews != null) { + List<RenderPreview> old = mPreviews; + mPreviews = null; + for (RenderPreview preview : old) { + preview.dispose(); + } + } + } + + /** + * Deletes the given preview + * + * @param preview the preview to be deleted + */ + public void deletePreview(RenderPreview preview) { + mPreviews.remove(preview); + preview.dispose(); + layout(true); + mCanvas.redraw(); + + if (mManualList != null) { + mManualList.remove(preview); + saveList(); + } + } + + /** + * Compute the total width required for the previews, including internal padding + * + * @return total width in pixels + */ + public int computePreviewWidth() { + int maxPreviewWidth = 0; + if (hasPreviews()) { + for (RenderPreview preview : mPreviews) { + maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth()); + } + + if (maxPreviewWidth > 0) { + maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side + maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; + } + + return maxPreviewWidth; + } + + return 0; + } + + /** + * Layout Algorithm. This sets the {@link RenderPreview#getX()} and + * {@link RenderPreview#getY()} coordinates of all the previews. It also marks + * previews as visible or invisible via {@link RenderPreview#setVisible(boolean)} + * according to their position and the current visible view port in the layout canvas. + * Finally, it also sets the {@code mMaxVisibleY} and {@code mLayoutHeight} fields, + * such that the scrollbars can compute the right scrolled area, and that scrolling + * can cause render refreshes on views that are made visible. + * + * <p> + * Two shapes to fill. The screen is typically wide. When showing a phone, + * I should use all the vertical space; I then show thumbnails on the right. + * When showing the tablet, I need to do something in between. I reserve at least + * 200 pixels either on the right or on the bottom and use the remainder. + * TODO: Look up better algorithms. Optimal space division algorithm. Can prune etc. + * <p> + * This is not a traditional bin packing problem, because the objects to be packaged + * do not have a fixed size; we can scale them up and down in order to provide an + * "optimal" size. + * <p> + * See http://en.wikipedia.org/wiki/Packing_problem + * See http://en.wikipedia.org/wiki/Bin_packing_problem + * <p> + * Returns true if the layout changed (so a redraw is desired) + */ + boolean layout(boolean refresh) { + if (mPreviews == null || mPreviews.isEmpty()) { + return false; + } + + if (mListener == null) { + mListener = new ScrollBarListener(); + mCanvas.getVerticalBar().addSelectionListener(mListener); + } + + // TODO: Separate layout heuristics for portrait and landscape orientations (though + // it also depends on the dimensions of the canvas window, which determines the + // shape of the leftover space) + + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + + if (!refresh && + (scaledImageWidth == mPrevImageWidth + && scaledImageHeight == mPrevImageHeight + && clientArea.width == mPrevCanvasWidth + && clientArea.height == mPrevCanvasHeight)) { + // No change + return false; + } + + mPrevImageWidth = scaledImageWidth; + mPrevImageHeight = scaledImageHeight; + mPrevCanvasWidth = clientArea.width; + mPrevCanvasHeight = clientArea.height; + + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int maxVisibleY = clientArea.y + clientArea.height; + + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + int nextY = 0; + + // First lay out images across the top right hand side + int x = rightHandSide; + int y = 0; + boolean wrapped = false; + + int vgap = PREVIEW_VGAP; + for (RenderPreview preview : mPreviews) { + // If we have forked previews, double the vgap to allow space for two labels + if (preview.isForked()) { + vgap *= 2; + break; + } + } + + for (RenderPreview preview : mPreviews) { + if (x > 0 && x + preview.getWidth() > availableWidth) { + x = rightHandSide; + int prevY = y; + y = nextY; + if ((prevY <= bottomBorder || + y <= bottomBorder) + && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { + // If there's really no visible room below, don't bother + // Similarly, don't wrap individually scaled views + if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) { + // If it's closer to the top row than the bottom, just + // mark the next row for left justify instead + if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { + rightHandSide = 0; + wrapped = true; + } else if (!wrapped) { + y = nextY = Math.max(nextY, bottomBorder + vgap); + x = rightHandSide = 0; + wrapped = true; + } + } + } + } + if (x > 0 && y <= bottomBorder + && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { + if (clientArea.height - bottomBorder < preview.getHeight()) { + // No room below the device on the left; just continue on the + // bottom row + } else if (preview.getScale() < 1.2) { + if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { + rightHandSide = 0; + wrapped = true; + } else { + y = nextY = Math.max(nextY, bottomBorder + vgap); + x = rightHandSide = 0; + wrapped = true; + } + } + } + + preview.setPosition(x, y); + + if (y > maxVisibleY) { + preview.setVisible(false); + } else if (!preview.isVisible()) { + preview.render(RENDER_DELAY); + preview.setVisible(true); + } + + x += preview.getWidth(); + x += PREVIEW_HGAP; + nextY = Math.max(nextY, y + preview.getHeight() + vgap); + } + + mLayoutHeight = nextY; + mMaxVisibleY = maxVisibleY; + mCanvas.updateScrollBars(); + + return true; + } + + /** + * Paints the configuration previews + * + * @param gc the graphics context to paint into + */ + void paint(GC gc) { + if (hasPreviews()) { + // Ensure up to date at all times; consider moving if it's too expensive + layout(false); + int rootX = getX(); + int rootY = getY(); + + for (RenderPreview preview : mPreviews) { + if (preview.isVisible()) { + int x = rootX + preview.getX(); + int y = rootY + preview.getY(); + preview.paint(gc, x, y); + } + } + + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + CanvasTransform hi = mHScale; + CanvasTransform vi = mVScale; + + int destX = hi.translate(0); + int destY = vi.translate(0); + int destWidth = hi.getScaledImgSize(); + int destHeight = vi.getScaledImgSize(); + + int x = destX + destWidth / 2 - preview.getWidth() / 2; + int y = destY + destHeight - preview.getHeight(); + preview.paintTitle(gc, x, y); + } + } else if (mMode == RenderPreviewMode.CUSTOM) { + int rootX = getX(); + rootX += mHScale.getScaledImgSize(); + rootX += 2 * PREVIEW_HGAP; + int rootY = getY(); + rootY += 20; + gc.setFont(mCanvas.getFont()); + gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK)); + gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu", + rootX, rootY, true); + } + } + + private void addPreview(@NonNull RenderPreview preview) { + if (mPreviews == null) { + mPreviews = Lists.newArrayList(); + } + mPreviews.add(preview); + } + + /** Adds the current configuration as a new configuration preview */ + public void addAsThumbnail() { + ConfigurationChooser chooser = getChooser(); + String name = chooser.getConfiguration().getDisplayName(); + if (name == null || name.isEmpty()) { + name = getUniqueName(); + } + InputDialog d = new InputDialog( + AdtPlugin.getDisplay().getActiveShell(), + "Add as Thumbnail Preview", // title + "Name of thumbnail:", + name, + null); + if (d.open() == Window.OK) { + selectMode(RenderPreviewMode.CUSTOM); + + String newName = d.getValue(); + // Create a new configuration from the current settings in the composite + Configuration configuration = Configuration.copy(chooser.getConfiguration()); + configuration.setDisplayName(newName); + + RenderPreview preview = RenderPreview.create(this, configuration); + addPreview(preview); + + layout(true); + preview.render(RENDER_DELAY); + mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/); + + if (mManualList == null) { + loadList(); + } + if (mManualList != null) { + mManualList.add(preview); + saveList(); + } + } + } + + /** + * Computes a unique new name for a configuration preview that represents + * the current, default configuration + * + * @return a unique name + */ + private String getUniqueName() { + if (mPreviews == null || mPreviews.isEmpty()) { + // NO, not for the first preview! + return "Config1"; + } + + Set<String> names = new HashSet<String>(mPreviews.size()); + for (RenderPreview preview : mPreviews) { + names.add(preview.getDisplayName()); + } + + int index = 2; + while (true) { + String name = String.format("Config%1$d", index); + if (!names.contains(name)) { + return name; + } + index++; + } + } + + /** Generates a bunch of default configuration preview thumbnails */ + public void addDefaultPreviews() { + ConfigurationChooser chooser = getChooser(); + Configuration parent = chooser.getConfiguration(); + if (parent instanceof NestedConfiguration) { + parent = ((NestedConfiguration) parent).getParent(); + } + if (mCanvas.getImageOverlay().getImage() != null) { + // Create Language variation + createLocaleVariation(chooser, parent); + + // Vary screen size + // TODO: Be smarter here: Pick a screen that is both as differently as possible + // from the current screen as well as also supported. So consider + // things like supported screens, targetSdk etc. + createScreenVariations(parent); + + // Vary orientation + createStateVariation(chooser, parent); + + // Vary render target + createRenderTargetVariation(chooser, parent); + } + + // Make a placeholder preview for the current screen, in case we switch from it + RenderPreview preview = RenderPreview.create(this, parent); + mCanvas.setPreview(preview); + + sortPreviewsByOrientation(); + } + + private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) { + /* This is disabled for now: need to load multiple versions of layoutlib. + When I did this, there seemed to be some drug interactions between + them, and I would end up with NPEs in layoutlib code which normally works. + ComplementingConfiguration configuration = + ComplementingConfiguration.create(chooser, parent); + configuration.setOverrideTarget(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + */ + } + + private void createStateVariation(ConfigurationChooser chooser, Configuration parent) { + State currentState = parent.getDeviceState(); + State nextState = parent.getNextDeviceState(currentState); + if (nextState != currentState) { + ComplementingConfiguration configuration = + ComplementingConfiguration.create(chooser, parent); + configuration.setOverrideDeviceState(true); + configuration.setDeviceState(nextState, false); + addPreview(RenderPreview.create(this, configuration)); + } + } + + private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) { + LanguageQualifier currentLanguage = parent.getLocale().language; + for (Locale locale : chooser.getLocaleList()) { + LanguageQualifier language = locale.language; + if (!language.equals(currentLanguage)) { + ComplementingConfiguration configuration = + ComplementingConfiguration.create(chooser, parent); + configuration.setOverrideLocale(true); + Locale otherLanguage = Locale.create(language); + configuration.setLocale(otherLanguage, false); + addPreview(RenderPreview.create(this, configuration)); + break; + } + } + } + + private void createScreenVariations(Configuration parent) { + ConfigurationChooser chooser = getChooser(); + ComplementingConfiguration configuration; + + configuration = ComplementingConfiguration.create(chooser, parent); + configuration.setVariation(0); + configuration.setOverrideDevice(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + + configuration = ComplementingConfiguration.create(chooser, parent); + configuration.setVariation(1); + configuration.setOverrideDevice(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + } + + /** + * Returns the current mode as seen by this {@link RenderPreviewManager}. + * Note that it may not yet have been synced with the global mode kept in + * {@link AdtPrefs#getRenderPreviewMode()}. + * + * @return the current preview mode + */ + @NonNull + public RenderPreviewMode getMode() { + return mMode; + } + + /** + * Update the set of previews for the current mode + * + * @param force force a refresh even if the preview type has not changed + * @return true if the views were recomputed, false if the previews were + * already showing and the mode not changed + */ + public boolean recomputePreviews(boolean force) { + RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode(); + if (newMode == mMode) { + if (!force || mMode == RenderPreviewMode.CUSTOM) { + return false; + } + } + + mMode = newMode; + + sScale = 1.0; + disposePreviews(); + + switch (mMode) { + case DEFAULT: + addDefaultPreviews(); + break; + case INCLUDES: + addIncludedInPreviews(); + break; + case LOCALES: + addLocalePreviews(); + break; + case SCREENS: + addScreenSizePreviews(); + break; + case VARIATIONS: + addVariationPreviews(); + break; + case CUSTOM: + addManualPreviews(); + break; + case NONE: + break; + default: + assert false : mMode; + } + + layout(true); + renderPreviews(); + boolean allowZoomIn = mMode == RenderPreviewMode.NONE; + mCanvas.setFitScale(true /*onlyZoomOut*/, allowZoomIn); + mCanvas.updateScrollBars(); + + return true; + } + + /** + * Sets the new render preview mode to use + * + * @param mode the new mode + */ + public void selectMode(@NonNull RenderPreviewMode mode) { + if (mode != mMode) { + AdtPrefs.getPrefs().setPreviewMode(mode); + recomputePreviews(false); + } + } + + /** Similar to {@link #addDefaultPreviews()} but for locales */ + public void addLocalePreviews() { + + ConfigurationChooser chooser = getChooser(); + List<Locale> locales = chooser.getLocaleList(); + Configuration parent = chooser.getConfiguration(); + + for (Locale locale : locales) { + if (!locale.hasLanguage() && !locale.hasRegion()) { + continue; + } + NestedConfiguration configuration = NestedConfiguration.create(chooser, parent); + configuration.setOverrideLocale(true); + configuration.setLocale(locale, false); + + String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, true); + assert displayName != null; // it's never non null when locale is non null + configuration.setDisplayName(displayName); + + addPreview(RenderPreview.create(this, configuration)); + } + + // Make a placeholder preview for the current screen, in case we switch from it + Configuration configuration = parent; + Locale locale = configuration.getLocale(); + String label = ConfigurationChooser.getLocaleLabel(chooser, locale, true); + if (label == null) { + label = "default"; + } + configuration.setDisplayName(label); + RenderPreview preview = RenderPreview.create(this, parent); + if (preview != null) { + mCanvas.setPreview(preview); + } + + // No need to sort: they should all be identical + } + + /** Similar to {@link #addDefaultPreviews()} but for screen sizes */ + public void addScreenSizePreviews() { + ConfigurationChooser chooser = getChooser(); + List<Device> devices = chooser.getDeviceList(); + Configuration configuration = chooser.getConfiguration(); + + // Rearrange the devices a bit such that the most interesting devices bubble + // to the front + // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first + // version of each seen screen size + List<Device> sorted = new ArrayList<Device>(devices); + Set<ScreenSize> seenSizes = new HashSet<ScreenSize>(); + State currentState = configuration.getDeviceState(); + String currentStateName = currentState != null ? currentState.getName() : ""; + + for (int i = 0, n = sorted.size(); i < n; i++) { + Device device = sorted.get(i); + boolean interesting = false; + + State state = device.getState(currentStateName); + if (state == null) { + state = device.getAllStates().get(0); + } + + if (device.getName().startsWith("Nexus ") //$NON-NLS-1$ + || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$ + // Not String#contains("Nexus") because that would also pick up all the generic + // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated + interesting = true; + } + + FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state); + if (c != null) { + ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier(); + if (sizeQualifier != null) { + ScreenSize size = sizeQualifier.getValue(); + if (!seenSizes.contains(size)) { + seenSizes.add(size); + interesting = true; + } + } + + // Omit LDPI, not really used anymore + DensityQualifier density = c.getDensityQualifier(); + if (density != null) { + Density d = density.getValue(); + if (Density.LOW.equals(d)) { + interesting = false; + } + } + } + + if (interesting) { + NestedConfiguration screenConfig = NestedConfiguration.create(chooser, + configuration); + screenConfig.setOverrideDevice(true); + screenConfig.setDevice(device, true); + screenConfig.syncFolderConfig(); + screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true)); + addPreview(RenderPreview.create(this, screenConfig)); + } + } + + // Sorted by screen size, in decreasing order + sortPreviewsByScreenSize(); + } + + /** + * Previews this layout as included in other layouts + */ + public void addIncludedInPreviews() { + ConfigurationChooser chooser = getChooser(); + IProject project = chooser.getProject(); + if (project == null) { + return; + } + IncludeFinder finder = IncludeFinder.get(project); + + final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile()); + + if (includedBy == null || includedBy.isEmpty()) { + // TODO: Generate some useful defaults, such as including it in a ListView + // as the list item layout? + return; + } + + for (final Reference reference : includedBy) { + String title = reference.getDisplayName(); + Configuration config = Configuration.create(chooser, reference.getFile()); + RenderPreview preview = RenderPreview.create(this, config); + preview.setDisplayName(title); + preview.setIncludedWithin(reference); + + addPreview(preview); + } + + sortPreviewsByOrientation(); + } + + /** + * Previews this layout as included in other layouts + */ + public void addVariationPreviews() { + ConfigurationChooser chooser = getChooser(); + + IFile file = chooser.getEditedFile(); + List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/); + + // Sort by parent folder + Collections.sort(variations, new Comparator<IFile>() { + @Override + public int compare(IFile file1, IFile file2) { + return file1.getParent().getName().compareTo(file2.getParent().getName()); + } + }); + + for (IFile variation : variations) { + String title = variation.getParent().getName(); + Configuration config = Configuration.create(chooser, variation); + RenderPreview preview = RenderPreview.create(this, config); + preview.setDisplayName(title); + preview.setInput(variation); + + addPreview(preview); + } + + sortPreviewsByOrientation(); + } + + /** + * Previews this layout using a custom configured set of layouts + */ + public void addManualPreviews() { + if (mManualList == null) { + loadList(); + } else { + mPreviews = mManualList.createPreviews(mCanvas); + } + } + + private void loadList() { + IProject project = getChooser().getProject(); + if (project == null) { + return; + } + + if (mManualList == null) { + mManualList = RenderPreviewList.get(project); + } + + try { + mManualList.load(getChooser().getDeviceList()); + mPreviews = mManualList.createPreviews(mCanvas); + } catch (IOException e) { + AdtPlugin.log(e, null); + } + } + + private void saveList() { + if (mManualList != null) { + try { + mManualList.save(); + } catch (IOException e) { + AdtPlugin.log(e, null); + } + } + } + + /** + * Notifies that the main configuration has changed. + * + * @param flags the change flags, a bitmask corresponding to the + * {@code CHANGE_} constants in {@link ConfigurationClient} + */ + public void configurationChanged(int flags) { + // Similar to renderPreviews, but only acts on incomplete previews + if (hasPreviews()) { + long delay = 0; + // Do zoomed images first + for (RenderPreview preview : mPreviews) { + if (preview.getScale() > 1.2) { + preview.configurationChanged(flags); + delay += RENDER_DELAY; + preview.render(delay); + } + } + for (RenderPreview preview : mPreviews) { + if (preview.getScale() <= 1.2) { + preview.configurationChanged(flags); + delay += RENDER_DELAY; + preview.render(delay); + } + } + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + preview.configurationChanged(flags); + preview.dispose(); + } + layout(true); + mCanvas.redraw(); + } + } + + /** Updates the configuration preview thumbnails */ + public void renderPreviews() { + if (hasPreviews()) { + long delay = 0; + // Do zoomed images first + for (RenderPreview preview : mPreviews) { + if (preview.getScale() > 1.2 && preview.isVisible()) { + delay += RENDER_DELAY; + preview.render(delay); + } + } + // Non-zoomed images + for (RenderPreview preview : mPreviews) { + if (preview.getScale() <= 1.2 && preview.isVisible()) { + delay += RENDER_DELAY; + preview.render(delay); + } + } + } + } + + /** + * Switch to the given configuration preview + * + * @param preview the preview to switch to + */ + public void switchTo(@NonNull RenderPreview preview) { + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + ConfigurationChooser chooser = editor.getConfigurationChooser(); + + RenderPreview newPreview = mCanvas.getPreview(); + if (newPreview == null) { + newPreview = RenderPreview.create(this, chooser.getConfiguration()); + } + + // Replace clicked preview with preview of the formerly edited main configuration + if (newPreview != null) { + // This doesn't work yet because the image overlay has had its image + // replaced by the configuration previews! I should make a list of them + //newPreview.setFullImage(mImageOverlay.getAwtImage()); + + for (int i = 0, n = mPreviews.size(); i < n; i++) { + if (preview == mPreviews.get(i)) { + mPreviews.set(i, newPreview); + break; + } + } + //newPreview.setPosition(preview.getX(), preview.getY()); + } + + // Switch main editor to the clicked configuration preview + mCanvas.setPreview(preview); + chooser.setConfiguration(preview.getConfiguration()); + editor.recomputeLayout(); + mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum()); + mCanvas.setFitScale(true /*onlyZoomOut*/, false /*allowZoomIn*/); + layout(true); + mCanvas.redraw(); + } + + /** + * Gets the preview at the given location, or null if none. This is + * currently deeply tied to where things are painted in onPaint(). + */ + RenderPreview getPreview(ControlPoint mousePos) { + if (hasPreviews()) { + int rootX = getX(); + if (mousePos.x < rootX) { + return null; + } + int rootY = getY(); + + for (RenderPreview preview : mPreviews) { + int x = rootX + preview.getX(); + int y = rootY + preview.getY(); + if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) { + if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) { + return preview; + } + } + } + } + + return null; + } + + private int getX() { + return mHScale.translate(0); + } + + private int getY() { + return mVScale.translate(0); + } + + /** + * Returns the height of the layout + * + * @return the height + */ + public int getHeight() { + return mLayoutHeight; + } + + /** + * Notifies that preview manager that the mouse cursor has moved to the + * given control position within the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void moved(ControlPoint mousePos) { + RenderPreview hovered = getPreview(mousePos); + if (hovered != mActivePreview) { + if (mActivePreview != null) { + mActivePreview.setActive(false); + } + mActivePreview = hovered; + if (mActivePreview != null) { + mActivePreview.setActive(true); + } + mCanvas.redraw(); + } + } + + /** + * Notifies that preview manager that the mouse cursor has entered the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void enter(ControlPoint mousePos) { + moved(mousePos); + } + + /** + * Notifies that preview manager that the mouse cursor has exited the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void exit(ControlPoint mousePos) { + if (mActivePreview != null) { + mActivePreview.setActive(false); + } + mActivePreview = null; + mCanvas.redraw(); + } + + /** + * Process a mouse click, and return true if it was handled by this manager + * (e.g. the click was on a preview) + * + * @param mousePos the mouse position where the click occurred + * @return true if the click occurred over a preview and was handled, false otherwise + */ + public boolean click(ControlPoint mousePos) { + RenderPreview preview = getPreview(mousePos); + if (preview != null) { + boolean handled = preview.click(mousePos.x - getX() - preview.getX(), + mousePos.y - getY() - preview.getY()); + if (handled) { + // In case layout was performed, there could be a new preview + // under this coordinate now, so make sure it's hover etc + // shows up + moved(mousePos); + return true; + } + } + + return false; + } + + /** + * Returns true if there are thumbnail previews + * + * @return true if thumbnails are being shown + */ + public boolean hasPreviews() { + return mPreviews != null && !mPreviews.isEmpty(); + } + + + private void sortPreviewsByScreenSize() { + if (mPreviews != null) { + Collections.sort(mPreviews, new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + Configuration config1 = preview1.getConfiguration(); + Configuration config2 = preview2.getConfiguration(); + Device device1 = config1.getDevice(); + Device device2 = config1.getDevice(); + if (device1 != null && device2 != null) { + Screen screen1 = device1.getDefaultHardware().getScreen(); + Screen screen2 = device2.getDefaultHardware().getScreen(); + if (screen1 != null && screen2 != null) { + double delta = screen1.getDiagonalLength() + - screen2.getDiagonalLength(); + if (delta != 0.0) { + return (int) Math.signum(delta); + } else { + if (screen1.getPixelDensity() != screen2.getPixelDensity()) { + return screen1.getPixelDensity().compareTo( + screen2.getPixelDensity()); + } + } + } + + } + State state1 = config1.getDeviceState(); + State state2 = config2.getDeviceState(); + if (state1 != state2 && state1 != null && state2 != null) { + return state1.getName().compareTo(state2.getName()); + } + + return preview1.getDisplayName().compareTo(preview2.getDisplayName()); + } + }); + } + } + + private void sortPreviewsByOrientation() { + if (mPreviews != null) { + Collections.sort(mPreviews, new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + Configuration config1 = preview1.getConfiguration(); + Configuration config2 = preview2.getConfiguration(); + State state1 = config1.getDeviceState(); + State state2 = config2.getDeviceState(); + if (state1 != state2 && state1 != null && state2 != null) { + return state1.getName().compareTo(state2.getName()); + } + + return preview1.getDisplayName().compareTo(preview2.getDisplayName()); + } + }); + } + } + + /** + * Vertical scrollbar listener which updates render previews which are not + * visible and triggers a redraw + */ + private class ScrollBarListener implements SelectionListener { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPreviews == null) { + return; + } + + ScrollBar bar = mCanvas.getVerticalBar(); + int selection = bar.getSelection(); + int thumb = bar.getThumb(); + int maxY = selection + thumb; + if (maxY > mMaxVisibleY) { + } + for (RenderPreview preview : mPreviews) { + if (!preview.isVisible() && preview.getY() <= maxY) { + preview.render(RENDER_DELAY); + preview.setVisible(true); + } + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java new file mode 100644 index 0000000..0f06d7f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java @@ -0,0 +1,43 @@ +/* + * 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.gle2; + +/** + * The {@linkplain RenderPreviewMode} records what type of configurations to + * render in the layout editor + */ +public enum RenderPreviewMode { + /** Generate a set of default previews with maximum variation */ + DEFAULT, + + /** Preview all the locales */ + LOCALES, + + /** Preview all the screen sizes */ + SCREENS, + + /** Preview layout as included in other layouts */ + INCLUDES, + + /** Preview all the variations of this layout */ + VARIATIONS, + + /** Show a manually configured set of previews */ + CUSTOM, + + /** No previews */ + NONE; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java index e0c3add..ccf4068 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java @@ -32,6 +32,8 @@ import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser; @@ -82,9 +84,9 @@ public class RenderService { private final LayoutLibrary mLayoutLib; private final IImageFactory mImageFactory; private final Density mDensity; - private final float mXdpi; - private final float mYdpi; - private final ScreenSizeQualifier mScreenSize; + private float mXdpi; + private float mYdpi; + private ScreenSizeQualifier mScreenSize; // The following fields are optional or configurable using the various chained // setters: @@ -120,6 +122,51 @@ public class RenderService { mTargetSdkVersion = editor.getTargetSdkVersion(); } + private RenderService(GraphicalEditorPart editor, FolderConfiguration configuration, + ResourceResolver resourceResolver) { + mEditor = editor; + + mProject = editor.getProject(); + LayoutCanvas canvas = editor.getCanvasControl(); + mImageFactory = canvas.getImageOverlay(); + Configuration config = editor.getConfigurationChooser().getConfiguration(); + mXdpi = config.getXDpi(); + mYdpi = config.getYDpi(); + mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/); + mResourceResolver = resourceResolver != null ? resourceResolver : editor.getResourceResolver(); + mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib); + mMinSdkVersion = editor.getMinSdkVersion(); + mTargetSdkVersion = editor.getTargetSdkVersion(); + + // TODO: Look up device etc and offer additional configuration options here? + Density density = Density.MEDIUM; + DensityQualifier densityQualifier = configuration.getDensityQualifier(); + if (densityQualifier != null) { + // just a sanity check + Density d = densityQualifier.getValue(); + if (d != Density.NODPI) { + density = d; + } + } + mDensity = density; + mScreenSize = configuration.getScreenSizeQualifier(); + } + + /** + * Sets the screen size and density to use for rendering + * + * @param screenSize the screen size + * @param xdpi the x density + * @param ydpi the y density + * @return this, for constructor chaining + */ + public RenderService setScreen(ScreenSizeQualifier screenSize, float xdpi, float ydpi) { + mXdpi = xdpi; + mYdpi = ydpi; + mScreenSize = screenSize; + return this; + } + /** * Creates a new {@link RenderService} associated with the given editor. * @@ -133,6 +180,21 @@ public class RenderService { } /** + * Creates a new {@link RenderService} associated with the given editor. + * + * @param editor the editor to provide configuration data such as the render target + * @param configuration the configuration to use (and fallback to editor for the rest) + * @param resolver a resource resolver to use to look up resources + * @return a {@link RenderService} which can perform rendering services + */ + public static RenderService create(GraphicalEditorPart editor, + FolderConfiguration configuration, ResourceResolver resolver) { + RenderService renderService = new RenderService(editor, configuration, resolver); + + return renderService; + } + + /** * Renders the given model, using this editor's theme and screen settings, and returns * the result as a {@link RenderSession}. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java index 586da12..45d0644 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java @@ -37,6 +37,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewEleme import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.resources.Density; import com.android.utils.Pair; +import com.google.common.io.Closeables; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -140,6 +141,8 @@ public class ViewMetadataRepository { } catch (Exception e) { AdtPlugin.log(e, "Parsing palette file failed"); return null; + } finally { + Closeables.closeQuietly(paletteStream); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java index 8f678c1..99a6c81 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java @@ -353,14 +353,8 @@ public class UseCompoundDrawableRefactoring extends VisualRefactoring { } } - XmlFormatPreferences formatPrefs = XmlFormatPreferences.create(); - XmlPrettyPrinter printer = new XmlPrettyPrinter(formatPrefs, XmlFormatStyle.LAYOUT, - null /*lineSeparator*/); - StringBuilder sb = new StringBuilder(300); - printer.prettyPrint(-1, tempDocument, null, null, sb, false /*openTagOnly*/); - String xml = sb.toString(); - - + String xml = XmlPrettyPrinter.prettyPrint(tempDocument, XmlFormatPreferences.create(), + XmlFormatStyle.LAYOUT, null); TextEdit replace = new ReplaceEdit(mSelectionStart, mSelectionEnd - mSelectionStart, xml); rootEdit.addChild(replace); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java index 8526ad9..8a2877e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java @@ -17,9 +17,11 @@ package com.android.ide.eclipse.adt.internal.preferences; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.internal.build.DebugKeyProvider; import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; @@ -30,6 +32,7 @@ import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.PropertyChangeEvent; import java.io.File; +import java.util.Locale; public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_SDK_DIR = AdtPlugin.PLUGIN_ID + ".sdk"; //$NON-NLS-1$ @@ -69,6 +72,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_LINT_SEVERITIES = AdtPlugin.PLUGIN_ID + ".lintSeverities"; //$NON-NLS-1$ public final static String PREFS_FIX_LEGACY_EDITORS = AdtPlugin.PLUGIN_ID + ".fixLegacyEditors"; //$NON-NLS-1$ public final static String PREFS_SHARED_LAYOUT_EDITOR = AdtPlugin.PLUGIN_ID + ".sharedLayoutEditor"; //$NON-NLS-1$ + public final static String PREFS_PREVIEWS = AdtPlugin.PLUGIN_ID + ".previews"; //$NON-NLS-1$ /** singleton instance */ private final static AdtPrefs sThis = new AdtPrefs(); @@ -99,6 +103,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { private boolean mLintOnExport; private AttributeSortOrder mAttributeSort; private boolean mSharedLayoutEditor; + private RenderPreviewMode mPreviewMode = RenderPreviewMode.NONE; private int mPreferXmlEditor; public static enum BuildVerbosity { @@ -254,6 +259,17 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { mSharedLayoutEditor = mStore.getBoolean(PREFS_SHARED_LAYOUT_EDITOR); } + if (property == null || PREFS_PREVIEWS.equals(property)) { + mPreviewMode = RenderPreviewMode.NONE; + String previewMode = mStore.getString(PREFS_PREVIEWS); + if (previewMode != null && !previewMode.isEmpty()) { + try { + mPreviewMode = RenderPreviewMode.valueOf(previewMode.toUpperCase(Locale.US)); + } catch (IllegalArgumentException iae) { + // Ignore: Leave it as RenderPreviewMode.NONE + } + } + } } /** @@ -538,4 +554,29 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { store.setValue(PREFS_PREFER_XML, xml); } } + + /** + * Gets the {@link RenderPreviewMode} + * + * @return the preview mode + */ + @NonNull + public RenderPreviewMode getRenderPreviewMode() { + return mPreviewMode; + } + + /** + * Sets the {@link RenderPreviewMode} + * + * @param previewMode the preview mode + */ + public void setPreviewMode(@NonNull RenderPreviewMode previewMode) { + mPreviewMode = previewMode; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + if (previewMode != RenderPreviewMode.NONE) { + store.setValue(PREFS_PREVIEWS, previewMode.name().toLowerCase(Locale.US)); + } else { + store.setToDefault(PREFS_PREVIEWS); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java index 434384c..d78fa03 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java @@ -660,7 +660,9 @@ class TemplateHandler { } Document currentManifest = DomUtilities.parseStructuredDocument(currentXml); + assert currentManifest != null : currentXml; Document fragment = DomUtilities.parseStructuredDocument(xml); + assert fragment != null : xml; XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST; boolean modified; @@ -686,11 +688,8 @@ class TemplateHandler { String contents = null; if (ok) { if (modified) { - XmlPrettyPrinter printer = new XmlPrettyPrinter( + contents = XmlPrettyPrinter.prettyPrint(currentManifest, XmlFormatPreferences.create(), formatStyle, null); - StringBuilder sb = new StringBuilder(2 ); - printer.prettyPrint(-1, currentManifest, null, null, sb, false /*openTagOnly*/); - contents = sb.toString(); } } else { // Just insert into file along with comment, using the "standard" conflict diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationTest.java index f55cce4..c14e7f7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationTest.java @@ -101,4 +101,37 @@ public class ConfigurationTest extends TestCase { assertEquals(145.0f, configuration.getYDpi(), 0.001); assertEquals(new Rect(0, 0, 320, 480), configuration.getScreenBounds()); } + + public void testCopy() throws Exception { + Configuration configuration = createConfiguration(); + assertNotNull(configuration); + configuration.setTheme("@style/Theme"); + assertEquals("@style/Theme", configuration.getTheme()); + DeviceManager deviceManager = new DeviceManager(new StdLogger(StdLogger.Level.VERBOSE)); + List<Device> devices = deviceManager.getDefaultDevices(); + assertNotNull(devices); + assertTrue(devices.size() > 0); + configuration.setDevice(devices.get(0), false); + configuration.setActivity("foo.bar.FooActivity"); + configuration.setTheme("@android:style/Theme.Holo.Light"); + Locale locale = Locale.create(new LanguageQualifier("nb")); + configuration.setLocale(locale, false /* skipSync */); + + Configuration copy = Configuration.copy(configuration); + assertEquals(locale, copy.getLocale()); + assertEquals("foo.bar.FooActivity", copy.getActivity()); + assertEquals("@android:style/Theme.Holo.Light", copy.getTheme()); + assertEquals(devices.get(0), copy.getDevice()); + + // Make sure edits to master does not affect the child + configuration.setLocale(Locale.ANY, false); + configuration.setTheme("@android:style/Theme.Holo"); + configuration.setDevice(devices.get(1), true); + + assertTrue(copy.getFullConfig().getLanguageQualifier().equals(locale.language)); + assertEquals(locale, copy.getLocale()); + assertEquals("foo.bar.FooActivity", copy.getActivity()); + assertEquals("@android:style/Theme.Holo.Light", copy.getTheme()); + assertEquals(devices.get(0), copy.getDevice()); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtilsTest.java index 9e9c734..d1c56c2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtilsTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtilsTest.java @@ -30,6 +30,7 @@ import java.util.List; import junit.framework.TestCase; +@SuppressWarnings("javadoc") public class ImageUtilsTest extends TestCase { public void testCropBlank() throws Exception { BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB_PRE); @@ -339,6 +340,21 @@ public class ImageUtilsTest extends TestCase { assertEquals(0xFF00FF00, scaled.getRGB(48, 48)); assertEquals(0xFFFF0000, scaled.getRGB(100, 100)); assertEquals(0xFF00FF00, scaled.getRGB(199, 199)); + + scaled = ImageUtils.scale(image, 0.25, 0.25); + assertEquals(25, scaled.getWidth()); + assertEquals(25, scaled.getHeight()); + assertEquals(0xFF00FF00, scaled.getRGB(0, 0)); + assertEquals(0xFF00FF00, scaled.getRGB(24, 24)); + assertEquals(0xFFFF0000, scaled.getRGB(13, 13)); + + scaled = ImageUtils.scale(image, 0.25, 0.25, 75, 95); + assertEquals(100, scaled.getWidth()); + assertEquals(120, scaled.getHeight()); + assertEquals(0xFF00FF00, scaled.getRGB(0, 0)); + assertEquals(0xFF00FF00, scaled.getRGB(24, 24)); + assertEquals(0xFFFF0000, scaled.getRGB(13, 13)); + } public void testCreateColoredImage() throws Exception { @@ -349,5 +365,4 @@ public class ImageUtilsTest extends TestCase { assertEquals(0xFFFEFDFC, image.getRGB(50, 50)); assertEquals(0xFFFEFDFC, image.getRGB(119, 109)); } - } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java index 24fa0ae..c86623c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java @@ -17,9 +17,11 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import java.util.Arrays; import java.util.Collections; +import java.util.List; import junit.framework.TestCase; +@SuppressWarnings("javadoc") public class IncludeFinderTest extends TestCase { public void testEncodeDecode1() throws Exception { // Test ending with just a key @@ -66,4 +68,61 @@ public class IncludeFinderTest extends TestCase { finder.setIncluded("baz", Collections.<String>emptyList(), false); assertEquals(Collections.emptyList(), finder.getIncludedBy("foo")); } + + public void testFindIncludes() throws Exception { + String xml = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " android:orientation=\"vertical\" >\n" + + "\n" + + " <RadioButton\n" + + " android:id=\"@+id/radioButton1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:text=\"RadioButton\" />\n" + + "\n" + + " <include\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " layout=\"@layout/layout3\" />\n" + + "\n" + + " <include\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " layout=\"@layout/layout4\" />\n" + + "\n" + + "</LinearLayout>"; + List<String> includes = IncludeFinder.findIncludes(xml); + Collections.sort(includes); + assertEquals(Arrays.asList("layout3", "layout4"), includes); + } + + public void testFindFragments() throws Exception { + String xml = + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:context=\".MainActivity\" >\n" + + "\n" + + " <fragment\n" + + " android:id=\"@+id/fragment1\"\n" + + " android:name=\"android.app.ListFragment\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_alignParentTop=\"true\"\n" + + " android:layout_marginLeft=\"58dp\"\n" + + " android:layout_marginTop=\"74dp\"\n" + + " tools:layout=\"@layout/myfragment\" />\n" + + "\n" + + "</RelativeLayout>"; + List<String> includes = IncludeFinder.findIncludes(xml); + Collections.sort(includes); + assertEquals(Arrays.asList("myfragment"), includes); + } + + } diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java b/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java index e2fe767..a513c1f 100644 --- a/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java +++ b/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java @@ -22,6 +22,7 @@ import com.android.resources.ResourceFolderType; import com.android.resources.ScreenOrientation; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; @@ -105,6 +106,52 @@ public final class FolderConfiguration implements Comparable<FolderConfiguration } /** + * Creates a {@link FolderConfiguration} matching the folder segments. + * @param folderSegments The segments of the folder name. The first segments should contain + * the name of the folder + * @return a FolderConfiguration object, or null if the folder name isn't valid.. + * @see FolderConfiguration#getConfig(String[]) + */ + public static FolderConfiguration getConfig(Iterable<String>folderSegments) { + FolderConfiguration config = new FolderConfiguration(); + + // we are going to loop through the segments, and match them with the first + // available qualifier. If the segment doesn't match we try with the next qualifier. + // Because the order of the qualifier is fixed, we do not reset the first qualifier + // after each successful segment. + // If we run out of qualifier before processing all the segments, we fail. + + int qualifierIndex = 0; + int qualifierCount = DEFAULT_QUALIFIERS.length; + + Iterator<String> iterator = folderSegments.iterator(); + if (iterator.hasNext()) { + // Skip the first segment: it should be just the base folder, such as "values" or + // "layout" + iterator.next(); + } + while (iterator.hasNext()) { + String seg = iterator.next(); + if (seg.length() > 0) { + while (qualifierIndex < qualifierCount && + DEFAULT_QUALIFIERS[qualifierIndex].checkAndSet(seg, config) == false) { + qualifierIndex++; + } + + // if we reached the end of the qualifier we didn't find a matching qualifier. + if (qualifierIndex == qualifierCount) { + return null; + } + + } else { + return null; + } + } + + return config; + } + + /** * Returns the number of {@link ResourceQualifier} that make up a Folder configuration. */ public static int getQualifierCount() { |