diff options
author | Tor Norbye <tnorbye@google.com> | 2012-09-17 14:42:52 -0700 |
---|---|---|
committer | android code review <noreply-gerritcodereview@google.com> | 2012-09-17 14:42:53 -0700 |
commit | 0502801c8450f5263316792ed782fb58fe684768 (patch) | |
tree | 0dd75f1568f7ca7bd277d060249e72002928fa1a | |
parent | 0f86143e55290c4851e51b9a46ff21e198a35acc (diff) | |
parent | fe51dba2aa25e559786e5da315d4db714ffe7559 (diff) | |
download | sdk-0502801c8450f5263316792ed782fb58fe684768.zip sdk-0502801c8450f5263316792ed782fb58fe684768.tar.gz sdk-0502801c8450f5263316792ed782fb58fe684768.tar.bz2 |
Merge "Support separate layout editors for a single layout resource"
41 files changed, 5436 insertions, 4090 deletions
diff --git a/common/src/com/android/SdkConstants.java b/common/src/com/android/SdkConstants.java index d2f1421..53277c5 100644 --- a/common/src/com/android/SdkConstants.java +++ b/common/src/com/android/SdkConstants.java @@ -26,9 +26,15 @@ import java.io.File; * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> * <li><code>FN_</code> File name constant.</li> * <li><code>FD_</code> Folder name constant.</li> + * <li><code>TAG_</code> XML element tag name</li> + * <li><code>ATTR_</code> XML attribute name</li> + * <li><code>VALUE_</code> XML attribute value</li> + * <li><code>CLASS_</code> Class name</li> + * <li><code>DOT_</code> File name extension, including the dot </li> + * <li><code>EXT_</code> File name extension, without the dot </li> * </ul> - * */ +@SuppressWarnings("javadoc") // Not documenting all the fields here public final class SdkConstants { public static final int PLATFORM_UNKNOWN = 0; public static final int PLATFORM_LINUX = 1; @@ -690,7 +696,7 @@ public final class SdkConstants { public static final String ATTR_LAYOUT_RESOURCE_PREFIX = "layout_"; //$NON-NLS-1$ public static final String ATTR_CLASS = "class"; //$NON-NLS-1$ public static final String ATTR_STYLE = "style"; //$NON-NLS-1$ - + public static final String ATTR_CONTEXT = "context"; //$NON-NLS-1$ public static final String ATTR_ID = "id"; //$NON-NLS-1$ public static final String ATTR_TEXT = "text"; //$NON-NLS-1$ public static final String ATTR_TEXT_SIZE = "textSize"; //$NON-NLS-1$ diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index e2c4340..d678c2d 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -240,6 +240,7 @@ registry reindent reindenting remap +renderable reparse reparses rescales @@ -306,6 +307,7 @@ timestamp timestamps tmp toolbar +toolbars tooltip tooltips traceview diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index 692adc7..a7ef6c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -88,7 +88,6 @@ import org.eclipse.ui.IEditorDescriptor; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.browser.IWebBrowser; @@ -1964,15 +1963,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { */ public static IEditorPart openFile(IFile file, IRegion region, boolean showEditorTab) throws PartInitException { - IWorkbench workbench = PlatformUI.getWorkbench(); - if (workbench == null) { - return null; - } - IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow(); - if (activeWorkbenchWindow == null) { - return null; - } - IWorkbenchPage page = activeWorkbenchWindow.getActivePage(); + IWorkbenchPage page = AdtUtils.getActiveWorkbenchPage(); if (page == null) { return null; } 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 f5f7770..40d5e6f 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 @@ -405,6 +405,29 @@ public class AdtUtils { } /** + * Returns the current active workbench page, or null if not found + * + * @return the current page, or null + */ + @Nullable + public static IWorkbenchPage getActiveWorkbenchPage() { + IWorkbenchWindow window = getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + IWorkbenchPage[] pages = window.getPages(); + if (pages.length > 0) { + page = pages[0]; + } + } + + return page; + } + + return null; + } + + /** * Returns the current active workbench part, or null if not found * * @return the current active workbench part, or null @@ -1327,4 +1350,14 @@ public class AdtUtils { return null; } + + /** + * Returns whether the current thread is the UI thread + * + * @return true if the current thread is the UI thread + */ + public static boolean isUiThread() { + return AdtPlugin.getDisplay() != null + && AdtPlugin.getDisplay().getThread() == Thread.currentThread(); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java index 421ffb0..55d463b 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java @@ -16,9 +16,12 @@ package com.android.ide.eclipse.adt.internal.editors.common; +import static com.android.SdkConstants.FD_RES_LAYOUT; + import com.android.ide.common.resources.ResourceFolder; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorMatchingStrategy; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.resources.ResourceFolderType; @@ -41,27 +44,29 @@ public class CommonMatchingStrategy implements IEditorMatchingStrategy { FileEditorInput fileInput = (FileEditorInput)input; // get the IFile object and check it's in one of the layout folders. - IFile iFile = fileInput.getFile(); - ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile); - - if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) { + IFile file = fileInput.getFile(); + if (file.getParent().getName().startsWith(FD_RES_LAYOUT)) { + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); + if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT + && AdtPrefs.getPrefs().isSharedLayoutEditor()) { + LayoutEditorMatchingStrategy m = new LayoutEditorMatchingStrategy(); + return m.matches(editorRef, fileInput); + } + } - LayoutEditorMatchingStrategy m = new LayoutEditorMatchingStrategy(); - return m.matches(editorRef, fileInput); - } else { - // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput() - // is expensive so try exclude files that definitely don't match, such - // as those with the wrong extension or wrong file name - if (iFile.getName().equals(editorRef.getName()) && - editorRef.getId().equals(CommonXmlEditor.ID)) { - try { - return input.equals(editorRef.getEditorInput()); - } catch (PartInitException e) { - AdtPlugin.log(e, null); - } + // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput() + // is expensive so try exclude files that definitely don't match, such + // as those with the wrong extension or wrong file name + if (file.getName().equals(editorRef.getName()) && + editorRef.getId().equals(CommonXmlEditor.ID)) { + try { + return input.equals(editorRef.getEditorInput()); + } catch (PartInitException e) { + AdtPlugin.log(e, null); } } } + return false; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java index 6edb68c..b6cbf2c 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java @@ -225,4 +225,14 @@ public abstract class CommonXmlDelegate { /** Called after an editor has been deactivated */ public void delegateDeactivated() { } + + /** + * Returns the name of the editor to be shown in the editor tab etc. Return + * null to keep the default. + * + * @return the part name, or null to use the default + */ + public String delegateGetPartName() { + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java index e3b5721..7fb820c 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java @@ -417,6 +417,18 @@ public class CommonXmlEditor extends AndroidXmlEditor implements IShowEditorInpu } } + @Override + public String getPartName() { + if (mDelegate != null) { + String name = mDelegate.delegateGetPartName(); + if (name != null) { + return name; + } + } + + return super.getPartName(); + } + // -------------------- // Base methods exposed so that XmlEditorDelegate can access them diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java index 6e151cc..e10c33b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java @@ -49,6 +49,7 @@ import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; import com.android.tools.lint.client.api.IssueRegistry; +import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; @@ -82,6 +83,7 @@ import org.eclipse.wst.sse.ui.StructuredTextEditor; import org.w3c.dom.Document; import org.w3c.dom.Node; +import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -95,6 +97,9 @@ import java.util.Set; public class LayoutEditorDelegate extends CommonXmlDelegate implements IShowEditorInput, CommonXmlDelegate.IActionContributorDelegate { + /** The prefix for layout folders that are not the default layout folder */ + private static final String LAYOUT_FOLDER_PREFIX = "layout-"; //$NON-NLS-1$ + public static class Creator implements IDelegateCreator { @Override @SuppressWarnings("unchecked") @@ -313,8 +318,10 @@ public class LayoutEditorDelegate extends CommonXmlDelegate /** * Called to replace the current {@link IEditorInput} with another one. - * <p/>This is used when {@link LayoutEditorMatchingStrategy} returned <code>true</code> which means we're - * opening a different configuration of the same layout. + * <p/> + * This is used when {@link LayoutEditorMatchingStrategy} returned + * <code>true</code> which means we're opening a different configuration of + * the same layout. */ @Override public void showEditorInput(IEditorInput editorInput) { @@ -737,6 +744,25 @@ public class LayoutEditorDelegate extends CommonXmlDelegate } } + @Override + public String delegateGetPartName() { + IEditorInput editorInput = getEditor().getEditorInput(); + if (editorInput instanceof IFileEditorInput) { + IFileEditorInput fileInput = (IFileEditorInput) editorInput; + IFile file = fileInput.getFile(); + IContainer parent = file.getParent(); + if (parent != null) { + String parentName = parent.getName(); + if (parentName.startsWith(LAYOUT_FOLDER_PREFIX)) { + parentName = parentName.substring(LAYOUT_FOLDER_PREFIX.length()); + return parentName + File.separatorChar + file.getName(); + } + } + } + + return super.delegateGetPartName(); + } + // ---- Local Methods ---- /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java index 4ea49f9..c1c6068 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java @@ -41,14 +41,14 @@ public class LayoutEditorMatchingStrategy implements IEditorMatchingStrategy { FileEditorInput fileInput = (FileEditorInput)input; // get the IFile object and check it's in one of the layout folders. - IFile iFile = fileInput.getFile(); + IFile file = fileInput.getFile(); ResourceManager manager = ResourceManager.getInstance(); - ResourceFolder resFolder = manager.getResourceFolder(iFile); + ResourceFolder resFolder = manager.getResourceFolder(file); // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput() // is expensive so try exclude files that definitely don't match, such // as those with the wrong extension or wrong file name - if (!iFile.getName().equals(editorRef.getName()) || + if (!file.getName().equals(editorRef.getName()) || !editorRef.getId().equals(CommonXmlEditor.ID)) { return false; } @@ -60,16 +60,16 @@ public class LayoutEditorMatchingStrategy implements IEditorMatchingStrategy { IEditorInput editorInput = editorRef.getEditorInput(); if (editorInput instanceof FileEditorInput) { FileEditorInput editorFileInput = (FileEditorInput)editorInput; - IFile editorIFile = editorFileInput.getFile(); + IFile editorFile = editorFileInput.getFile(); - ResourceFolder editorFolder = manager.getResourceFolder(editorIFile); + ResourceFolder editorFolder = manager.getResourceFolder(editorFile); if (editorFolder == null || editorFolder.getType() != ResourceFolderType.LAYOUT) { return false; } - return editorIFile.getProject().equals(iFile.getProject()) - && editorIFile.getName().equals(iFile.getName()); + return editorFile.getProject().equals(file.getProject()) + && editorFile.getName().equals(file.getName()); } } catch (PartInitException e) { // we do nothing, we'll just return false. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java new file mode 100644 index 0000000..1f85a32 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.ui.ISharedImages; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The {@linkplain ActivityMenuListener} class is responsible for + * generating the activity menu in the {@link ConfigurationChooser}. + */ +class ActivityMenuListener extends SelectionAdapter { + private static final int ACTION_OPEN_ACTIVITY = 1; + private static final int ACTION_SELECT_ACTIVITY = 2; + + private final ConfigurationChooser mConfigChooser; + private final int mAction; + private final String mFqcn; + + ActivityMenuListener( + @NonNull ConfigurationChooser configChooser, + int action, + @Nullable String fqcn) { + mConfigChooser = configChooser; + mAction = action; + mFqcn = fqcn; + } + + @Override + public void widgetSelected(SelectionEvent e) { + switch (mAction) { + case ACTION_OPEN_ACTIVITY: { + Configuration configuration = mConfigChooser.getConfiguration(); + String fqcn = configuration.getActivity(); + AdtPlugin.openJavaClass(mConfigChooser.getProject(), fqcn); + break; + } + case ACTION_SELECT_ACTIVITY: { + mConfigChooser.selectActivity(mFqcn); + mConfigChooser.onSelectActivity(); + break; + } + default: assert false : mAction; + } + } + + static void show(ConfigurationChooser chooser, ToolItem combo) { + // TODO: Allow using fragments here as well? + Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + ISharedImages sharedImages = JavaUI.getSharedImages(); + Configuration configuration = chooser.getConfiguration(); + String current = configuration.getActivity(); + + if (current != null) { + MenuItem item = new MenuItem(menu, SWT.PUSH); + String label = ConfigurationChooser.getActivityLabel(current, true);; + item.setText( String.format("Open %1$s...", label)); + Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CUNIT); + item.setImage(image); + item.addSelectionListener( + new ActivityMenuListener(chooser, ACTION_OPEN_ACTIVITY, null)); + + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + } + + IProject project = chooser.getProject(); + Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CLASS); + + // Add activities found to be relevant to this layout + String layoutName = ResourceHelper.getLayoutName(chooser.getEditedFile()); + String pkg = ManifestInfo.get(project).getPackage(); + List<String> preferred = ManifestInfo.guessActivities(project, layoutName, pkg); + current = addActivities(chooser, menu, current, image, preferred); + + // Add all activities + List<String> activities = ManifestInfo.getProjectActivities(project); + if (preferred.size() > 0) { + // Filter out the activities we've already listed above + List<String> filtered = new ArrayList<String>(activities.size()); + Set<String> remove = new HashSet<String>(preferred); + for (String fqcn : activities) { + if (!remove.contains(fqcn)) { + filtered.add(fqcn); + } + } + activities = filtered; + } + + if (activities.size() > 0) { + if (preferred.size() > 0) { + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + } + + addActivities(chooser, menu, current, image, activities); + } + + Rectangle bounds = combo.getBounds(); + Point location = new Point(bounds.x, bounds.y + bounds.height); + location = combo.getParent().toDisplay(location); + menu.setLocation(location.x, location.y); + menu.setVisible(true); + } + + private static String addActivities(ConfigurationChooser chooser, Menu menu, String current, + Image image, List<String> activities) { + for (final String fqcn : activities) { + String title = ConfigurationChooser.getActivityLabel(fqcn, false); + MenuItem item = new MenuItem(menu, SWT.CHECK); + item.setText(title); + item.setImage(image); + + boolean selected = title.equals(current); + if (selected) { + item.setSelection(true); + current = null; // Only show the first occurrence as selected + // such that we don't show it selected again in the full activity list + } + + item.addSelectionListener(new ActivityMenuListener(chooser, + ACTION_OPEN_ACTIVITY, fqcn)); + } + + return current; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java new file mode 100644 index 0000000..2106f8d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java @@ -0,0 +1,891 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; +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.NightModeQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.UiModeQualifier; +import com.android.ide.common.resources.configuration.VersionQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.resources.Density; +import com.android.resources.NightMode; +import com.android.resources.ScreenOrientation; +import com.android.resources.UiMode; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.State; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; + +import java.util.List; + +/** + * A {@linkplain Configuration} is a selection of device, orientation, theme, + * etc for use when rendering a layout. + */ +public class Configuration { + /** + * Setting name for project-wide setting controlling rendering target and locale which + * is shared for all files + */ + public final static QualifiedName NAME_RENDER_STATE = + new QualifiedName(AdtPlugin.PLUGIN_ID, "render"); //$NON-NLS-1$ + + private final static String MARKER_FRAMEWORK = "-"; //$NON-NLS-1$ + private final static String MARKER_PROJECT = "+"; //$NON-NLS-1$ + private final static String SEP = ":"; //$NON-NLS-1$ + private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ + + @NonNull + protected final ConfigurationChooser mConfigChooser; + + /** The {@link FolderConfiguration} representing the state of the UI controls */ + @NonNull + protected final FolderConfiguration mFullConfig = new FolderConfiguration(); + + /** The {@link FolderConfiguration} being edited. */ + @Nullable + protected FolderConfiguration mEditedConfig; + + /** The target of the project of the file being edited. */ + @Nullable + private IAndroidTarget mTarget; + + /** The theme style to render with */ + @Nullable + private String mTheme; + + /** The device to render with */ + @Nullable + private Device mDevice; + + /** The device state */ + @Nullable + private State mState; + + /** + * The activity associated with the layout. This is just a cached value of + * the true value stored on the layout. + */ + @Nullable + private String mActivity; + + /** The locale to use for this configuration */ + @NonNull + private Locale mLocale = Locale.ANY; + + /** UI mode */ + @NonNull + private UiMode mUiMode = UiMode.NORMAL; + + /** Night mode */ + @NonNull + private NightMode mNightMode = NightMode.NOTNIGHT; + + /** + * Creates a new {@linkplain Configuration} + * + * @param chooser the associated chooser + */ + protected Configuration(@NonNull ConfigurationChooser chooser) { + mConfigChooser = chooser; + } + + /** + * Creates a new {@linkplain Configuration} + * + * @param chooser the associated chooser + * @return a new configuration + */ + @NonNull + public static Configuration create(@NonNull ConfigurationChooser chooser) { + return new Configuration(chooser); + } + + /** + * Returns the associated activity + * + * @return the activity + */ + @Nullable + public String getActivity() { + return mActivity; + } + + /** + * Returns the chosen device. + * + * @return the chosen device + */ + @Nullable + public Device getDevice() { + return mDevice; + } + + /** + * Returns the chosen device state + * + * @return the device state + */ + @Nullable + public State getDeviceState() { + return mState; + } + + /** + * Returns the chosen locale + * + * @return the locale + */ + @NonNull + public Locale getLocale() { + return mLocale; + } + + /** + * Returns the UI mode + * + * @return the UI mode + */ + @NonNull + public UiMode getUiMode() { + return mUiMode; + } + + /** + * Returns the day/night mode + * + * @return the night mode + */ + @NonNull + public NightMode getNightMode() { + return mNightMode; + } + + /** + * Returns the current theme style + * + * @return the theme style + */ + @Nullable + public String getTheme() { + return mTheme; + } + + /** + * Returns the rendering target + * + * @return the target + */ + @Nullable + public IAndroidTarget getTarget() { + return mTarget; + } + + /** + * Returns whether the configuration's theme is a project theme. + * <p/> + * The returned value is meaningless if {@link #getTheme()} returns + * <code>null</code>. + * + * @return true for project a theme, false for a framework theme + */ + public boolean isProjectTheme() { + String theme = getTheme(); + if (theme != null) { + assert theme.startsWith(STYLE_RESOURCE_PREFIX) + || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX); + + return ResourceHelper.isProjectStyle(theme); + } + + return false; + } + + /** + * Returns true if the current layout is locale-specific + * + * @return if this configuration represents a locale-specific layout + */ + public boolean isLocaleSpecificLayout() { + return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null; + } + + /** + * Returns the full, complete {@link FolderConfiguration} + * + * @return the full configuration + */ + @NonNull + public FolderConfiguration getFullConfig() { + return mFullConfig; + } + + /** + * Copies the full, complete {@link FolderConfiguration} into the given + * folder config instance. + * + * @param dest the {@link FolderConfiguration} instance to copy into + */ + public void copyFullConfig(FolderConfiguration dest) { + dest.set(mFullConfig); + } + + /** + * Returns the edited {@link FolderConfiguration} (this is not a full + * configuration, so you can think of it as the "constraints" used by the + * {@link ConfigurationMatcher} to produce a full configuration. + * + * @return the constraints configuration + */ + @NonNull + public FolderConfiguration getEditedConfig() { + return mEditedConfig; + } + + /** + * Sets the edited {@link FolderConfiguration} (this is not a full + * configuration, so you can think of it as the "constraints" used by the + * {@link ConfigurationMatcher} to produce a full configuration. + * + * @param editedConfig the constraints configuration + */ + public void setEditedConfig(@NonNull FolderConfiguration editedConfig) { + mEditedConfig = editedConfig; + } + + /** + * Sets the associated activity + * + * @param activity the activity + */ + public void setActivity(String activity) { + mActivity = activity; + } + + /** + * Sets the device + * + * @param device the device + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setDevice(Device device, boolean skipSync) { + mDevice = device; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the device state + * + * @param state the device state + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setDeviceState(State state, boolean skipSync) { + mState = state; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the locale + * + * @param locale the locale + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setLocale(@NonNull Locale locale, boolean skipSync) { + mLocale = locale; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the rendering target + * + * @param target rendering target + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setTarget(IAndroidTarget target, boolean skipSync) { + mTarget = target; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the night mode + * + * @param night the night mode + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setNightMode(@NonNull NightMode night, boolean skipSync) { + mNightMode = night; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the UI mode + * + * @param uiMode the UI mode + * @param skipSync if true, don't sync folder configuration (typically because + * you are going to set other configuration parameters and you'll call + * {@link #syncFolderConfig()} once at the end) + */ + public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) { + mUiMode = uiMode; + + if (!skipSync) { + syncFolderConfig(); + } + } + + /** + * Sets the theme style + * + * @param theme the theme + */ + public void setTheme(String theme) { + mTheme = theme; + } + + /** + * Updates the folder configuration such that it reflects changes in + * configuration state such as the device orientation, the UI mode, the + * rendering target, etc. + */ + public void syncFolderConfig() { + Device device = getDevice(); + if (device == null) { + return; + } + + // get the device config from the device/state combos. + FolderConfiguration config = DeviceConfigHelper.getFolderConfig(getDeviceState()); + + // replace the config with the one from the device + mFullConfig.set(config); + + // sync the selected locale + Locale locale = getLocale(); + mFullConfig.setLanguageQualifier(locale.language); + mFullConfig.setRegionQualifier(locale.region); + + // Replace the UiMode with the selected one, if one is selected + UiMode uiMode = getUiMode(); + if (uiMode != null) { + mFullConfig.setUiModeQualifier(new UiModeQualifier(uiMode)); + } + + // Replace the NightMode with the selected one, if one is selected + NightMode nightMode = getNightMode(); + if (nightMode != null) { + mFullConfig.setNightModeQualifier(new NightModeQualifier(nightMode)); + } + + // replace the API level by the selection of the combo + IAndroidTarget target = getTarget(); + if (target == null && mConfigChooser != null) { + target = mConfigChooser.getProjectTarget(); + } + if (target != null) { + int apiLevel = target.getVersion().getApiLevel(); + mFullConfig.setVersionQualifier(new VersionQualifier(apiLevel)); + } + } + + /** + * Creates a string suitable for persistence, which can be initialized back + * to a configuration via {@link #initialize(String)} + * + * @return a persistent string + */ + @NonNull + public String toPersistentString() { + StringBuilder sb = new StringBuilder(32); + Device device = getDevice(); + if (device != null) { + sb.append(device.getName()); + sb.append(SEP); + State state = getDeviceState(); + if (state != null) { + sb.append(state.getName()); + } + sb.append(SEP); + Locale locale = getLocale(); + if (isLocaleSpecificLayout() && locale != null) { + // locale[0]/[1] can be null sometimes when starting Eclipse + sb.append(locale.language.getValue()); + sb.append(SEP_LOCALE); + sb.append(locale.region.getValue()); + } + sb.append(SEP); + // Need to escape the theme: if we write the full theme style, then + // we can end up with ":"'s in the string (as in @android:style/Theme) which + // can be mistaken for {@link #SEP}. Instead use {@link #MARKER_FRAMEWORK}. + String theme = getTheme(); + if (theme != null) { + String themeName = ResourceHelper.styleToTheme(theme); + if (theme.startsWith(STYLE_RESOURCE_PREFIX)) { + sb.append(MARKER_PROJECT); + } else if (theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { + sb.append(MARKER_FRAMEWORK); + } + sb.append(themeName); + } + sb.append(SEP); + UiMode uiMode = getUiMode(); + if (uiMode != null) { + sb.append(uiMode.getResourceValue()); + } + sb.append(SEP); + NightMode nightMode = getNightMode(); + if (nightMode != null) { + sb.append(nightMode.getResourceValue()); + } + sb.append(SEP); + + // We used to store the render target here in R9. Leave a marker + // to ensure that we don't reuse this slot; add new extra fields after it. + sb.append(SEP); + String activity = getActivity(); + if (activity != null) { + sb.append(activity); + } + } + + return sb.toString(); + } + + /** + * Initializes a string previously created with + * {@link #toPersistentString()} + * + * @param data the string to initialize back from + * @return true if the configuration was initialized + */ + boolean initialize(String data) { + String[] values = data.split(SEP); + if (values.length >= 6 && values.length <= 8) { + for (Device d : mConfigChooser.getDeviceList()) { + if (d.getName().equals(values[0])) { + mDevice = d; + String stateName = null; + FolderConfiguration config = null; + if (!values[1].isEmpty() && !values[1].equals("null")) { //$NON-NLS-1$ + stateName = values[1]; + config = DeviceConfigHelper.getFolderConfig(mDevice, stateName); + } else if (mDevice.getAllStates().size() > 0) { + State first = mDevice.getAllStates().get(0); + stateName = first.getName(); + config = DeviceConfigHelper.getFolderConfig(first); + } + mState = getState(mDevice, stateName); + if (config != null) { + // Load locale. Note that this can get overwritten by the + // project-wide settings read below. + LanguageQualifier language = Locale.ANY_LANGUAGE; + RegionQualifier region = Locale.ANY_REGION; + String locales[] = values[2].split(SEP_LOCALE); + if (locales.length >= 2) { + if (locales[0].length() > 0) { + language = new LanguageQualifier(locales[0]); + } + if (locales[1].length() > 0) { + region = new RegionQualifier(locales[1]); + } + mLocale = Locale.create(language, region); + } + + // Decode the theme name: See {@link #getData} + mTheme = values[3]; + if (mTheme.startsWith(MARKER_FRAMEWORK)) { + mTheme = ANDROID_STYLE_RESOURCE_PREFIX + + mTheme.substring(MARKER_FRAMEWORK.length()); + } else if (mTheme.startsWith(MARKER_PROJECT)) { + mTheme = STYLE_RESOURCE_PREFIX + + mTheme.substring(MARKER_PROJECT.length()); + } + + mUiMode = UiMode.getEnum(values[4]); + if (mUiMode == null) { + mUiMode = UiMode.NORMAL; + } + mNightMode = NightMode.getEnum(values[5]); + if (mNightMode == null) { + mNightMode = NightMode.NOTNIGHT; + } + + // element 7/values[6]: used to store render target in R9. + // No longer stored here. If adding more data, make + // sure you leave 7 alone. + + Pair<Locale, IAndroidTarget> pair = loadRenderState(mConfigChooser); + if (pair != null) { + // We only use the "global" setting + if (!isLocaleSpecificLayout()) { + mLocale = pair.getFirst(); + } + mTarget = pair.getSecond(); + } + + if (values.length == 8) { + mActivity = values[7]; + } + + return true; + } + } + } + } + + return false; + } + + /** + * Loads the render state (the locale and the render target, which are shared among + * all the layouts meaning that changing it in one will change it in all) and returns + * the current project-wide locale and render target to be used. + * + * @param chooser the {@link ConfigurationChooser} providing information about + * loaded targets + * @return a pair of a locale and a render target + */ + @Nullable + static Pair<Locale, IAndroidTarget> loadRenderState(ConfigurationChooser chooser) { + IProject project = chooser.getProject(); + if (!project.isAccessible()) { + return null; + } + + try { + String data = project.getPersistentProperty(NAME_RENDER_STATE); + if (data != null) { + Locale locale = Locale.ANY; + IAndroidTarget target = null; + + String[] values = data.split(SEP); + if (values.length == 2) { + LanguageQualifier language = Locale.ANY_LANGUAGE; + RegionQualifier region = Locale.ANY_REGION; + String locales[] = values[0].split(SEP_LOCALE); + if (locales.length >= 2) { + if (locales[0].length() > 0) { + language = new LanguageQualifier(locales[0]); + } + if (locales[1].length() > 0) { + region = new RegionQualifier(locales[1]); + } + } + locale = Locale.create(language, region); + + target = stringToTarget(chooser, values[1]); + + // See if we should "correct" the rendering target to a better version. + // If you're using a pre-release version of the render target, and a + // final release is available and installed, we should switch to that + // one instead. + if (target != null) { + AndroidVersion version = target.getVersion(); + List<IAndroidTarget> targetList = chooser.getTargetList(); + if (version.getCodename() != null && targetList != null) { + int targetApiLevel = version.getApiLevel() + 1; + for (IAndroidTarget t : targetList) { + if (t.getVersion().getApiLevel() == targetApiLevel + && t.isPlatform()) { + target = t; + break; + } + } + } + } + } + + return Pair.of(locale, target); + } + + return Pair.of(Locale.ANY, ConfigurationMatcher.findDefaultRenderTarget(project)); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** + * Saves the render state (the current locale and render target settings) into the + * project wide settings storage + */ + void saveRenderState() { + IProject project = mConfigChooser.getProject(); + try { + // Generate a persistent string from locale+target + StringBuilder sb = new StringBuilder(); + Locale locale = getLocale(); + if (locale != null) { + // locale[0]/[1] can be null sometimes when starting Eclipse + sb.append(locale.language.getValue()); + sb.append(SEP_LOCALE); + sb.append(locale.region.getValue()); + } + sb.append(SEP); + IAndroidTarget target = getTarget(); + if (target != null) { + sb.append(targetToString(target)); + sb.append(SEP); + } + + project.setPersistentProperty(NAME_RENDER_STATE, sb.toString()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + /** + * Returns a String id to represent an {@link IAndroidTarget} which can be translated + * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id + * will never contain the {@link #SEP} character. + * + * @param target the target to return an id for + * @return an id for the given target; never null + */ + @NonNull + private static String targetToString(@NonNull IAndroidTarget target) { + return target.getFullName().replace(SEP, ""); //$NON-NLS-1$ + } + + /** + * Returns an {@link IAndroidTarget} that corresponds to the given id that was + * originally returned by {@link #targetToString}. May be null, if the platform is no + * longer available, or if the platform list has not yet been initialized. + * + * @param chooser the {@link ConfigurationChooser} providing information about + * loaded targets + * @param id the id that corresponds to the desired platform + * @return an {@link IAndroidTarget} that matches the given id, or null + */ + @Nullable + private static IAndroidTarget stringToTarget( + @NonNull ConfigurationChooser chooser, + @NonNull String id) { + List<IAndroidTarget> targetList = chooser.getTargetList(); + if (targetList != null && targetList.size() > 0) { + for (IAndroidTarget target : targetList) { + if (id.equals(targetToString(target))) { + return target; + } + } + } + + return null; + } + + /** + * Returns the {@link State} by the given name for the given {@link Device} + * + * @param device the device + * @param name the name of the state + */ + @Nullable + static State getState(@Nullable Device device, @Nullable String name) { + if (device == null) { + return null; + } else if (name != null) { + State state = device.getState(name); + if (state != null) { + return state; + } + } + + return device.getDefaultState(); + } + + /** + * Returns the currently selected {@link Density}. This is guaranteed to be non null. + * + * @return the density + */ + @NonNull + public Density getDensity() { + if (mFullConfig != null) { + DensityQualifier qual = mFullConfig.getDensityQualifier(); + if (qual != null) { + // just a sanity check + Density d = qual.getValue(); + if (d != Density.NODPI) { + return d; + } + } + } + + // no config? return medium as the default density. + return Density.MEDIUM; + } + + /** + * Returns the current device xdpi. + * + * @return the x dpi as a float + */ + public float getXDpi() { + Device device = getDevice(); + if (device != null) { + State currState = getDeviceState(); + if (currState == null) { + currState = device.getDefaultState(); + } + float dpi = (float) currState.getHardware().getScreen().getXdpi(); + if (!Float.isNaN(dpi)) { + return dpi; + } + } + + // get the pixel density as the density. + return getDensity().getDpiValue(); + } + + /** + * Returns the current device ydpi. + * + * @return the y dpi as a float + */ + public float getYDpi() { + Device device = getDevice(); + if (device != null) { + State currState = getDeviceState(); + if (currState == null) { + currState = device.getDefaultState(); + } + float dpi = (float) currState.getHardware().getScreen().getYdpi(); + if (!Float.isNaN(dpi)) { + return dpi; + } + } + + // get the pixel density as the density. + return getDensity().getDpiValue(); + } + + /** + * Returns the bounds of the screen + * + * @return the screen bounds + */ + public Rect getScreenBounds() { + return getScreenBounds(mFullConfig); + } + + /** + * Gets the orientation from the given configuration + * + * @param config the configuration to look up + * @return the bounds + */ + @NonNull + public static Rect getScreenBounds(FolderConfiguration config) { + // get the orientation from the given device config + ScreenOrientationQualifier qual = config.getScreenOrientationQualifier(); + ScreenOrientation orientation = ScreenOrientation.PORTRAIT; + if (qual != null) { + orientation = qual.getValue(); + } + + // get the device screen dimension + ScreenDimensionQualifier qual2 = config.getScreenDimensionQualifier(); + int s1, s2; + if (qual2 != null) { + s1 = qual2.getValue1(); + s2 = qual2.getValue2(); + } else { + s1 = 480; + s2 = 320; + } + + switch (orientation) { + default: + case PORTRAIT: + return new Rect(0, 0, s2, s1); + case LANDSCAPE: + return new Rect(0, 0, s1, s2); + case SQUARE: + return new Rect(0, 0, s1, s1); + } + } + + /** + * Get the next cyclical state after the given state + * + * @param from the state to start with + * @return the following state following + */ + @Nullable + public State getNextDeviceState(@Nullable State from) { + Device device = getDevice(); + if (device == null) { + return null; + } + List<State> states = device.getAllStates(); + for (int i = 0; i < states.size(); i++) { + if (states.get(i) == from) { + return states.get((i + 1) % states.size()); + } + } + + return null; + } + + @Override + public String toString() { + return toPersistentString(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java new file mode 100644 index 0000000..b512bcc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java @@ -0,0 +1,1945 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.RES_QUALIFIER_SEP; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.ide.eclipse.adt.AdtUtils.isUiThread; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_ALL; +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_DEVICE_CONFIG; +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 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.ResourceFolder; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.configuration.DeviceConfigHelper; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +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.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenSize; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DevicesChangeListener; +import com.android.sdklib.devices.State; +import com.android.utils.Pair; +import com.google.common.base.Objects; +import com.google.common.base.Strings; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; + +/** + * The {@linkplain ConfigurationChooser} allows the user to pick a + * {@link Configuration} by configuring various constraints. + */ +public class ConfigurationChooser extends Composite + implements DevicesChangeListener, DisposeListener { + /** + * Settings name for file-specific configuration preferences, such as which theme or + * device to render the current layout with + */ + public final static QualifiedName NAME_CONFIG_STATE = + new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ + + private static final String ICON_SQUARE = "square"; //$NON-NLS-1$ + private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$ + private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$ + private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$ + private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$ + private static final String ICON_DISPLAY = "display"; //$NON-NLS-1$ + private static final String ICON_THEMES = "themes"; //$NON-NLS-1$ + private static final String ICON_ACTIVITY = "activity"; //$NON-NLS-1$ + + /** The configuration state associated with this editor */ + private @NonNull Configuration mConfiguration = Configuration.create(this); + + /** Serialized state to use when initializing the configuration after the SDK is loaded */ + private String mInitialState; + + /** The client of the configuration editor */ + private final ConfigurationClient mClient; + + /** Counter for programmatic UI changes: if greater than 0, we're within a call */ + private int mDisableUpdates = 0; + + /** List of available devices */ + private List<Device> mDeviceList = Collections.emptyList(); + + /** List of available targets */ + private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>(); + + /** List of available themes */ + private final List<String> mThemeList = new ArrayList<String>(); + + /** List of available locales */ + private final List<Locale > mLocaleList = new ArrayList<Locale>(); + + /** The file being edited */ + private IFile mEditedFile; + + /** The {@link ProjectResources} for the edited file's project */ + private ProjectResources mResources; + + /** The target of the project of the file being edited. */ + private IAndroidTarget mProjectTarget; + + /** Dropdown for configurations */ + private ToolItem mConfigCombo; + + /** Dropdown for devices */ + private ToolItem mDeviceCombo; + + /** Dropdown for device states */ + private ToolItem mOrientationCombo; + + /** Dropdown for themes */ + private ToolItem mThemeCombo; + + /** Dropdown for locales */ + private ToolItem mLocaleCombo; + + /** Dropdown for activities */ + private ToolItem mActivityCombo; + + /** Dropdown for rendering targets */ + private ToolItem mTargetCombo; + + /** Whether the SDK has changed since the last model reload; if so we must reload targets */ + private boolean mSdkChanged = true; + + /** + * Creates a new {@linkplain ConfigurationChooser} and adds it to the + * parent. The method also receives custom buttons to set into the + * configuration composite. The list is organized as an array of arrays. + * Each array represents a group of buttons thematically grouped together. + * + * @param client the client embedding this configuration chooser + * @param parent The parent composite. + * @param initialState The initial state (serialized form) to use for the + * configuration + */ + public ConfigurationChooser( + @NonNull ConfigurationClient client, + Composite parent, + @Nullable String initialState) { + super(parent, SWT.NONE); + mClient = client; + + setVisible(false); // Delayed until the targets are loaded + + mInitialState = initialState; + setLayout(new GridLayout(1, false)); + + IconFactory icons = IconFactory.getInstance(); + + // TODO: Consider switching to a CoolBar instead + ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); + toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN | SWT.BOLD); + mConfigCombo.setImage(null); + mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse"); + + @SuppressWarnings("unused") + ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR); + + mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN); + mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY)); + + @SuppressWarnings("unused") + ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR); + + mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN); + mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT)); + mOrientationCombo.setToolTipText("Go to next state"); + + @SuppressWarnings("unused") + ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR); + + mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN); + mThemeCombo.setImage(icons.getIcon(ICON_THEMES)); + + @SuppressWarnings("unused") + ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR); + + mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN); + mActivityCombo.setToolTipText("Associated activity or fragment providing context"); + // The JDT class icon is lopsided, presumably because they've left room in the + // bottom right corner for badges (for static, final etc). Unfortunately, this + // means that the icon looks out of place when sitting close to the language globe + // icon, the theme icon, etc so that it looks vertically misaligned: + //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS)); + // ...so use one that is centered instead: + mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY)); + + @SuppressWarnings("unused") + ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR); + + //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); + //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + ToolBar rightToolBar = toolBar; + + mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); + mLocaleCombo.setImage(LocaleManager.getGlobeIcon()); + mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse"); + + @SuppressWarnings("unused") + ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR); + + mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); + mTargetCombo.setImage(AdtPlugin.getAndroidLogo()); + mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse"); + + SelectionListener listener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + + if (source == mConfigCombo) { + ConfigurationMenuListener.show(ConfigurationChooser.this, mConfigCombo); + } else if (source == mActivityCombo) { + ActivityMenuListener.show(ConfigurationChooser.this, mActivityCombo); + } else if (source == mLocaleCombo) { + LocaleMenuListener.show(ConfigurationChooser.this, mLocaleCombo); + } else if (source == mDeviceCombo) { + DeviceMenuListener.show(ConfigurationChooser.this, mDeviceCombo); + } else if (source == mTargetCombo) { + TargetMenuListener.show(ConfigurationChooser.this, mTargetCombo); + } else if (source == mThemeCombo) { + ThemeMenuAction.showThemeMenu(ConfigurationChooser.this, mThemeCombo, + mThemeList); + } else if (source == mOrientationCombo) { + if (e.detail == SWT.ARROW) { + OrientationMenuAction.showMenu(ConfigurationChooser.this, + mOrientationCombo); + } else { + gotoNextState(); + } + } + } + }; + mConfigCombo.addSelectionListener(listener); + mActivityCombo.addSelectionListener(listener); + mLocaleCombo.addSelectionListener(listener); + mDeviceCombo.addSelectionListener(listener); + mTargetCombo.addSelectionListener(listener); + mThemeCombo.addSelectionListener(listener); + mOrientationCombo.addSelectionListener(listener); + + addDisposeListener(this); + } + + IFile getEditedFile() { + return mEditedFile; + } + + IProject getProject() { + return mEditedFile.getProject(); + } + + ConfigurationClient getClient() { + return mClient; + } + + ProjectResources getResources() { + return mResources; + } + + /** + * Returns the full, complete {@link FolderConfiguration} + * + * @return the full configuration + */ + public FolderConfiguration getFullConfiguration() { + return mConfiguration.getFullConfig(); + } + + /** + * Returns the project target + * + * @return the project target + */ + IAndroidTarget getProjectTarget() { + return mProjectTarget; + } + + /** + * Returns the configuration being edited by this {@linkplain ConfigurationChooser} + * + * @return the configuration + */ + public Configuration getConfiguration() { + return mConfiguration; + } + + /** + * Returns the list of locales + * @return a list of {@link ResourceQualifier} pairs + */ + @NonNull + public List<Locale> getLocaleList() { + return mLocaleList; + } + + /** + * Returns the list of available devices + * + * @return a list of {@link Device} objects + */ + @NonNull + public List<Device> getDeviceList() { + return mDeviceList; + } + + /** + * Returns the list of available render targets + * + * @return a list of {@link IAndroidTarget} objects + */ + @NonNull + public List<IAndroidTarget> getTargetList() { + return mTargetList; + } + + // ---- Configuration State Lookup ---- + + /** + * Returns the rendering target to be used + * + * @return the target + */ + @NonNull + public IAndroidTarget getTarget() { + IAndroidTarget target = mConfiguration.getTarget(); + if (target == null) { + target = mProjectTarget; + } + + return target; + } + + /** + * Returns the current device string, or null if no device is selected + * + * @return the device name, or null + */ + @Nullable + public String getDeviceName() { + Device device = mConfiguration.getDevice(); + if (device != null) { + return device.getName(); + } + + return null; + } + + /** + * Returns the current theme, or null if none has been selected + * + * @return the theme name, or null + */ + @Nullable + public String getThemeName() { + String theme = mConfiguration.getTheme(); + if (theme != null) { + theme = ResourceHelper.styleToTheme(theme); + } + + return theme; + } + + /** Move to the next device state, changing the icon if it changes orientation */ + private void gotoNextState() { + State state = mConfiguration.getDeviceState(); + State flipped = mConfiguration.getNextDeviceState(state); + if (flipped != state) { + selectDeviceState(flipped); + onDeviceConfigChange(); + } + } + + // ---- Implements DisposeListener ---- + + @Override + public void widgetDisposed(DisposeEvent e) { + dispose(); + } + + @Override + public void dispose() { + if (!isDisposed()) { + super.dispose(); + + final Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + DeviceManager manager = sdk.getDeviceManager(); + manager.unregisterListener(this); + } + } + } + + // ---- Init and reset/reload methods ---- + + /** + * Sets the reference to the file being edited. + * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is + * loaded (or reloaded as the SDK/target changes). + * + * @param file the file being opened + * + * @see #onXmlModelLoaded() + * @see #replaceFile(IFile) + * @see #changeFileOnNewConfig(IFile) + */ + public void setFile(IFile file) { + mEditedFile = file; + } + + /** + * Replaces the UI with a given file configuration. This is meant to answer the user + * explicitly opening a different version of the same layout from the Package Explorer. + * <p/>This attempts to keep the current config, but may change it if it's not compatible or + * not the best match + * @param file the file being opened. + */ + public void replaceFile(IFile file) { + // if there is no previous selection, revert to default mode. + if (mConfiguration.getDevice() == null) { + setFile(file); // onTargetChanged will be called later. + return; + } + + mEditedFile = file; + IProject project = mEditedFile.getProject(); + mResources = ResourceManager.getInstance().getProjectResources(project); + + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); + mConfiguration.setEditedConfig(resFolder.getConfiguration()); + + mDisableUpdates++; // we do not want to trigger onXXXChange when setting + // new values in the widgets. + + try { + // only attempt to do anything if the SDK and targets are loaded. + LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); + if (sdkStatus == LoadStatus.LOADED) { + setVisible(true); + + LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, + null /*project*/); + + if (targetStatus == LoadStatus.LOADED) { + + // update the current config selection to make sure it's + // compatible with the new file + ConfigurationMatcher matcher = new ConfigurationMatcher(this); + matcher.adaptConfigSelection(true /*needBestMatch*/); + mConfiguration.syncFolderConfig(); + + // update the string showing the config value + selectConfiguration(mConfiguration.getEditedConfig()); + updateActivity(); + } + } + } finally { + mDisableUpdates--; + } + } + + /** + * Updates the UI with a new file that was opened in response to a config change. + * @param file the file being opened. + * + * @see #replaceFile(IFile) + */ + public void changeFileOnNewConfig(IFile file) { + mEditedFile = file; + IProject project = mEditedFile.getProject(); + mResources = ResourceManager.getInstance().getProjectResources(project); + + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); + FolderConfiguration config = resFolder.getConfiguration(); + mConfiguration.setEditedConfig(config); + + // All that's needed is to update the string showing the config value + // (since the config combo settings chosen by the user). + selectConfiguration(config); + } + + /** + * Resets the configuration chooser to reflect the given file configuration. This is + * intended to be used by the "Show Included In" functionality where the user has + * picked a non-default configuration (such as a particular landscape layout) and the + * configuration chooser must be switched to a landscape layout. This method will + * trigger a model change. + * <p> + * This will NOT trigger a redraw event! + * <p> + * FIXME: We are currently setting the configuration file to be the configuration for + * the "outer" (the including) file, rather than the inner file, which is the file the + * user is actually editing. We need to refine this, possibly with a way for the user + * to choose which configuration they are editing. And in particular, we should be + * filtering the configuration chooser to only show options in the outer configuration + * that are compatible with the inner included file. + * + * @param file the file to be configured + */ + public void resetConfigFor(IFile file) { + setFile(file); + + IFolder parent = (IFolder) mEditedFile.getParent(); + ResourceFolder resFolder = mResources.getResourceFolder(parent); + if (resFolder != null) { + mConfiguration.setEditedConfig(resFolder.getConfiguration()); + } else { + mConfiguration.setEditedConfig(FolderConfiguration.getConfig( + parent.getName().split(RES_QUALIFIER_SEP))); + } + + onXmlModelLoaded(); + } + + + /** + * Sets the current configuration to match the given folder configuration, + * the given theme name, the given device and device state. + * + * @param configuration new folder configuration to use + */ + public void setConfiguration(@NonNull Configuration configuration) { + if (mClient != null) { + mClient.aboutToChange(CHANGED_ALL); + } + + Configuration oldConfiguration = mConfiguration; + mConfiguration = configuration; + + if (mClient != null) { + mClient.changed(CHANGED_ALL); + } + + selectTheme(configuration.getTheme()); + selectLocale(configuration.getLocale()); + selectDevice(configuration.getDevice()); + selectDeviceState(configuration.getDeviceState()); + selectTarget(configuration.getTarget()); + selectActivity(configuration.getActivity()); + + // This may be a second refresh after triggered by theme above + if (mClient != null) { + boolean accepted = mClient.changed(CHANGED_ALL); + if (!accepted) { + configuration = oldConfiguration; + selectTheme(configuration.getTheme()); + selectLocale(configuration.getLocale()); + selectDevice(configuration.getDevice()); + selectDeviceState(configuration.getDeviceState()); + selectTarget(configuration.getTarget()); + selectActivity(configuration.getActivity()); + return; + } + } + + saveConstraints(); + } + + /** + * Responds to the event that the basic SDK information finished loading. + * @param target the possibly new target object associated with the file being edited (in case + * the SDK path was changed). + */ + public void onSdkLoaded(IAndroidTarget target) { + // a change to the SDK means that we need to check for new/removed devices. + mSdkChanged = true; + + // store the new target. + mProjectTarget = target; + + mDisableUpdates++; // we do not want to trigger onXXXChange when setting + // new values in the widgets. + try { + // 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(); + } finally { + mDisableUpdates--; + } + } + + /** + * Responds to the XML model being loaded, either the first time or when the + * Target/SDK changes. + * <p> + * This initializes the UI, either with the first compatible configuration + * found, or it will attempt to restore a configuration if one is found to + * have been saved in the file persistent storage. + * <p> + * If the SDK or target are not loaded, nothing will happen (but the method + * must be called back when they are.) + * <p> + * The method automatically handles being called the first time after editor + * creation, or being called after during SDK/Target changes (as long as + * {@link #onSdkLoaded(IAndroidTarget)} is properly called). + * + * @return the target data for the rendering target used to render the + * layout + * + * @see #saveConstraints() + * @see #onSdkLoaded(IAndroidTarget) + */ + public AndroidTargetData onXmlModelLoaded() { + AndroidTargetData targetData = null; + + // only attempt to do anything if the SDK and targets are loaded. + LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); + if (sdkStatus == LoadStatus.LOADED) { + mDisableUpdates++; // we do not want to trigger onXXXChange when setting + + try { + // init the devices if needed (new SDK or first time going through here) + if (mSdkChanged) { + initDevices(); + initTargets(); + mSdkChanged = false; + } + + IProject project = mEditedFile.getProject(); + + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + mProjectTarget = currentSdk.getTarget(project); + } + + LoadStatus targetStatus = LoadStatus.FAILED; + if (mProjectTarget != null) { + targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); + initTargets(); + } + + if (targetStatus == LoadStatus.LOADED) { + setVisible(true); + if (mResources == null) { + mResources = ResourceManager.getInstance().getProjectResources(project); + } + if (mConfiguration.getEditedConfig() == null) { + IFolder parent = (IFolder) mEditedFile.getParent(); + ResourceFolder resFolder = mResources.getResourceFolder(parent); + if (resFolder != null) { + mConfiguration.setEditedConfig(resFolder.getConfiguration()); + } else { + mConfiguration.setEditedConfig(FolderConfiguration.getConfig( + parent.getName().split(RES_QUALIFIER_SEP))); + } + } + + 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); + } + + // Load locale list. This must be run after we initialize the + // configuration above, since it attempts to sync the UI with + // the value loaded into the configuration. + updateLocales(); + + // If the current state was loaded from the persistent storage, we update the + // UI with it and then try to adapt it (which will handle incompatible + // configuration). + // Otherwise, just look for the first compatible configuration. + ConfigurationMatcher matcher = new ConfigurationMatcher(this); + if (loadedConfigData) { + // first make sure we have the config to adapt + selectDevice(mConfiguration.getDevice()); + selectDeviceState(mConfiguration.getDeviceState()); + mConfiguration.syncFolderConfig(); + + matcher.adaptConfigSelection(false); + + IAndroidTarget target = mConfiguration.getTarget(); + selectTarget(target); + targetData = Sdk.getCurrent().getTargetData(target); + } else { + matcher.findAndSetCompatibleConfig(false); + + // Default to modern layout lib + IProject p = mEditedFile.getProject(); + IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(p); + if (target != null) { + targetData = Sdk.getCurrent().getTargetData(target); + selectTarget(target); + } + } + + // Update activity: This is done before updateThemes() since + // the themes selection can depend on the currently selected activity + // (e.g. when there are manifest registrations for the theme to use + // for a given activity) + updateActivity(); + + // Update themes. This is done after updating the devices above, + // since we want to look at the chosen device size to decide + // what the default theme (for example, with Honeycomb we choose + // Holo as the default theme but only if the screen size is XLARGE + // (and of course only if the manifest does not specify another + // default theme). + updateThemes(); + + // update the string showing the config value + selectConfiguration(mConfiguration.getEditedConfig()); + + // compute the final current config + mConfiguration.syncFolderConfig(); + } + } finally { + mDisableUpdates--; + } + } + + return targetData; + } + + /** + * An alternate layout for this layout has been created. This means that the + * current layout may no longer be a best fit. However, since we support multiple + * layouts being open at the same time, we need to adjust the current configuration + * back to something where this layout <b>is</b> a best match. + */ + public void onAlternateLayoutCreated() { + IFile best = ConfigurationMatcher.getBestFileMatch(this); + if (best != null && !best.equals(mEditedFile)) { + ConfigurationMatcher matcher = new ConfigurationMatcher(this); + matcher.adaptConfigSelection(true /*needBestMatch*/); + mConfiguration.syncFolderConfig(); + if (mClient != null) { + mClient.changed(CHANGED_ALL); + } + } + } + + /** + * Loads the list of {@link Device}s and inits the UI with it. + */ + private void initDevices() { + final Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + mDeviceList = sdk.getDevices(); + DeviceManager manager = sdk.getDeviceManager(); + // This method can be called more than once, so avoid duplicate entries + manager.unregisterListener(this); + manager.registerListener(this); + } 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() { + 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); + + // the rendering target is the same as the project. + renderingTarget = mProjectTarget; + } else { + selectTarget(match); + + // set the rendering target to the new object. + renderingTarget = match; + } + } + } + + /** Update the toolbar whenever a label has changed, to not only + * cause the layout in the current toolbar to update, but to possibly + * wrap the toolbars and update the layout of the surrounding area. + */ + private void resizeToolBar() { + Point size = getSize(); + Point newSize = computeSize(size.x, SWT.DEFAULT, true); + setSize(newSize); + Composite parent = getParent(); + parent.layout(); + parent.redraw(); + } + + + Image getOrientationIcon(ScreenOrientation orientation, boolean flip) { + IconFactory icons = IconFactory.getInstance(); + switch (orientation) { + case LANDSCAPE: + return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); + case SQUARE: + return icons.getIcon(ICON_SQUARE); + case PORTRAIT: + default: + return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); + } + } + + ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) { + IconFactory icons = IconFactory.getInstance(); + switch (orientation) { + case LANDSCAPE: + return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); + case SQUARE: + return icons.getImageDescriptor(ICON_SQUARE); + case PORTRAIT: + default: + return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); + } + } + + @NonNull + ScreenOrientation getOrientation(State state) { + FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state); + ScreenOrientation orientation = null; + if (config != null && config.getScreenOrientationQualifier() != null) { + orientation = config.getScreenOrientationQualifier().getValue(); + } + + if (orientation == null) { + orientation = ScreenOrientation.PORTRAIT; + } + + return orientation; + } + + /** + * Stores the current config selection into the edited file such that we can + * bring it back the next time this layout is opened. + */ + public void saveConstraints() { + String description = mConfiguration.toPersistentString(); + AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, description); + } + + // ---- Setting the current UI state ---- + + void selectDeviceState(@Nullable State state) { + assert isUiThread(); + try { + mDisableUpdates++; + mOrientationCombo.setData(state); + + State nextState = mConfiguration.getNextDeviceState(state); + mOrientationCombo.setImage(getOrientationIcon(getOrientation(state), + nextState != state)); + } finally { + mDisableUpdates--; + } + } + + void selectTarget(IAndroidTarget target) { + assert isUiThread(); + try { + mDisableUpdates++; + mTargetCombo.setData(target); + String label = getRenderingTargetLabel(target, true); + mTargetCombo.setText(label); + resizeToolBar(); + } finally { + mDisableUpdates--; + } + } + + /** + * Selects a given {@link Device} in the device combo, if it is found. + * @param device the device to select + * @return true if the device was found. + */ + boolean selectDevice(@Nullable Device device) { + assert isUiThread(); + try { + mDisableUpdates++; + mDeviceCombo.setData(device); + if (device != null) { + mDeviceCombo.setText(getDeviceLabel(device, true)); + } else { + mDeviceCombo.setText("Device"); + } + resizeToolBar(); + } finally { + mDisableUpdates--; + } + + return false; + } + + void selectActivity(@Nullable String fqcn) { + assert isUiThread(); + try { + mDisableUpdates++; + if (fqcn != null) { + mActivityCombo.setData(fqcn); + String label = getActivityLabel(fqcn, true); + mActivityCombo.setText(label); + } else { + mActivityCombo.setText("(Select)"); + } + resizeToolBar(); + } finally { + mDisableUpdates--; + } + } + + void selectTheme(@Nullable String theme) { + assert isUiThread(); + try { + mDisableUpdates++; + assert theme == null || theme.startsWith(STYLE_RESOURCE_PREFIX) + || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme; + mThemeCombo.setData(theme); + if (theme != null) { + mThemeCombo.setText(getThemeLabel(theme, true)); + } else { + // FIXME eclipse claims this is dead code. + mThemeCombo.setText("(Set Theme)"); + } + resizeToolBar(); + } finally { + mDisableUpdates--; + } + } + + void selectLocale(@Nullable Locale locale) { + assert isUiThread(); + try { + mDisableUpdates++; + mLocaleCombo.setData(locale); + String label = Strings.nullToEmpty(getLocaleLabel(this, locale, true)); + mLocaleCombo.setText(label); + + Image image = getFlagImage(locale); + mLocaleCombo.setImage(image); + + resizeToolBar(); + } finally { + mDisableUpdates--; + } + } + + @NonNull + Image getFlagImage(@Nullable Locale locale) { + if (locale != null) { + return locale.getFlagImage(); + } + + return LocaleManager.getGlobeIcon(); + } + + private void selectConfiguration(FolderConfiguration fileConfig) { + assert isUiThread(); + try { + String current = mEditedFile.getParent().getName(); + if (current.equals(FD_RES_LAYOUT)) { + current = "default"; + } + + // Pretty things up a bit + //if (current == null || current.equals("default")) { + // current = "Default Configuration"; + //} + mConfigCombo.setText(current); + resizeToolBar(); + } finally { + mDisableUpdates--; + } + } + + /** + * Finds a locale matching the config from a file. + * + * @param language the language qualifier or null if none is set. + * @param region the region qualifier or null if none is set. + * @return true if there was a change in the combobox as a result of + * applying the locale + */ + private boolean setLocale(@Nullable Locale locale) { + boolean changed = !Objects.equal(mConfiguration.getLocale(), locale); + selectLocale(locale); + + return changed; + } + + // ---- Creating UI labels ---- + + /** + * Returns a suitable label to use to display the given activity + * + * @param fqcn the activity class to look up a label for + * @param brief if true, generate a brief label (suitable for a toolbar + * button), otherwise a fuller name (suitable for a menu item) + * @return the label + */ + public static String getActivityLabel(String fqcn, boolean brief) { + if (brief) { + String label = fqcn; + int packageIndex = label.lastIndexOf('.'); + if (packageIndex != -1) { + label = label.substring(packageIndex + 1); + } + int innerClass = label.lastIndexOf('$'); + if (innerClass != -1) { + label = label.substring(innerClass + 1); + } + + // Also strip out the "Activity" or "Fragment" common suffix + // if this is a long name + if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix + label = label.substring(0, label.length() - 8); + } else if (label.endsWith("Fragment") && label.length() > 8 + 12) { + label = label.substring(0, label.length() - 8); + } + + return label; + } + + return fqcn; + } + + /** + * Returns a suitable label to use to display the given theme + * + * @param theme the theme to produce a label for + * @param brief if true, generate a brief label (suitable for a toolbar + * button), otherwise a fuller name (suitable for a menu item) + * @return the label + */ + public static String getThemeLabel(String theme, boolean brief) { + theme = ResourceHelper.styleToTheme(theme); + + if (brief) { + int index = theme.lastIndexOf('.'); + if (index < theme.length() - 1) { + return theme.substring(index + 1); + } + } + return theme; + } + + /** + * Returns a suitable label to use to display the given rendering target + * + * @param target the target to produce a label for + * @param brief if true, generate a brief label (suitable for a toolbar + * button), otherwise a fuller name (suitable for a menu item) + * @return the label + */ + public static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) { + if (target == null) { + return "<null>"; + } + + AndroidVersion version = target.getVersion(); + + if (brief) { + if (target.isPlatform()) { + return Integer.toString(version.getApiLevel()); + } else { + return target.getName() + ':' + Integer.toString(version.getApiLevel()); + } + } + + String label = String.format("API %1$d: %2$s", + version.getApiLevel(), + target.getShortClasspathName()); + + return label; + } + + /** + * Returns a suitable label to use to display the given device + * + * @param device the device to produce a label for + * @param brief if true, generate a brief label (suitable for a toolbar + * button), otherwise a fuller name (suitable for a menu item) + * @return the label + */ + public static String getDeviceLabel(@Nullable Device device, boolean brief) { + if (device == null) { + return ""; + } + String name = device.getName(); + + if (brief) { + // Produce a really brief summary of the device name, suitable for + // use in the narrow space available in the toolbar for example + int nexus = name.indexOf("Nexus"); //$NON-NLS-1$ + if (nexus != -1) { + int begin = name.indexOf('('); + if (begin != -1) { + begin++; + int end = name.indexOf(')', begin); + if (end != -1) { + return name.substring(begin, end).trim(); + } + } + } + } + + return name; + } + + /** + * Returns a suitable label to use to display the given locale + * + * @param chooser the chooser, if known + * @param locale the locale to look up a label for + * @param brief if true, generate a brief label (suitable for a toolbar + * button), otherwise a fuller name (suitable for a menu item) + * @return the label + */ + @Nullable + public static String getLocaleLabel( + @Nullable ConfigurationChooser chooser, + @Nullable Locale locale, + boolean brief) { + if (locale == null) { + return null; + } + + if (!locale.hasLanguage()) { + if (brief) { + // Just use the icon + return ""; + } + + boolean hasLocale = false; + ResourceRepository projectRes = chooser != null ? chooser.mClient.getProjectResources() + : null; + if (projectRes != null) { + hasLocale = projectRes.getLanguages().size() > 0; + } + + if (hasLocale) { + return "Other"; + } else { + return "Any"; + } + } + + String languageCode = locale.language.getValue(); + String languageName = LocaleManager.getLanguageName(languageCode); + + if (!locale.hasRegion()) { + // TODO: Make the region string use "Other" instead of "Any" if + // there is more than one region for a given language + //if (regions.size() > 0) { + // return String.format("%1$s / Other", language); + //} else { + // return String.format("%1$s / Any", language); + //} + if (!brief && languageName != null) { + return String.format("%1$s (%2$s)", languageName, languageCode); + } else { + return languageCode; + } + } else { + String regionCode = locale.region.getValue(); + if (!brief && languageName != null) { + String regionName = LocaleManager.getRegionName(regionCode); + if (regionName != null) { + return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode, + regionName, regionCode); + } + return String.format("%1$s (%2$s) in %3$s", languageName, languageCode, + regionCode); + } + return String.format("%1$s / %2$s", languageCode, regionCode); + } + } + + // ---- Implements DevicesChangeListener ---- + + @Override + public void onDevicesChange() { + final Sdk sdk = Sdk.getCurrent(); + mDeviceList = sdk.getDevices(); + } + + // ---- Reacting to UI changes ---- + + /** + * Called when the selection of the device combo changes. + */ + void onDeviceChange() { + // because changing the content of a combo triggers a change event, respect the + // mDisableUpdates flag + if (mDisableUpdates > 0) { + return; + } + + // Attempt to preserve the device state + String stateName = null; + Device prevDevice = mConfiguration.getDevice(); + State prevState = mConfiguration.getDeviceState(); + Device device = (Device) mDeviceCombo.getData(); + if (prevDevice != null && prevState != null && device != null) { + // get the previous config, so that we can look for a close match + FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState); + if (oldConfig != null) { + stateName = ConfigurationMatcher.getClosestMatch(oldConfig, device.getAllStates()); + } + } + mConfiguration.setDevice(device, true); + State newState = Configuration.getState(device, stateName); + mConfiguration.setDeviceState(newState, true); + selectDeviceState(newState); + mConfiguration.syncFolderConfig(); + + // Notify + boolean accepted = mClient.changed(CHANGED_DEVICE | CHANGED_DEVICE_CONFIG); + if (!accepted) { + mConfiguration.setDevice(prevDevice, true); + mConfiguration.setDeviceState(prevState, true); + mConfiguration.syncFolderConfig(); + selectDevice(prevDevice); + selectDeviceState(prevState); + return; + } + + saveConstraints(); + } + + /** + * Called when the device config selection changes. + */ + void onDeviceConfigChange() { + // because changing the content of a combo triggers a change event, respect the + // mDisableUpdates flag + if (mDisableUpdates > 0) { + return; + } + + State prev = mConfiguration.getDeviceState(); + State state = (State) mOrientationCombo.getData(); + mConfiguration.setDeviceState(state, false); + + if (mClient != null) { + boolean accepted = mClient.changed(CHANGED_DEVICE | CHANGED_DEVICE_CONFIG); + if (!accepted) { + mConfiguration.setDeviceState(prev, false); + selectDeviceState(prev); + return; + } + } + + saveConstraints(); + } + + /** + * Call back for language combo selection + */ + void onLocaleChange() { + // because mLocaleList triggers onLocaleChange at each modification, the filling + // of the combo with data will trigger notifications, and we don't want that. + if (mDisableUpdates > 0) { + return; + } + + Locale prev = mConfiguration.getLocale(); + Locale locale = (Locale) mLocaleCombo.getData(); + if (locale == null) { + locale = Locale.ANY; + } + mConfiguration.setLocale(locale, false); + + if (mClient != null) { + boolean accepted = mClient.changed(CHANGED_LOCALE); + if (!accepted) { + mConfiguration.setLocale(prev, false); + selectLocale(prev); + } + } + + // Store locale project-wide setting + mConfiguration.saveRenderState(); + } + + + void onThemeChange() { + if (mDisableUpdates > 0) { + return; + } + + String prev = mConfiguration.getTheme(); + mConfiguration.setTheme((String) mThemeCombo.getData()); + + if (mClient != null) { + boolean accepted = mClient.changed(CHANGED_THEME); + if (!accepted) { + mConfiguration.setTheme(prev); + selectTheme(prev); + return; + } + } + + saveConstraints(); + } + + void notifyFolderConfigChanged() { + if (mDisableUpdates > 0 || mClient == null) { + return; + } + + if (mClient.changed(CHANGED_FOLDER)) { + saveConstraints(); + } + } + + void onSelectActivity() { + if (mDisableUpdates > 0) { + return; + } + + String activity = (String) mActivityCombo.getData(); + mConfiguration.setActivity(activity); + + if (activity == null) { + return; + } + + // See if there is a default theme assigned to this activity, and if so, use it + ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); + Map<String, String> activityThemes = manifest.getActivityThemes(); + String preferred = activityThemes.get(activity); + if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) { + // Yes, switch to it + selectTheme(preferred); + onThemeChange(); + } + + // Persist in XML + if (mClient != null) { + mClient.setActivity(activity); + } + + saveConstraints(); + } + + /** + * Call back for api level combo selection + */ + void onRenderingTargetChange() { + // because mApiCombo triggers onApiLevelChange at each modification, the filling + // of the combo with data will trigger notifications, and we don't want that. + if (mDisableUpdates > 0) { + return; + } + + IAndroidTarget prevTarget = mConfiguration.getTarget(); + String prevTheme = mConfiguration.getTheme(); + + int changeFlags = 0; + + // tell the listener a new rendering target is being set. Need to do this before updating + // mRenderingTarget. + if (prevTarget != null) { + changeFlags |= CHANGED_RENDER_TARGET; + mClient.aboutToChange(changeFlags); + } + + IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData(); + mConfiguration.setTarget(target, true); + + // force a theme update to reflect the new rendering target. + // This must be done after computeCurrentConfig since it'll depend on the currentConfig + // to figure out the theme list. + String oldTheme = mConfiguration.getTheme(); + updateThemes(); + // updateThemes may change the theme (based on theme availability in the new rendering + // target) so mark theme change if necessary + if (!Objects.equal(oldTheme, mConfiguration.getTheme())) { + changeFlags |= CHANGED_THEME; + } + + if (target != null) { + changeFlags |= CHANGED_RENDER_TARGET; + changeFlags |= CHANGED_FOLDER; // In case we added a -vNN qualifier + } + + // Store project-wide render-target setting + mConfiguration.saveRenderState(); + + mConfiguration.syncFolderConfig(); + + if (mClient != null) { + boolean accepted = mClient.changed(changeFlags); + if (!accepted) { + mConfiguration.setTarget(prevTarget, true); + mConfiguration.setTheme(prevTheme); + mConfiguration.syncFolderConfig(); + selectTheme(prevTheme); + selectTarget(prevTarget); + } + } + } + + /** + * Syncs this configuration to the project wide locale and render target settings. The + * locale may ignore the project-wide setting if it is a locale-specific + * configuration. + * + * @return true if one or both of the toggles were changed, false if there were no + * changes + */ + public boolean syncRenderState() { + if (mConfiguration.getEditedConfig() == null) { + // Startup; ignore + return false; + } + + boolean renderTargetChanged = false; + + // When a page is re-activated, force the toggles to reflect the current project + // state + + Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this); + + int changeFlags = 0; + // Only sync the locale if this layout is not already a locale-specific layout! + if (pair != null && !mConfiguration.isLocaleSpecificLayout()) { + Locale locale = pair.getFirst(); + if (locale != null) { + boolean localeChanged = setLocale(locale); + if (localeChanged) { + changeFlags |= CHANGED_LOCALE; + } + } else { + locale = Locale.ANY; + } + mConfiguration.setLocale(locale, true); + } + + // Sync render target + IAndroidTarget configurationTarget = mConfiguration.getTarget(); + IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget; + if (target != null && configurationTarget != target) { + if (mClient != null && configurationTarget != null) { + changeFlags |= CHANGED_RENDER_TARGET; + mClient.aboutToChange(changeFlags); + } + + mConfiguration.setTarget(target, true); + selectTarget(target); + renderTargetChanged = true; + } + + // Neither locale nor render target changed: nothing to do + if (changeFlags == 0) { + return false; + } + + // Update the locale and/or the render target. This code contains a logical + // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined + // such that we don't duplicate work. + + // Compute the new configuration; we want to do this both for locale changes + // and for render targets. + mConfiguration.syncFolderConfig(); + changeFlags |= CHANGED_FOLDER; // in case we added/remove a -v<NN> qualifier + + if (renderTargetChanged) { + // force a theme update to reflect the new rendering target. + // This must be done after computeCurrentConfig since it'll depend on the currentConfig + // to figure out the theme list. + updateThemes(); + } + + if (mClient != null) { + mClient.changed(changeFlags); + } + + return true; + } + + // ---- Populate data structures with themes, locales, etc ---- + + /** + * Updates the internal list of themes. + */ + private void updateThemes() { + if (mClient == null) { + return; // can't do anything without it. + } + + ResourceRepository frameworkRes = mClient.getFrameworkResources( + mConfiguration.getTarget()); + + mDisableUpdates++; + + try { + if (mEditedFile != null) { + String theme = mConfiguration.getTheme(); + if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) { + mConfiguration.setTheme(null); + computePreferredTheme(); + } + assert mConfiguration.getTheme() != null; + } + + mThemeList.clear(); + + ArrayList<String> themes = new ArrayList<String>(); + ResourceRepository projectRes = mClient.getProjectResources(); + // in cases where the opened file is not linked to a project, this could be null. + if (projectRes != null) { + // get the configured resources for the project + Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = + mClient.getConfiguredProjectResources(); + + if (configuredProjectRes != null) { + // get the styles. + Map<String, ResourceValue> styleMap = configuredProjectRes.get( + ResourceType.STYLE); + + if (styleMap != null) { + // collect the themes out of all the styles, ie styles that extend, + // directly or indirectly a platform theme. + for (ResourceValue value : styleMap.values()) { + if (isTheme(value, styleMap, null)) { + String theme = value.getName(); + themes.add(theme); + } + } + + Collections.sort(themes); + + for (String theme : themes) { + if (!theme.startsWith(PREFIX_RESOURCE_REF)) { + theme = STYLE_RESOURCE_PREFIX + theme; + } + mThemeList.add(theme); + } + } + } + themes.clear(); + } + + // get the themes, and languages from the Framework. + if (frameworkRes != null) { + // get the configured resources for the framework + Map<ResourceType, Map<String, ResourceValue>> frameworResources = + frameworkRes.getConfiguredResources(mConfiguration.getFullConfig()); + + if (frameworResources != null) { + // get the styles. + Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE); + + // collect the themes out of all the styles. + for (ResourceValue value : styles.values()) { + String name = value.getName(); + if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$ + themes.add(value.getName()); + } + } + + // sort them and add them to the combo + Collections.sort(themes); + + for (String theme : themes) { + if (!theme.startsWith(PREFIX_RESOURCE_REF)) { + theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; + } + mThemeList.add(theme); + } + + themes.clear(); + } + } + + // Migration: In the past we didn't store the style prefix in the settings; + // this meant we might lose track of whether the theme is a project style + // or a framework style. For now we need to migrate. Search through the + // theme list until we have a match + String theme = mConfiguration.getTheme(); + if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) { + String projectStyle = STYLE_RESOURCE_PREFIX + theme; + String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme; + for (String t : mThemeList) { + if (t.equals(projectStyle)) { + mConfiguration.setTheme(projectStyle); + break; + } else if (t.equals(frameworkStyle)) { + mConfiguration.setTheme(frameworkStyle); + break; + } + } + } + + // TODO: Handle the case where you have a theme persisted that isn't available?? + // We could look up mConfiguration.theme and make sure it appears in the list! And if + // not, picking one. + selectTheme(mConfiguration.getTheme()); + } finally { + mDisableUpdates--; + } + } + + private void updateActivity() { + if (mEditedFile != null) { + String preferred = getPreferredActivity(mEditedFile); + selectActivity(preferred); + } + } + + /** + * Updates the locale combo. + * This must be called from the UI thread. + */ + public void updateLocales() { + if (mClient == null) { + return; // can't do anything w/o it. + } + + mDisableUpdates++; + + try { + mLocaleList.clear(); + + SortedSet<String> languages = null; + + // get the languages from the project. + ResourceRepository projectRes = mClient.getProjectResources(); + + // in cases where the opened file is not linked to a project, this could be null. + if (projectRes != null) { + // now get the languages from the project. + languages = projectRes.getLanguages(); + + for (String language : languages) { + LanguageQualifier langQual = new LanguageQualifier(language); + + // find the matching regions and add them + SortedSet<String> regions = projectRes.getRegions(language); + for (String region : regions) { + RegionQualifier regionQual = new RegionQualifier(region); + mLocaleList.add(Locale.create(langQual, regionQual)); + } + + // now the entry for the other regions the language alone + // create a region qualifier that will never be matched by qualified resources. + mLocaleList.add(Locale.create(langQual)); + } + } + + // create language/region qualifier that will never be matched by qualified resources. + mLocaleList.add(Locale.ANY); + + Locale locale = mConfiguration.getLocale(); + setLocale(locale); + } finally { + mDisableUpdates--; + } + } + + /** Returns the preferred theme, or null */ + @Nullable + String computePreferredTheme() { + if (mClient == null) { + return null; + } + + IProject project = mEditedFile.getProject(); + ManifestInfo manifest = ManifestInfo.get(project); + + // Look up the screen size for the current state + ScreenSize screenSize = null; + Device device = mConfiguration.getDevice(); + if (device != null) { + List<State> states = device.getAllStates(); + for (State state : states) { + FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state); + if (folderConfig != null) { + ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); + screenSize = qualifier.getValue(); + break; + } + } + } + + // Look up the default/fallback theme to use for this project (which + // depends on the screen size when no particular theme is specified + // in the manifest) + String defaultTheme = manifest.getDefaultTheme(mConfiguration.getTarget(), screenSize); + + String preferred = defaultTheme; + if (mConfiguration.getTheme() == null) { + // If we are rendering a layout in included context, pick the theme + // from the outer layout instead + + String activity = mConfiguration.getActivity(); + if (activity != null) { + Map<String, String> activityThemes = manifest.getActivityThemes(); + preferred = activityThemes.get(activity); + } + if (preferred == null) { + preferred = defaultTheme; + } + mConfiguration.setTheme(preferred); + } + + return preferred; + } + + @Nullable + private String getPreferredActivity(@NonNull IFile file) { + // Store/restore the activity context in the config state to help with + // performance if for some reason we can't write it into the XML file and to + // avoid having to open the model below + if (mConfiguration.getActivity() != null) { + return mConfiguration.getActivity(); + } + + IProject project = file.getProject(); + + // Look up from XML file + Document document = DomUtilities.getDocument(file); + if (document != null) { + Element element = document.getDocumentElement(); + if (element != null) { + String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT); + if (activity != null && !activity.isEmpty()) { + if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$ + ManifestInfo manifest = ManifestInfo.get(project); + String pkg = manifest.getPackage(); + if (!pkg.isEmpty()) { + if (activity.startsWith(".")) { //$NON-NLS-1$ + activity = pkg + activity; + } else { + activity = activity + '.' + pkg; + } + } + } + + mConfiguration.setActivity(activity); + saveConstraints(); + return activity; + } + } + } + + // No, not available there: try to infer it from the code index + String includedIn = null; + Reference includedWithin = mClient.getIncludedWithin(); + if (mClient != null && includedWithin != null) { + includedIn = includedWithin.getName(); + } + + ManifestInfo manifest = ManifestInfo.get(project); + String pkg = manifest.getPackage(); + String layoutName = ResourceHelper.getLayoutName(mEditedFile); + + // If we are rendering a layout in included context, pick the theme + // from the outer layout instead + if (includedIn != null) { + layoutName = includedIn; + } + + String activity = ManifestInfo.guessActivity(project, layoutName, pkg); + + if (activity == null) { + List<String> activities = ManifestInfo.getProjectActivities(project); + if (activities.size() == 1) { + activity = activities.get(0); + } + } + + if (activity != null) { + mConfiguration.setActivity(activity); + saveConstraints(); + return activity; + } + + // TODO: Do anything else, such as pick the first activity found? + // Or just leave some default label instead? + // Also, figure out what to store in the mState so I don't keep trying + + return null; + } + + /** + * Returns whether the given <var>style</var> is a theme. + * This is done by making sure the parent is a theme. + * @param value the style to check + * @param styleMap the map of styles for the current project. Key is the style name. + * @param seen the map of styles we have already processed (or null if not yet + * initialized). Only the keys are significant (since there is no IdentityHashSet). + * @return True if the given <var>style</var> is a theme. + */ + private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap, + IdentityHashMap<ResourceValue, Boolean> seen) { + if (value instanceof StyleResourceValue) { + StyleResourceValue style = (StyleResourceValue)value; + + boolean frameworkStyle = false; + String parentStyle = style.getParentStyle(); + if (parentStyle == null) { + // if there is no specified parent style we look an implied one. + // For instance 'Theme.light' is implied child style of 'Theme', + // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' + String name = style.getName(); + int index = name.lastIndexOf('.'); + if (index != -1) { + parentStyle = name.substring(0, index); + } + } else { + // remove the useless @ if it's there + if (parentStyle.startsWith("@")) { + parentStyle = parentStyle.substring(1); + } + + // check for framework identifier. + if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) { + frameworkStyle = true; + parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length()); + } + + // at this point we could have the format style/<name>. we want only the name + if (parentStyle.startsWith("style/")) { + parentStyle = parentStyle.substring("style/".length()); + } + } + + if (parentStyle != null) { + if (frameworkStyle) { + // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' + return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); + } else { + // if it's a project style, we check this is a theme. + ResourceValue parentValue = styleMap.get(parentStyle); + + // also prevent stack overflow in case the dev mistakenly declared + // the parent of the style as the style itself. + if (parentValue != null && !parentValue.equals(value)) { + if (seen == null) { + seen = new IdentityHashMap<ResourceValue, Boolean>(); + seen.put(value, Boolean.TRUE); + } else if (seen.containsKey(parentValue)) { + return false; + } + seen.put(parentValue, Boolean.TRUE); + return isTheme(parentValue, styleMap, seen); + } + } + } + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java new file mode 100644 index 0000000..a7c26d4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.resources.NightMode; +import com.android.resources.ResourceType; +import com.android.resources.UiMode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.State; + +import java.util.Map; + +/** + * Interface implemented by clients who embed a {@link ConfigurationChooser}. + */ +public interface ConfigurationClient { + /** The {@link FolderConfiguration} in the configuration has changed */ + public static final int CHANGED_FOLDER = 1 << 0; + /** The {@link Device} in the configuration has changed */ + public static final int CHANGED_DEVICE = 1 << 1; + /** The {@link State} in the configuration has changed */ + public static final int CHANGED_DEVICE_CONFIG = 1 << 2; + /** The theme in the configuration has changed */ + public static final int CHANGED_THEME = 1 << 3; + /** The locale in the configuration has changed */ + public static final int CHANGED_LOCALE = 1 << 4; + /** The rendering {@link IAndroidTarget} in the configuration has changed */ + public static final int CHANGED_RENDER_TARGET = 1 << 5; + /** The {@link NightMode} in the configuration has changed */ + public static final int CHANGED_NIGHT_MODE = 1 << 6; + /** The {@link UiMode} in the configuration has changed */ + public static final int CHANGED_UI_MODE = 1 << 7; + + /** Everything has changed */ + public static final int CHANGED_ALL = 0xFFFF; + + /** + * The configuration is about to be changed. + * + * @param flags details about what changed; consult the {@code CHANGED_} flags + * such as {@link #CHANGED_DEVICE}, {@link #CHANGED_LOCALE}, etc. + */ + void aboutToChange(int flags); + + /** + * The configuration has changed. If the client returns false, it means that + * the change was rejected. This typically means that changing the + * configuration in this particular way makes a configuration which has a + * better file match than the current client's file, so it will open that + * file to edit the new configuration -- and the current configuration + * should go back to editing the state prior to this change. + * + * @param flags details about what changed; consult the {@code CHANGED_} flags + * such as {@link #CHANGED_DEVICE}, {@link #CHANGED_LOCALE}, etc. + * @return true if the change was accepted, false if it was rejected. + */ + boolean changed(int flags); + + /** + * Compute the project resources + * + * @return the project resources as a {@link ResourceRepository} + */ + @Nullable + ResourceRepository getProjectResources(); + + /** + * Compute the framework resources + * + * @return the project resources as a {@link ResourceRepository} + */ + @Nullable + ResourceRepository getFrameworkResources(); + + /** + * Compute the framework resources for the given Android API target + * + * @param target the target to look up framework resources for + * @return the project resources as a {@link ResourceRepository} + */ + @Nullable + ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target); + + /** + * Returns the configured project resources for the current file and + * configuration + * + * @return resource type maps to names to resource values + */ + @NonNull + Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources(); + + /** + * Returns the configured framework resources for the current file and + * configuration + * + * @return resource type maps to names to resource values + */ + @NonNull + Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources(); + + /** + * If the current layout is an included layout rendered within an outer layout, + * returns the outer layout. + * + * @return the outer including layout, or null + */ + @Nullable + Reference getIncludedWithin(); + + /** + * Called when the "Create" button is clicked. + */ + void createConfigFile(); + + /** + * Called when an associated activity is picked + * + * @param fqcn the fully qualified class name for the associated activity context + */ + void setActivity(@NonNull String fqcn); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java deleted file mode 100644 index 1dec5cd..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java +++ /dev/null @@ -1,3418 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ide.eclipse.adt.internal.editors.layout.configuration; - -import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; -import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.FD_RES_LAYOUT; -import static com.android.SdkConstants.PREFIX_RESOURCE_REF; -import static com.android.SdkConstants.RES_QUALIFIER_SEP; -import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.TOOLS_URI; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.ide.common.api.Rect; -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.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.NightModeQualifier; -import com.android.ide.common.resources.configuration.RegionQualifier; -import com.android.ide.common.resources.configuration.ResourceQualifier; -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.common.resources.configuration.UiModeQualifier; -import com.android.ide.common.resources.configuration.VersionQualifier; -import com.android.ide.common.sdk.LoadStatus; -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.IconFactory; -import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; -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.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.internal.wizards.newxmlfile.AddTranslationDialog; -import com.android.resources.Density; -import com.android.resources.NightMode; -import com.android.resources.ResourceFolderType; -import com.android.resources.ResourceType; -import com.android.resources.ScreenOrientation; -import com.android.resources.ScreenSize; -import com.android.resources.UiMode; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.devices.Device; -import com.android.sdklib.devices.DeviceManager; -import com.android.sdklib.devices.DeviceManager.DevicesChangeListener; -import com.android.sdklib.devices.State; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.util.SparseIntArray; -import com.android.utils.Pair; -import com.google.common.collect.Maps; - -import org.eclipse.core.resources.IContainer; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.QualifiedName; -import org.eclipse.jdt.ui.ISharedImages; -import org.eclipse.jdt.ui.JavaUI; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.DisposeEvent; -import org.eclipse.swt.events.DisposeListener; -import org.eclipse.swt.events.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.events.SelectionListener; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.Rectangle; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Event; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.Menu; -import org.eclipse.swt.widgets.MenuItem; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.swt.widgets.ToolBar; -import org.eclipse.swt.widgets.ToolItem; -import org.eclipse.ui.IEditorPart; -import org.eclipse.ui.PartInitException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeMap; - -/** - * A composite that displays the current configuration displayed in a Graphical Layout Editor. - * <p/> - * The composite has several entry points:<br> - * - {@link #setFile(IFile)}<br> - * Called after the constructor to set the file being edited. Nothing else is performed.<br> - *<br> - * - {@link #onXmlModelLoaded()}<br> - * Called when the XML model is loaded, either the first time or when the Target/SDK changes. - * This initializes the UI, either with the first compatible configuration found, or attempts - * to restore a configuration if one is found to have been saved in the file persistent storage. - * (see {@link #storeState()})<br> - *<br> - * - {@link #replaceFile(IFile)}<br> - * Called when a file, representing the same resource but with a different config is opened<br> - * by the user.<br> - *<br> - * - {@link #changeFileOnNewConfig(IFile)}<br> - * Called when config change triggers the editing of a file with a different config. - *<p/> - * Additionally, the composite can handle the following events.<br> - * - SDK reload. This is when the main SDK is finished loading.<br> - * - Target reload. This is when the target used by the project is the edited file has finished<br> - * loading.<br> - */ -public class ConfigurationComposite extends Composite - implements SelectionListener, DevicesChangeListener, DisposeListener { - public static final String ATTR_CONTEXT = "context"; //$NON-NLS-1$ - private static final String ICON_SQUARE = "square"; //$NON-NLS-1$ - private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$ - private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$ - private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$ - private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$ - private static final String ICON_DISPLAY = "display"; //$NON-NLS-1$ - private static final String ICON_NEW_CONFIG = "newConfig"; //$NON-NLS-1$ - private static final String ICON_THEMES = "themes"; //$NON-NLS-1$ - private static final String ICON_ACTIVITY = "activity"; //$NON-NLS-1$ - private final static String SEP = ":"; //$NON-NLS-1$ - private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ - private final static String MARKER_FRAMEWORK = "-"; //$NON-NLS-1$ - private final static String MARKER_PROJECT = "+"; //$NON-NLS-1$ - - /** - * Setting name for project-wide setting controlling rendering target and locale which - * is shared for all files - */ - public final static QualifiedName NAME_RENDER_STATE = - new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$ - - /** - * Settings name for file-specific configuration preferences, such as which theme or - * device to render the current layout with - */ - public final static QualifiedName NAME_CONFIG_STATE = - new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ - - private final static int LOCALE_LANG = 0; - private final static int LOCALE_REGION = 1; - - private ToolItem mDeviceCombo; - private ToolItem mThemeCombo; - private ToolItem mOrientationCombo; - private ToolItem mLocaleCombo; - private ToolItem mTargetCombo; - private ToolItem mConfigCombo; - private ToolItem mActivityCombo; - - /** updates are disabled if > 0 */ - private int mDisableUpdates = 0; - - private List<Device> mDeviceList = new ArrayList<Device>(); - private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>(); - - private final List<String> mThemeList = new ArrayList<String>(); - - private final List<ResourceQualifier[] > mLocaleList = - new ArrayList<ResourceQualifier[]>(); - - private final ConfigState mState = new ConfigState(); - - private boolean mSdkChanged = false; - private boolean mFirstXmlModelChange = true; - - /** The config listener given to the constructor. Never null. */ - private final IConfigListener mListener; - - /** The device menu listener, so we can remove it when the device lists are updated */ - private Listener mDeviceListener; - - /** The {@link FolderConfiguration} representing the state of the UI controls */ - private final FolderConfiguration mCurrentConfig = new FolderConfiguration(); - - /** The file being edited */ - private IFile mEditedFile; - /** The {@link ProjectResources} for the edited file's project */ - private ProjectResources mResources; - /** The target of the project of the file being edited. */ - private IAndroidTarget mProjectTarget; - /** The target of the project of the file being edited. */ - private IAndroidTarget mRenderingTarget; - /** The {@link FolderConfiguration} being edited. */ - private FolderConfiguration mEditedConfig; - /** Serialized state to use when initializing the configuration after the SDK is loaded */ - private String mInitialState; - - /** - * Interface implemented by the part which owns a {@link ConfigurationComposite}. - * This notifies the owners when the configuration change. - * The owner must also provide methods to provide the configuration that will - * be displayed. - */ - public interface IConfigListener { - /** - * Called when the {@link FolderConfiguration} change. The new config can be queried - * with {@link ConfigurationComposite#getCurrentConfig()}. - */ - void onConfigurationChange(); - - /** - * Called after a device has changed (in addition to {@link #onConfigurationChange} - * getting called) - */ - void onDevicePostChange(); - - /** - * Called when the current theme changes. The theme can be queried with - * {@link ConfigurationComposite#getThemeName()}. - */ - void onThemeChange(); - - /** - * Called when the "Create" button is clicked. - */ - void onCreate(); - - /** - * Called when an associated activity is picked - * - * @param fqcn the fully qualified class name for the associated activity context - */ - void onSetActivity(String fqcn); - - /** - * Called before the rendering target changes. - * @param oldTarget the old rendering target - */ - void onRenderingTargetPreChange(IAndroidTarget oldTarget); - - /** - * Called after the rendering target changes. - * - * @param target the new rendering target - */ - void onRenderingTargetPostChange(IAndroidTarget target); - - ResourceRepository getProjectResources(); - ResourceRepository getFrameworkResources(); - ResourceRepository getFrameworkResources(IAndroidTarget target); - Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources(); - Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources(); - String getIncludedWithin(); - } - - /** - * State of the current config. This is used during UI reset to attempt to return the - * rendering to its original configuration. - */ - private class ConfigState { - Device device; - String stateName; - ResourceQualifier[] locale; - String theme; - // TODO: Need to know if it's the project theme or the framework theme! - /** UI mode. Guaranteed to be non null */ - UiMode uiMode = UiMode.NORMAL; - /** night mode. Guaranteed to be non null */ - NightMode night = NightMode.NOTNIGHT; - /** the version being targeted for rendering */ - IAndroidTarget target; - String activity; - - String getData() { - StringBuilder sb = new StringBuilder(); - if (device != null) { - sb.append(device.getName()); - sb.append(SEP); - if (stateName == null) { - State state= getSelectedDeviceState(); - if (state != null) { - stateName = state.getName(); - } - } - if (stateName != null) { - sb.append(stateName); - } - sb.append(SEP); - if (isLocaleSpecificLayout() && locale != null) { - if (locale[0] != null && locale[1] != null) { - // locale[0]/[1] can be null sometimes when starting Eclipse - sb.append(((LanguageQualifier) locale[0]).getValue()); - sb.append(SEP_LOCALE); - sb.append(((RegionQualifier) locale[1]).getValue()); - } - } - sb.append(SEP); - // Need to escape the theme: if we write the full theme style, then - // we can end up with ":"'s in the string (as in @android:style/Theme) which - // can be mistaken for {@link #SEP}. Instead use {@link #MARKER_FRAMEWORK}. - if (theme != null) { - String themeName = ResourceHelper.styleToTheme(theme); - if (theme.startsWith(STYLE_RESOURCE_PREFIX)) { - sb.append(MARKER_PROJECT); - } else if (theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { - sb.append(MARKER_FRAMEWORK); - } - sb.append(themeName); - } - sb.append(SEP); - if (uiMode != null) { - sb.append(uiMode.getResourceValue()); - } - sb.append(SEP); - if (night != null) { - sb.append(night.getResourceValue()); - } - sb.append(SEP); - - // We used to store the render target here in R9. Leave a marker - // to ensure that we don't reuse this slot; add new extra fields after it. - sb.append(SEP); - if (activity != null) { - sb.append(activity); - } - } - - return sb.toString(); - } - - boolean setData(String data) { - String[] values = data.split(SEP); - if (values.length >= 6 && values.length <= 8) { - for (Device d : mDeviceList) { - if (d.getName().equals(values[0])) { - device = d; - FolderConfiguration config = null; - if (!values[1].isEmpty() && !values[1].equals("null")) { //$NON-NLS-1$ - stateName = values[1]; - config = DeviceConfigHelper.getFolderConfig(device, stateName); - } else if (device.getAllStates().size() > 0) { - State first = device.getAllStates().get(0); - stateName = first.getName(); - config = DeviceConfigHelper.getFolderConfig(first); - } - if (config != null) { - // Load locale. Note that this can get overwritten by the - // project-wide settings read below. - locale = new ResourceQualifier[2]; - String locales[] = values[2].split(SEP_LOCALE); - if (locales.length >= 2) { - if (locales[0].length() > 0) { - locale[0] = new LanguageQualifier(locales[0]); - } - if (locales[1].length() > 0) { - locale[1] = new RegionQualifier(locales[1]); - } - } - - // Decode the theme name: See {@link #getData} - theme = values[3]; - if (theme.startsWith(MARKER_FRAMEWORK)) { - theme = ANDROID_STYLE_RESOURCE_PREFIX - + theme.substring(MARKER_FRAMEWORK.length()); - } else if (theme.startsWith(MARKER_PROJECT)) { - theme = STYLE_RESOURCE_PREFIX - + theme.substring(MARKER_PROJECT.length()); - } - - uiMode = UiMode.getEnum(values[4]); - if (uiMode == null) { - uiMode = UiMode.NORMAL; - } - night = NightMode.getEnum(values[5]); - if (night == null) { - night = NightMode.NOTNIGHT; - } - - // element 7/values[6]: used to store render target in R9. - // No longer stored here. If adding more data, make - // sure you leave 7 alone. - - Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState(); - - // We only use the "global" setting - if (!isLocaleSpecificLayout()) { - locale = pair.getFirst(); - } - target = pair.getSecond(); - - if (values.length == 8) { - activity = values[7]; - } - - return true; - } - } - } - } - - return false; - } - - @Override - public String toString() { - return getData(); - } - } - - /** - * Returns a String id to represent an {@link IAndroidTarget} which can be translated - * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id - * will never contain the {@link #SEP} character. - * - * @param target the target to return an id for - * @return an id for the given target; never null - */ - private String targetToString(IAndroidTarget target) { - return target.getFullName().replace(SEP, ""); //$NON-NLS-1$ - } - - /** - * Returns an {@link IAndroidTarget} that corresponds to the given id that was - * originally returned by {@link #targetToString}. May be null, if the platform is no - * longer available, or if the platform list has not yet been initialized. - * - * @param id the id that corresponds to the desired platform - * @return an {@link IAndroidTarget} that matches the given id, or null - */ - private IAndroidTarget stringToTarget(String id) { - if (mTargetList != null && mTargetList.size() > 0) { - for (IAndroidTarget target : mTargetList) { - if (id.equals(targetToString(target))) { - return target; - } - } - } - - return null; - } - - /** - * Creates a new {@link ConfigurationComposite} and adds it to the parent. - * - * The method also receives custom buttons to set into the configuration composite. The list - * is organized as an array of arrays. Each array represents a group of buttons thematically - * grouped together. - * - * @param listener An {@link IConfigListener} that gets and sets configuration properties. - * Mandatory, cannot be null. - * @param parent The parent composite. - * @param style The style of this composite. - * @param initialState The initial state (serialized form) to use for the configuration - */ - public ConfigurationComposite(IConfigListener listener, - Composite parent, int style, String initialState) { - super(parent, style); - setVisible(false); // Delayed until the targets are loaded - - mListener = listener; - mInitialState = initialState; - setLayout(new GridLayout(1, false)); - - IconFactory icons = IconFactory.getInstance(); - - // TODO: Consider switching to a CoolBar instead - ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); - toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); - - mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN | SWT.BOLD); - mConfigCombo.setImage(null); - mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse"); - - @SuppressWarnings("unused") - ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR); - - mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN); - mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY)); - - @SuppressWarnings("unused") - ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR); - - mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN); - mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT)); - mOrientationCombo.setToolTipText("Go to next state"); - - @SuppressWarnings("unused") - ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR); - - mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN); - mThemeCombo.setImage(icons.getIcon(ICON_THEMES)); - - @SuppressWarnings("unused") - ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR); - - mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN); - mActivityCombo.setToolTipText("Associated activity or fragment providing context"); - // The JDT class icon is lopsided, presumably because they've left room in the - // bottom right corner for badges (for static, final etc). Unfortunately, this - // means that the icon looks out of place when sitting close to the language globe - // icon, the theme icon, etc so that it looks vertically misaligned: - //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS)); - // ...so use one that is centered instead: - mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY)); - - @SuppressWarnings("unused") - ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR); - - //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); - //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); - ToolBar rightToolBar = toolBar; - - mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); - mLocaleCombo.setImage(LocaleManager.getGlobeIcon()); - mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse"); - - @SuppressWarnings("unused") - ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR); - - mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); - mTargetCombo.setImage(AdtPlugin.getAndroidLogo()); - mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse"); - - addConfigurationMenuListener(mConfigCombo); - addActivityMenuListener(mActivityCombo); - addLocaleMenuListener(mLocaleCombo); - addDeviceMenuListener(mDeviceCombo); - addTargetMenuListener(mTargetCombo); - addThemeListener(mThemeCombo); - addOrientationMenuListener(mOrientationCombo); - - addDisposeListener(this); - } - - private void updateActivity() { - if (mEditedFile != null) { - String preferred = getPreferredActivity(mEditedFile); - selectActivity(preferred); - } - } - - // ---- Dispose - - @Override - public void widgetDisposed(DisposeEvent e) { - dispose(); - } - - @Override - public void dispose() { - if (!isDisposed()) { - super.dispose(); - - final Sdk sdk = Sdk.getCurrent(); - if (sdk != null) { - DeviceManager manager = sdk.getDeviceManager(); - manager.unregisterListener(this); - } - } - } - - // ---- Init and reset/reload methods ---- - - /** - * Sets the reference to the file being edited. - * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is - * loaded (or reloaded as the SDK/target changes). - * - * @param file the file being opened - * - * @see #onXmlModelLoaded() - * @see #replaceFile(IFile) - * @see #changeFileOnNewConfig(IFile) - */ - public void setFile(IFile file) { - mEditedFile = file; - } - - /** - * Replaces the UI with a given file configuration. This is meant to answer the user - * explicitly opening a different version of the same layout from the Package Explorer. - * <p/>This attempts to keep the current config, but may change it if it's not compatible or - * not the best match - * <p/>This will NOT trigger a redraw event (will not call - * {@link IConfigListener#onConfigurationChange()}.) - * @param file the file being opened. - */ - public void replaceFile(IFile file) { - // if there is no previous selection, revert to default mode. - if (mState.device == null) { - setFile(file); // onTargetChanged will be called later. - return; - } - - mEditedFile = file; - IProject iProject = mEditedFile.getProject(); - mResources = ResourceManager.getInstance().getProjectResources(iProject); - - ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); - mEditedConfig = resFolder.getConfiguration(); - - mDisableUpdates++; // we do not want to trigger onXXXChange when setting - // new values in the widgets. - - try { - // only attempt to do anything if the SDK and targets are loaded. - LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); - if (sdkStatus == LoadStatus.LOADED) { - setVisible(true); - - LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, - null /*project*/); - - if (targetStatus == LoadStatus.LOADED) { - - // update the current config selection to make sure it's - // compatible with the new file - adaptConfigSelection(true /*needBestMatch*/); - - // compute the final current config - computeCurrentConfig(); - - // update the string showing the config value - updateConfigDisplay(mEditedConfig); - - updateActivity(); - } - } - } finally { - mDisableUpdates--; - } - } - - /** - * Updates the UI with a new file that was opened in response to a config change. - * @param file the file being opened. - * - * @see #replaceFile(IFile) - */ - public void changeFileOnNewConfig(IFile file) { - mEditedFile = file; - IProject iProject = mEditedFile.getProject(); - mResources = ResourceManager.getInstance().getProjectResources(iProject); - - ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); - mEditedConfig = resFolder.getConfiguration(); - - // All that's needed is to update the string showing the config value - // (since the config combo were chosen by the user). - updateConfigDisplay(mEditedConfig); - } - - /** - * Responds to the event that the basic SDK information finished loading. - * @param target the possibly new target object associated with the file being edited (in case - * the SDK path was changed). - */ - public void onSdkLoaded(IAndroidTarget target) { - // a change to the SDK means that we need to check for new/removed devices. - mSdkChanged = true; - - // store the new target. - mProjectTarget = target; - - mDisableUpdates++; // we do not want to trigger onXXXChange when setting - // new values in the widgets. - try { - // 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(); - } finally { - mDisableUpdates--; - } - } - - /** - * Answers to the XML model being loaded, either the first time or when the Target/SDK changes. - * <p>This initializes the UI, either with the first compatible configuration found, - * or attempts to restore a configuration if one is found to have been saved in the file - * persistent storage. - * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called - * back when those are loaded). - * <p>The method automatically handles being called the first time after editor creation, or - * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)} - * is properly called). - * - * @see #storeState() - * @see #onSdkLoaded(IAndroidTarget) - */ - public AndroidTargetData onXmlModelLoaded() { - AndroidTargetData targetData = null; - - // only attempt to do anything if the SDK and targets are loaded. - LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); - if (sdkStatus == LoadStatus.LOADED) { - mDisableUpdates++; // we do not want to trigger onXXXChange when setting - - try { - // init the devices if needed (new SDK or first time going through here) - if (mSdkChanged || mFirstXmlModelChange) { - initDevices(); - initTargets(); - mSdkChanged = false; - } - - IProject iProject = mEditedFile.getProject(); - - Sdk currentSdk = Sdk.getCurrent(); - if (currentSdk != null) { - mProjectTarget = currentSdk.getTarget(iProject); - } - - LoadStatus targetStatus = LoadStatus.FAILED; - if (mProjectTarget != null) { - targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); - initTargets(); - } - - if (targetStatus == LoadStatus.LOADED) { - setVisible(true); - if (mResources == null) { - mResources = ResourceManager.getInstance().getProjectResources(iProject); - } - if (mEditedConfig == null) { - IFolder parent = (IFolder) mEditedFile.getParent(); - ResourceFolder resFolder = mResources.getResourceFolder(parent); - if (resFolder != null) { - mEditedConfig = resFolder.getConfiguration(); - } else { - mEditedConfig = FolderConfiguration.getConfig( - parent.getName().split(RES_QUALIFIER_SEP)); - } - } - - 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 = mState.setData(data); - } - - updateLocales(); - - // If the current state was loaded from the persistent storage, we update the - // UI with it and then try to adapt it (which will handle incompatible - // configuration). - // Otherwise, just look for the first compatible configuration. - if (loadedConfigData) { - // first make sure we have the config to adapt - selectDevice(mState.device); - selectState(mState.stateName); - - adaptConfigSelection(false /*needBestMatch*/); - - selectTarget(mState.target); - - targetData = Sdk.getCurrent().getTargetData(mState.target); - } else { - findAndSetCompatibleConfig(false /*favorCurrentConfig*/); - - // Default to modern layout lib - IAndroidTarget target = findDefaultRenderTarget(); - if (target != null) { - targetData = Sdk.getCurrent().getTargetData(target); - selectTarget(target); - } - } - - // Update activity: This is done before updateThemes() since - // the themes selection can depend on the currently selected activity - // (e.g. when there are manifest registrations for the theme to use - // for a given activity) - updateActivity(); - - // Update themes. This is done after updating the devices above, - // since we want to look at the chosen device size to decide - // what the default theme (for example, with Honeycomb we choose - // Holo as the default theme but only if the screen size is XLARGE - // (and of course only if the manifest does not specify another - // default theme). - updateThemes(); - - // update the string showing the config value - updateConfigDisplay(mEditedConfig); - - // compute the final current config - computeCurrentConfig(); - } - } finally { - mDisableUpdates--; - mFirstXmlModelChange = false; - } - } - - return targetData; - } - - private void selectActivity(@Nullable String fqcn) { - if (fqcn != null) { - mActivityCombo.setData(fqcn); - String label = getActivityLabel(fqcn, true); - mActivityCombo.setText(label); - } else { - mActivityCombo.setText("(Select)"); - } - resizeToolBar(); - } - - @Nullable - private String getPreferredActivity(@NonNull IFile file) { - // Store/restore the activity context in the config state to help with - // performance if for some reason we can't write it into the XML file and to - // avoid having to open the model below - if (mState.activity != null) { - return mState.activity; - } - - IProject project = file.getProject(); - - // Look up from XML file - Document document = DomUtilities.getDocument(file); - if (document != null) { - Element element = document.getDocumentElement(); - if (element != null) { - String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT); - if (activity != null && !activity.isEmpty()) { - if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$ - ManifestInfo manifest = ManifestInfo.get(project); - String pkg = manifest.getPackage(); - if (!pkg.isEmpty()) { - if (activity.startsWith(".")) { //$NON-NLS-1$ - activity = pkg + activity; - } else { - activity = activity + "." + pkg; - } - } - } - - mState.activity = activity; - storeState(); - return activity; - } - } - } - - // No, not available there: try to infer it from the code index - String includedIn = mListener != null ? mListener.getIncludedWithin() : null; - - ManifestInfo manifest = ManifestInfo.get(project); - String pkg = manifest.getPackage(); - String layoutName = ResourceHelper.getLayoutName(mEditedFile); - - // If we are rendering a layout in included context, pick the theme - // from the outer layout instead - if (includedIn != null) { - layoutName = includedIn; - } - - String activity = ManifestInfo.guessActivity(project, layoutName, pkg); - - if (activity == null) { - List<String> activities = ManifestInfo.getProjectActivities(project); - if (activities.size() == 1) { - activity = activities.get(0); - } - } - - if (activity != null) { - mState.activity = activity; - storeState(); - return activity; - } - - // TODO: Do anything else, such as pick the first activity found? - // Or just leave some default label instead? - // Also, figure out what to store in the mState so I don't keep trying - - return null; - } - - private void onSelectActivity() { - String activity = getSelectedActivity(); - mState.activity = activity; - saveState(); - storeState(); - - if (activity == null) { - return; - } - - // See if there is a default theme assigned to this activity, and if so, use it - ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); - Map<String, String> activityThemes = manifest.getActivityThemes(); - String preferred = activityThemes.get(activity); - if (preferred != null) { - // Yes, switch to it - selectTheme(preferred); - onThemeChange(); - } - - // Persist in XML - if (mListener != null) { - mListener.onSetActivity(activity); - } - } - - - /** Update the toolbar whenever a label has changed, to not only - * cause the layout in the current toolbar to update, but to possibly - * wrap the toolbars and update the layout of the surrounding area. - */ - private void resizeToolBar() { - Point size = getSize(); - Point newSize = computeSize(size.x, SWT.DEFAULT, true); - setSize(newSize); - Composite parent = getParent(); - parent.layout(); - parent.redraw(); - } - - private String getActivityLabel(String fqcn, boolean brief) { - if (brief) { - String label = fqcn; - int packageIndex = label.lastIndexOf('.'); - if (packageIndex != -1) { - label = label.substring(packageIndex + 1); - } - int innerClass = label.lastIndexOf('$'); - if (innerClass != -1) { - label = label.substring(innerClass + 1); - } - - // Also strip out the "Activity" or "Fragment" common suffix - // if this is a long name - if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix - label = label.substring(0, label.length() - 8); - } else if (label.endsWith("Fragment") && label.length() > 8 + 12) { - label = label.substring(0, label.length() - 8); - } - - return label; - } - - return fqcn; - } - - String getSelectedActivity() { - return (String) mActivityCombo.getData(); - } - - private void selectTarget(IAndroidTarget target) { - mTargetCombo.setData(target); - String label = getRenderingTargetLabel(target, true); - mTargetCombo.setText(label); - resizeToolBar(); - } - - private static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) { - if (target == null) { - return "<null>"; - } - - AndroidVersion version = target.getVersion(); - - if (brief) { - if (target.isPlatform()) { - return Integer.toString(version.getApiLevel()); - } else { - return target.getName() + ':' + Integer.toString(version.getApiLevel()); - } - } - - String label = String.format("API %1$d: %2$s", - version.getApiLevel(), - target.getShortClasspathName()); - - return label; - } - - private String getLocaleLabel(ResourceQualifier[] qualifiers, boolean brief) { - if (qualifiers == null) { - return null; - } - - LanguageQualifier language = (LanguageQualifier) qualifiers[LOCALE_LANG]; - - if (language.hasFakeValue()) { - if (brief) { - // Just use the icon - return ""; - } - - boolean hasLocale = false; - ResourceRepository projectRes = mListener.getProjectResources(); - if (projectRes != null) { - hasLocale = projectRes.getLanguages().size() > 0; - } - - if (hasLocale) { - return "Other"; - } else { - return "Any"; - } - } - - String languageCode = language.getValue(); - String languageName = LocaleManager.getLanguageName(languageCode); - - RegionQualifier region = (RegionQualifier) qualifiers[LOCALE_REGION]; - if (region.hasFakeValue()) { - // TODO: Make the region string use "Other" instead of "Any" if - // there is more than one region for a given language - //if (regions.size() > 0) { - // return String.format("%1$s / Other", language); - //} else { - // return String.format("%1$s / Any", language); - //} - if (!brief && languageName != null) { - return String.format("%1$s (%2$s)", languageName, languageCode); - } else { - return languageCode; - } - } else { - String regionCode = region.getValue(); - if (!brief && languageName != null) { - String regionName = LocaleManager.getRegionName(regionCode); - if (regionName != null) { - return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode, - regionName, regionCode); - } - return String.format("%1$s (%2$s) in %3$s", languageName, languageCode, - regionCode); - } - return String.format("%1$s / %2$s", languageCode, regionCode); - } - } - - private void selectLocale(ResourceQualifier[] qualifiers) { - mLocaleCombo.setData(qualifiers); - String label = getLocaleLabel(qualifiers, true); - - mLocaleCombo.setText(label); - - Image image = getFlagImage(qualifiers); - mLocaleCombo.setImage(image); - - resizeToolBar(); - } - - private ResourceQualifier[] getSelectedLocale() { - return (ResourceQualifier[]) mLocaleCombo.getData(); - } - - private IAndroidTarget getSelectedTarget() { - if (!mTargetCombo.isDisposed()) { - return (IAndroidTarget) mTargetCombo.getData(); - } - - return null; - } - - void selectTheme(String theme) { - assert theme.startsWith(STYLE_RESOURCE_PREFIX) - || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme; - mThemeCombo.setData(theme); - if (theme != null) { - mThemeCombo.setText(getThemeLabel(theme, true)); - } else { - // FIXME eclipse claims this is dead code. - mThemeCombo.setText("(Set Theme)"); - } - resizeToolBar(); - } - - /** Return the default render target to use, or null if no strong preference */ - private IAndroidTarget findDefaultRenderTarget() { - // Default to layoutlib version 5 - Sdk current = Sdk.getCurrent(); - if (current != null) { - IAndroidTarget projectTarget = current.getTarget(mEditedFile.getProject()); - int minProjectApi = Integer.MAX_VALUE; - if (projectTarget != null) { - if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) { - // Renderable non-platform targets are all going to be adequate (they - // will have at least version 5 of layoutlib) so use the project - // target as the render target. - return projectTarget; - } - - if (projectTarget.getVersion().isPreview() - && projectTarget.hasRenderingLibrary()) { - // If the project target is a preview version, then just use it - return projectTarget; - } - - minProjectApi = projectTarget.getVersion().getApiLevel(); - } - - // We want to pick a render target that contains at least version 5 (and - // preferably version 6) of the layout library. To do this, we go through the - // targets and pick the -smallest- API level that is both simultaneously at - // least as big as the project API level, and supports layoutlib level 5+. - IAndroidTarget best = null; - int bestApiLevel = Integer.MAX_VALUE; - - for (IAndroidTarget target : current.getTargets()) { - // Non-platform targets are not chosen as the default render target - if (!target.isPlatform()) { - continue; - } - - int apiLevel = target.getVersion().getApiLevel(); - - // Ignore targets that have a lower API level than the minimum project - // API level: - if (apiLevel < minProjectApi) { - continue; - } - - // Look up the layout lib API level. This property is new so it will only - // be defined for version 6 or higher, which means non-null is adequate - // to see if this target is eligible: - String property = target.getProperty(PkgProps.LAYOUTLIB_API); - // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate: - if (property != null || apiLevel >= 11) { - if (apiLevel < bestApiLevel) { - bestApiLevel = apiLevel; - best = target; - } - } - } - - return best; - } - - return null; - } - - private static class ConfigBundle { - FolderConfiguration config; - int localeIndex; - int dockModeIndex; - int nightModeIndex; - - ConfigBundle() { - config = new FolderConfiguration(); - localeIndex = 0; - dockModeIndex = 0; - nightModeIndex = 0; - } - - ConfigBundle(ConfigBundle bundle) { - config = new FolderConfiguration(); - config.set(bundle.config); - localeIndex = bundle.localeIndex; - dockModeIndex = bundle.dockModeIndex; - nightModeIndex = bundle.nightModeIndex; - } - } - - private static class ConfigMatch { - final FolderConfiguration testConfig; - final Device device; - final String name; - final ConfigBundle bundle; - - public ConfigMatch(@NonNull FolderConfiguration testConfig, Device device, String name, - ConfigBundle bundle) { - this.testConfig = testConfig; - this.device = device; - this.name = name; - this.bundle = bundle; - } - - @Override - public String toString() { - return device.getName() + " - " + name; - } - } - - /** - * Finds a device/config that can display {@link #mEditedConfig}. - * <p/>Once found the device and config combos are set to the config. - * <p/>If there is no compatible configuration, a custom one is created. - * @param favorCurrentConfig if true, and no best match is found, don't change - * the current config. This must only be true if the current config is compatible. - */ - private void findAndSetCompatibleConfig(boolean favorCurrentConfig) { - // list of compatible device/state/locale - List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>(); - - // list of actual best match (ie the file is a best match for the - // device/state) - List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>(); - - // get a locale that match the host locale roughly (may not be exact match on the region.) - int localeHostMatch = getLocaleMatch(); - - // build a list of combinations of non standard qualifiers to add to each device's - // qualifier set when testing for a match. - // These qualifiers are: locale, night-mode, car dock. - List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200); - - // If the edited file has locales, then we have to select a matching locale from - // the list. - // However, if it doesn't, we don't randomly take the first locale, we take one - // matching the current host locale (making sure it actually exist in the project) - int start, max; - if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) { - // add all the locales - start = 0; - max = mLocaleList.size(); - } else { - // only add the locale host match - start = localeHostMatch; - max = localeHostMatch + 1; // test is < - } - - for (int i = start ; i < max ; i++) { - ResourceQualifier[] l = mLocaleList.get(i); - - ConfigBundle bundle = new ConfigBundle(); - bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]); - bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]); - - bundle.localeIndex = i; - configBundles.add(bundle); - } - - // add the dock mode to the bundle combinations. - addDockModeToBundles(configBundles); - - // add the night mode to the bundle combinations. - addNightModeToBundles(configBundles); - - addRenderTargetToBundles(configBundles); - - for (Device device : mDeviceList) { - for (State state : device.getAllStates()) { - - // loop on the list of config bundles to create full - // configurations. - FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state); - for (ConfigBundle bundle : configBundles) { - // create a new config with device config - FolderConfiguration testConfig = new FolderConfiguration(); - testConfig.set(stateConfig); - - // add on top of it, the extra qualifiers from the bundle - testConfig.add(bundle.config); - - if (mEditedConfig.isMatchFor(testConfig)) { - // this is a basic match. record it in case we don't - // find a match - // where the edited file is a best config. - anyMatches - .add(new ConfigMatch(testConfig, device, state.getName(), bundle)); - - if (isCurrentFileBestMatchFor(testConfig)) { - // this is what we want. - bestMatches.add(new ConfigMatch(testConfig, device, state.getName(), - bundle)); - } - } - } - } - } - - if (bestMatches.size() == 0) { - if (favorCurrentConfig) { - // quick check - if (mEditedConfig.isMatchFor(mCurrentConfig) == false) { - AdtPlugin.log(IStatus.ERROR, - "favorCurrentConfig can only be true if the current config is compatible"); - } - - // just display the warning - AdtPlugin.printErrorToConsole(mEditedFile.getProject(), - String.format( - "'%1$s' is not a best match for any device/locale combination.", - mEditedConfig.toDisplayString()), - String.format( - "Displaying it with '%1$s'", - mCurrentConfig.toDisplayString())); - } else if (anyMatches.size() > 0) { - // select the best device anyway. - ConfigMatch match = selectConfigMatch(anyMatches); - selectDevice(mState.device = match.device); - selectState(match.name); - selectLocale(mLocaleList.get(match.bundle.localeIndex)); - - mState.uiMode = UiMode.getByIndex(match.bundle.dockModeIndex); - mState.night = NightMode.getByIndex(match.bundle.nightModeIndex); - - // TODO: display a better warning! - computeCurrentConfig(); - AdtPlugin.printErrorToConsole(mEditedFile.getProject(), - String.format( - "'%1$s' is not a best match for any device/locale combination.", - mEditedConfig.toDisplayString()), - String.format( - "Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.", - mCurrentConfig.toDisplayString())); - - } else { - // TODO: there is no device/config able to display the layout, create one. - // For the base config values, we'll take the first device and state, - // and replace whatever qualifier required by the layout file. - } - } else { - ConfigMatch match = selectConfigMatch(bestMatches); - selectDevice(mState.device = match.device); - selectState(match.name); - selectLocale(mLocaleList.get(match.bundle.localeIndex)); - mState.uiMode = UiMode.getByIndex(match.bundle.dockModeIndex); - mState.night = NightMode.getByIndex(match.bundle.nightModeIndex); - } - } - - /** - * Note: this comparator imposes orderings that are inconsistent with equals. - */ - private static class TabletConfigComparator implements Comparator<ConfigMatch> { - @Override - public int compare(ConfigMatch o1, ConfigMatch o2) { - FolderConfiguration config1 = o1 != null ? o1.testConfig : null; - FolderConfiguration config2 = o2 != null ? o2.testConfig : null; - if (config1 == null) { - if (config2 == null) { - return 0; - } else { - return -1; - } - } else if (config2 == null) { - return 1; - } - - ScreenSizeQualifier size1 = config1.getScreenSizeQualifier(); - ScreenSizeQualifier size2 = config2.getScreenSizeQualifier(); - ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL; - ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL; - - // X-LARGE is better than all others (which are considered identical) - // if both X-LARGE, then LANDSCAPE is better than all others (which are identical) - - if (ss1 == ScreenSize.XLARGE) { - if (ss2 == ScreenSize.XLARGE) { - ScreenOrientationQualifier orientation1 = - config1.getScreenOrientationQualifier(); - ScreenOrientation so1 = orientation1.getValue(); - if (so1 == null) { - so1 = ScreenOrientation.PORTRAIT; - } - ScreenOrientationQualifier orientation2 = - config2.getScreenOrientationQualifier(); - ScreenOrientation so2 = orientation2.getValue(); - if (so2 == null) { - so2 = ScreenOrientation.PORTRAIT; - } - - if (so1 == ScreenOrientation.LANDSCAPE) { - if (so2 == ScreenOrientation.LANDSCAPE) { - return 0; - } else { - return -1; - } - } else if (so2 == ScreenOrientation.LANDSCAPE) { - return 1; - } else { - return 0; - } - } else { - return -1; - } - } else if (ss2 == ScreenSize.XLARGE) { - return 1; - } else { - return 0; - } - } - } - - /** - * Note: this comparator imposes orderings that are inconsistent with equals. - */ - private static class PhoneConfigComparator implements Comparator<ConfigMatch> { - - private SparseIntArray mDensitySort = new SparseIntArray(4); - - public PhoneConfigComparator() { - // put the sort order for the density. - mDensitySort.put(Density.HIGH.getDpiValue(), 1); - mDensitySort.put(Density.MEDIUM.getDpiValue(), 2); - mDensitySort.put(Density.XHIGH.getDpiValue(), 3); - mDensitySort.put(Density.LOW.getDpiValue(), 4); - } - - @Override - public int compare(ConfigMatch o1, ConfigMatch o2) { - FolderConfiguration config1 = o1 != null ? o1.testConfig : null; - FolderConfiguration config2 = o2 != null ? o2.testConfig : null; - if (config1 == null) { - if (config2 == null) { - return 0; - } else { - return -1; - } - } else if (config2 == null) { - return 1; - } - - int dpi1 = Density.DEFAULT_DENSITY; - int dpi2 = Density.DEFAULT_DENSITY; - - DensityQualifier dpiQualifier1 = config1.getDensityQualifier(); - if (dpiQualifier1 != null) { - Density value = dpiQualifier1.getValue(); - dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; - } - dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/); - - DensityQualifier dpiQualifier2 = config2.getDensityQualifier(); - if (dpiQualifier2 != null) { - Density value = dpiQualifier2.getValue(); - dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; - } - dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/); - - if (dpi1 == dpi2) { - // portrait is better - ScreenOrientation so1 = ScreenOrientation.PORTRAIT; - ScreenOrientationQualifier orientationQualifier1 = - config1.getScreenOrientationQualifier(); - if (orientationQualifier1 != null) { - so1 = orientationQualifier1.getValue(); - if (so1 == null) { - so1 = ScreenOrientation.PORTRAIT; - } - } - ScreenOrientation so2 = ScreenOrientation.PORTRAIT; - ScreenOrientationQualifier orientationQualifier2 = - config2.getScreenOrientationQualifier(); - if (orientationQualifier2 != null) { - so2 = orientationQualifier2.getValue(); - if (so2 == null) { - so2 = ScreenOrientation.PORTRAIT; - } - } - - if (so1 == ScreenOrientation.PORTRAIT) { - if (so2 == ScreenOrientation.PORTRAIT) { - return 0; - } else { - return -1; - } - } else if (so2 == ScreenOrientation.PORTRAIT) { - return 1; - } else { - return 0; - } - } - - return dpi1 - dpi2; - } - } - - private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { - // API 11-13: look for a x-large device - int apiLevel = mProjectTarget.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 { - // lets look for a high density device - Collections.sort(matches, new PhoneConfigComparator()); - } - - // Look at the currently active editor to see if it's a layout editor, and if so, - // look up its configuration and if the configuration is in our match list, - // use it. This means we "preserve" the current configuration when you open - // new layouts. - IEditorPart activeEditor = AdtUtils.getActiveEditor(); - LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); - if (delegate != null - && mEditedFile != null - // (Only do this when the two files are in the same project) - && delegate.getEditor().getProject() == mEditedFile.getProject()) { - FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); - if (configuration != null) { - for (ConfigMatch match : matches) { - if (configuration.equals(match.testConfig)) { - return match; - } - } - } - } - - // the list has been sorted so that the first item is the best config - return matches.get(0); - } - - private void addRenderTargetToBundles(List<ConfigBundle> configBundles) { - Pair<ResourceQualifier[], IAndroidTarget> state = loadRenderState(); - if (state != null) { - IAndroidTarget target = state.getSecond(); - if (target != null) { - int apiLevel = target.getVersion().getApiLevel(); - for (ConfigBundle bundle : configBundles) { - bundle.config.setVersionQualifier( - new VersionQualifier(apiLevel)); - } - } - } - } - - private void addDockModeToBundles(List<ConfigBundle> addConfig) { - ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); - - // loop on each item and for each, add all variations of the dock modes - for (ConfigBundle bundle : addConfig) { - int index = 0; - for (UiMode mode : UiMode.values()) { - ConfigBundle b = new ConfigBundle(bundle); - b.config.setUiModeQualifier(new UiModeQualifier(mode)); - b.dockModeIndex = index++; - list.add(b); - } - } - - addConfig.clear(); - addConfig.addAll(list); - } - - private void addNightModeToBundles(List<ConfigBundle> addConfig) { - ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); - - // loop on each item and for each, add all variations of the night modes - for (ConfigBundle bundle : addConfig) { - int index = 0; - for (NightMode mode : NightMode.values()) { - ConfigBundle b = new ConfigBundle(bundle); - b.config.setNightModeQualifier(new NightModeQualifier(mode)); - b.nightModeIndex = index++; - list.add(b); - } - } - - addConfig.clear(); - addConfig.addAll(list); - } - - /** - * Adapts the current device/config selection so that it's compatible with - * {@link #mEditedConfig}. - * <p/>If the current selection is compatible, nothing is changed. - * <p/>If it's not compatible, configs from the current devices are tested. - * <p/>If none are compatible, it reverts to - * {@link #findAndSetCompatibleConfig(boolean)} - */ - private void adaptConfigSelection(boolean needBestMatch) { - // check the device config (ie sans locale) - boolean needConfigChange = true; // if still true, we need to find another config. - boolean currentConfigIsCompatible = false; - State selectedState = getSelectedDeviceState(); - if (selectedState != null) { - FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState); - if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) { - currentConfigIsCompatible = true; // current config is compatible - if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) { - needConfigChange = false; - } - } - } - - if (needConfigChange) { - // if the current state/locale isn't a correct match, then - // look for another state/locale in the same device. - FolderConfiguration testConfig = new FolderConfiguration(); - - // first look in the current device. - String matchName = null; - int localeIndex = -1; - mainloop: for (State state : mState.device.getAllStates()) { - testConfig.set(DeviceConfigHelper.getFolderConfig(state)); - - // loop on the locales. - for (int i = 0 ; i < mLocaleList.size() ; i++) { - ResourceQualifier[] locale = mLocaleList.get(i); - - // update the test config with the locale qualifiers - testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]); - testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]); - - if (mEditedConfig.isMatchFor(testConfig) && - isCurrentFileBestMatchFor(testConfig)) { - matchName = state.getName(); - localeIndex = i; - break mainloop; - } - } - } - - if (matchName != null) { - selectState(matchName); - selectLocale(mLocaleList.get(localeIndex)); - } else { - // no match in current device with any state/locale - // attempt to find another device that can display this - // particular state. - findAndSetCompatibleConfig(currentConfigIsCompatible); - } - } - } - - /** - * Finds a locale matching the config from a file. - * @param language the language qualifier or null if none is set. - * @param region the region qualifier or null if none is set. - * @return true if there was a change in the combobox as a result of applying the locale - */ - private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) { - boolean changed = false; - - // find the locale match. Since the locale list is based on the content of the - // project resources there must be an exact match. - // The only trick is that the region could be null in the fileConfig but in our - // list of locales, this is represented as a RegionQualifier with value of - // FAKE_LOCALE_VALUE. - ResourceQualifier[] selectedLocale = getSelectedLocale(); - //changed = prevLanguage != language || region != prevRegion; - if (selectedLocale != null) { - ResourceQualifier prevLanguage = selectedLocale[LOCALE_LANG]; - ResourceQualifier prevRegion = selectedLocale[LOCALE_REGION]; - changed = !prevLanguage.equals(language) || !prevRegion.equals(region); - } - - selectLocale(new ResourceQualifier[] { language, region}); - - return changed; - } - - private void updateConfigDisplay(FolderConfiguration fileConfig) { - // Label currently hidden - //String current = fileConfig.toDisplayString(); - //String current = fileConfig.getFolderName(ResourceFolderType.LAYOUT); - String current = mEditedFile.getParent().getName(); - if (current.equals(FD_RES_LAYOUT)) { - current = "default"; - } - - // Pretty things up a bit - //if (current == null || current.equals("default")) { - // current = "Default Configuration"; - //} - mConfigCombo.setText(current); - resizeToolBar(); - } - - private void saveState() { - if (mDisableUpdates == 0) { - State state = getSelectedDeviceState(); - String stateName = state != null ? state.getName() : null; - mState.stateName = stateName; - - // since the locales are relative to the project, only keeping the index is enough - mState.locale = getSelectedLocale(); - mState.theme = getSelectedTheme(); - mState.target = getRenderingTarget(); - mState.activity = getSelectedActivity(); - } - } - - /** - * Stores the current config selection into the edited file. - */ - public void storeState() { - AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData()); - } - - private void addLocaleMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - Menu menu = new Menu(ConfigurationComposite.this.getShell(), SWT.POP_UP); - ResourceQualifier[] current = getSelectedLocale(); - - for (final ResourceQualifier[] qualifiers : mLocaleList) { - String title = getLocaleLabel(qualifiers, false); - MenuItem item = new MenuItem(menu, SWT.CHECK); - item.setText(title); - Image image = getFlagImage(qualifiers); - item.setImage(image); - - boolean selected = current == qualifiers; - if (selected) { - item.setSelection(true); - } - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - selectLocale(qualifiers); - onLocaleChange(); - } - }); - } - - @SuppressWarnings("unused") - MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); - - MenuItem item = new MenuItem(menu, SWT.PUSH); - item.setText("Add New Translation..."); - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - IProject project = mEditedFile.getProject(); - Shell shell = ConfigurationComposite.this.getShell(); - AddTranslationDialog dialog = new AddTranslationDialog(shell, project); - dialog.open(); - } - }); - - Rectangle bounds = combo.getBounds(); - Point location = new Point(bounds.x, bounds.y + bounds.height); - location = combo.getParent().toDisplay(location); - menu.setLocation(location.x, location.y); - menu.setVisible(true); - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - private Map<String, String> mCountryToLanguage; - - @SuppressWarnings("unused") // FIXME cleanup if really not used anymore? - private String getCountry(String language, String region) { - if (RegionQualifier.FAKE_REGION_VALUE.equals(region)) { - region = ""; - } - - String country = region; - if (country.isEmpty()) { - // Special cases - if (language.equals("ar")) { //$NON-NLS-1$ - country = "AE"; //$NON-NLS-1$ - } else if (language.equals("zh")) { //$NON-NLS-1$ - country = "CN"; //$NON-NLS-1$ - } else if (language.equals("en")) { //$NON-NLS-1$ - country = "US"; //$NON-NLS-1$ - } else if (language.equals("fa")) { //$NON-NLS-1$ - country = "IR"; //$NON-NLS-1$ - } - } - - if (country.isEmpty()) { - if (mCountryToLanguage == null) { - Locale[] locales = Locale.getAvailableLocales(); - mCountryToLanguage = Maps.newHashMapWithExpectedSize(locales.length); - Map<String, Locale> localeMap = Maps.newHashMapWithExpectedSize(locales.length); - for (int i = 0; i < locales.length; i++) { - Locale locale = locales[i]; - String localeLanguage = locale.getLanguage(); - String localeCountry = locale.getCountry(); - if (!localeCountry.isEmpty()) { - localeCountry = localeCountry.toLowerCase(Locale.US); - Locale old = localeMap.get(localeLanguage); - if (old != null) { - // For Italian for example it has both a locale with country = Italy - // and one with country = Switzerland, so prefer the one where the - // language code matches the country. - if (!localeLanguage.equals(localeCountry)) { - continue; - } - } - mCountryToLanguage.put(localeLanguage, localeCountry); - localeMap.put(localeLanguage, locale); - } - } - } - - country = mCountryToLanguage.get(language); - } - - return country; - } - - @NonNull - private Image getFlagImage(@NonNull ResourceQualifier[] qualifiers) { - Image image = null; - assert qualifiers.length == 2; - String language = ((LanguageQualifier) qualifiers[LOCALE_LANG]).getValue(); - if (LanguageQualifier.FAKE_LANG_VALUE.equals(language)) { - language = null; - } - String region = ((RegionQualifier) qualifiers[LOCALE_REGION]).getValue(); - if (RegionQualifier.FAKE_REGION_VALUE.equals(region)) { - region = null; - } - LocaleManager icons = LocaleManager.get(); - if (language == null && region == null) { - return LocaleManager.getGlobeIcon(); - } else { - image = icons.getFlag(language, region); - if (image == null) { - image = LocaleManager.getEmptyIcon(); - } - - return image; - } - } - - private String getDeviceLabel(Device device, boolean brief) { - if(device == null) { - return ""; - } - String name = device.getName(); - - if (brief) { - // Produce a really brief summary of the device name, suitable for - // use in the narrow space available in the toolbar for example - int nexus = name.indexOf("Nexus"); //$NON-NLS-1$ - if (nexus != -1) { - int begin = name.indexOf('('); - if (begin != -1) { - begin++; - int end = name.indexOf(')', begin); - if (end != -1) { - return name.substring(begin, end).trim(); - } - } - } - } - - return name; - } - - private void addDeviceMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - Device current = getSelectedDevice(); - Menu menu = new Menu(ConfigurationComposite.this.getShell(), SWT.POP_UP); - - AvdManager avdManager = Sdk.getCurrent().getAvdManager(); - AvdInfo[] avds = avdManager.getValidAvds(); - boolean separatorNeeded = false; - for (AvdInfo avd : avds) { - for (final Device d : mDeviceList) { - if (d.getManufacturer().equals(avd.getDeviceManufacturer()) - && d.getName().equals(avd.getDeviceName())) { - separatorNeeded = true; - MenuItem item = new MenuItem(menu, SWT.CHECK); - item.setText(avd.getName()); - item.setSelection(current == d); - - item.addSelectionListener(new SelectionAdapter() { - - @Override - public void widgetSelected(SelectionEvent e) { - selectDevice(d); - onDeviceChange(true /*recomputeLayout*/); - } - }); - } - } - } - - if (separatorNeeded) { - @SuppressWarnings("unused") - MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); - } - - // Group the devices by manufacturer, then put them in the menu - if (!mDeviceList.isEmpty()) { - Map<String, List<Device>> manufacturers = new TreeMap<String, List<Device>>(); - for (Device device : mDeviceList) { - List<Device> devices; - if(manufacturers.containsKey(device.getManufacturer())) { - devices = manufacturers.get(device.getManufacturer()); - } else { - devices = new ArrayList<Device>(); - manufacturers.put(device.getManufacturer(), devices); - } - devices.add(device); - } - for (List<Device> devices : manufacturers.values()) { - Menu manufacturerMenu = menu; - if (manufacturers.size() > 1) { - MenuItem item = new MenuItem(menu, SWT.CASCADE); - item.setText(devices.get(0).getManufacturer()); - manufacturerMenu = new Menu(menu); - item.setMenu(manufacturerMenu); - } - for (final Device d : devices) { - MenuItem deviceItem = new MenuItem(manufacturerMenu, SWT.CHECK); - deviceItem.setText(d.getName()); - deviceItem.setSelection(current == d); - - deviceItem.addSelectionListener(new SelectionAdapter() { - - @Override - public void widgetSelected(SelectionEvent e) { - selectDevice(d); - onDeviceChange(true /*recomputeLayout*/); - } - }); - } - } - } - - // TODO - how do I dispose of this? - - Rectangle bounds = combo.getBounds(); - Point location = new Point(bounds.x, bounds.y + bounds.height); - location = combo.getParent().toDisplay(location); - menu.setLocation(location.x, location.y); - menu.setVisible(true); - } - }; - - if (mDeviceListener != null) { - combo.removeListener(SWT.Selection, mDeviceListener); - } - mDeviceListener = menuListener; - combo.addListener(SWT.Selection, menuListener); - } - - private void addTargetMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - Menu menu = new Menu(ConfigurationComposite.this.getShell(), SWT.POP_UP); - IAndroidTarget current = getSelectedTarget(); - - for (final IAndroidTarget target : mTargetList) { - String title = getRenderingTargetLabel(target, false); - MenuItem item = new MenuItem(menu, SWT.CHECK); - item.setText(title); - - boolean selected = current == target; - if (selected) { - item.setSelection(true); - } - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - selectTarget(target); - onRenderingTargetChange(); - } - }); - } - - Rectangle bounds = combo.getBounds(); - Point location = new Point(bounds.x, bounds.y + bounds.height); - location = combo.getParent().toDisplay(location); - menu.setLocation(location.x, location.y); - menu.setVisible(true); - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - private void addThemeListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - ThemeMenuAction.showThemeMenu(ConfigurationComposite.this, combo, mThemeList); - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - private void addOrientationMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - if (event.detail == SWT.ARROW) { - OrientationMenuAction.showMenu(ConfigurationComposite.this, combo); - } else { - gotoNextState(); - } - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - /** Move to the next device state, changing the icon if it changes orientation */ - private void gotoNextState() { - State state = getSelectedDeviceState(); - State flipped = getNextDeviceState(state); - if (flipped != state) { - selectDeviceState(flipped); - onDeviceConfigChange(); - } - } - - /** Get the next cyclical state after the given state */ - @Nullable - State getNextDeviceState(State state) { - Device device = getSelectedDevice(); - List<State> states = device.getAllStates(); - for (int i = 0; i < states.size(); i++) { - if (states.get(i) == state) { - return states.get((i + 1) % states.size()); - } - } - - return null; - } - - protected String getThemeLabel(String theme, boolean brief) { - theme = ResourceHelper.styleToTheme(theme); - - if (brief) { - int index = theme.lastIndexOf('.'); - if (index < theme.length() - 1) { - return theme.substring(index + 1); - } - } - return theme; - } - - private void addActivityMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - // TODO: Allow using fragments here as well? - Menu menu = new Menu(ConfigurationComposite.this.getShell(), SWT.POP_UP); - ISharedImages sharedImages = JavaUI.getSharedImages(); - String current = getSelectedActivity(); - - if (current != null) { - MenuItem item = new MenuItem(menu, SWT.PUSH); - String label = getActivityLabel(current, true);; - item.setText( String.format("Open %1$s...", label)); - Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CUNIT); - item.setImage(image); - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - String fqcn = getSelectedActivity(); - AdtPlugin.openJavaClass(mEditedFile.getProject(), fqcn); - } - }); - - @SuppressWarnings("unused") - MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); - } - - IProject project = mEditedFile.getProject(); - Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CLASS); - - // Add activities found to be relevant to this layout - String layoutName = ResourceHelper.getLayoutName(mEditedFile); - String pkg = ManifestInfo.get(project).getPackage(); - List<String> preferred = ManifestInfo.guessActivities(project, layoutName, pkg); - current = addActivities(menu, current, image, preferred); - - // Add all activities - List<String> activities = ManifestInfo.getProjectActivities(project); - if (preferred.size() > 0) { - // Filter out the activities we've already listed above - List<String> filtered = new ArrayList<String>(activities.size()); - Set<String> remove = new HashSet<String>(preferred); - for (String fqcn : activities) { - if (!remove.contains(fqcn)) { - filtered.add(fqcn); - } - } - activities = filtered; - } - - if (activities.size() > 0) { - if (preferred.size() > 0) { - @SuppressWarnings("unused") - MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); - } - - addActivities(menu, current, image, activities); - } - - Rectangle bounds = combo.getBounds(); - Point location = new Point(bounds.x, bounds.y + bounds.height); - location = combo.getParent().toDisplay(location); - menu.setLocation(location.x, location.y); - menu.setVisible(true); - } - - private String addActivities(Menu menu, String current, Image image, - List<String> activities) { - for (final String fqcn : activities) { - String title = getActivityLabel(fqcn, false); - MenuItem item = new MenuItem(menu, SWT.CHECK); - item.setText(title); - item.setImage(image); - - boolean selected = title.equals(current); - if (selected) { - item.setSelection(true); - current = null; // Only show the first occurrence as selected - // such that we don't show it selected again in the full activity list - } - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - selectActivity(fqcn); - onSelectActivity(); - } - }); - } - - return current; - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - private void addConfigurationMenuListener(final ToolItem combo) { - Listener menuListener = new Listener() { - @Override - public void handleEvent(Event event) { - Menu menu = new Menu(ConfigurationComposite.this.getShell(), SWT.POP_UP); - - // Compute the set of layout files defining this layout resource - String name = mEditedFile.getName(); - IContainer resFolder = mEditedFile.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) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - ResourceManager manager = ResourceManager.getInstance(); - for (final IFile resource : variations) { - MenuItem item = new MenuItem(menu, SWT.CHECK); - - IFolder parent = (IFolder) resource.getParent(); - ResourceFolder parentResource = manager.getResourceFolder(parent); - FolderConfiguration configuration = parentResource.getConfiguration(); - String title = configuration.toDisplayString(); - item.setText(title); - - boolean selected = mEditedFile.equals(resource); - if (selected) { - item.setSelection(true); - } - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - ConfigurationComposite.this.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - try { - AdtPlugin.openFile(resource, null, false); - } catch (PartInitException ex) { - AdtPlugin.log(ex, null); - } - } - }); - } - }); - } - - if (!mEditedConfig.equals(mCurrentConfig)) { - 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.CHECK); - item.setText("Create New..."); - item.setImage(IconFactory.getInstance().getIcon(ICON_NEW_CONFIG)); - //item.setToolTipText("Duplicate: Create new configuration for this layout"); - - item.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - if (mListener != null) { - mListener.onCreate(); - } - } - }); - } - - Rectangle bounds = combo.getBounds(); - Point location = new Point(bounds.x, bounds.y + bounds.height); - location = combo.getParent().toDisplay(location); - menu.setLocation(location.x, location.y); - menu.setVisible(true); - } - }; - combo.addListener(SWT.Selection, menuListener); - } - - /** - * Updates the locale combo. - * This must be called from the UI thread. - */ - public void updateLocales() { - if (mListener == null) { - return; // can't do anything w/o it. - } - - mDisableUpdates++; - - try { - mLocaleList.clear(); - - SortedSet<String> languages = null; - - // get the languages from the project. - ResourceRepository projectRes = mListener.getProjectResources(); - - // in cases where the opened file is not linked to a project, this could be null. - if (projectRes != null) { - // now get the languages from the project. - languages = projectRes.getLanguages(); - - for (String language : languages) { - LanguageQualifier langQual = new LanguageQualifier(language); - - // find the matching regions and add them - SortedSet<String> regions = projectRes.getRegions(language); - for (String region : regions) { - RegionQualifier regionQual = new RegionQualifier(region); - mLocaleList.add(new ResourceQualifier[] { langQual, regionQual }); - } - - // now the entry for the other regions the language alone - // create a region qualifier that will never be matched by qualified resources. - mLocaleList.add(new ResourceQualifier[] { - langQual, - new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) - }); - } - } - - // create language/region qualifier that will never be matched by qualified resources. - mLocaleList.add(new ResourceQualifier[] { - new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), - new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) - }); - - if (mState.locale != null) { - // FIXME: this may fails if the layout was deleted (and was the last one to have - // that local. (we have other problem in this case though) - setLocaleCombo(mState.locale[LOCALE_LANG], - mState.locale[LOCALE_REGION]); - } else { - //mLocaleCombo.select(0); - selectLocale(mLocaleList.get(0)); - } - } finally { - mDisableUpdates--; - } - } - - private int getLocaleMatch() { - Locale locale = Locale.getDefault(); - if (locale != null) { - String currentLanguage = locale.getLanguage(); - String currentRegion = locale.getCountry(); - - final int count = mLocaleList.size(); - for (int l = 0 ; l < count ; l++) { - ResourceQualifier[] localeArray = mLocaleList.get(l); - LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG]; - RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION]; - - // there's always a ##/Other or ##/Any (which is the same, the region - // contains FAKE_REGION_VALUE). If we don't find a perfect region match - // we take the fake region. Since it's last in the list, this makes the - // test easy. - if (langQ.getValue().equals(currentLanguage) && - (regionQ.getValue().equals(currentRegion) || - regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) { - return l; - } - } - - // if no locale match the current local locale, it's likely that it is - // the default one which is the last one. - return count - 1; - } - - return -1; - } - - /** - * Updates the theme combo. - * This must be called from the UI thread. - */ - private void updateThemes() { - if (mListener == null) { - return; // can't do anything w/o it. - } - - ResourceRepository frameworkRes = mListener.getFrameworkResources(getRenderingTarget()); - - mDisableUpdates++; - - try { - if (mEditedFile != null) { - if (mState.theme == null || mState.theme.isEmpty() - || mListener.getIncludedWithin() != null) { - mState.theme = null; - getPreferredTheme(); - } - assert mState.theme != null; - } - - mThemeList.clear(); - - ArrayList<String> themes = new ArrayList<String>(); - ResourceRepository projectRes = mListener.getProjectResources(); - // in cases where the opened file is not linked to a project, this could be null. - if (projectRes != null) { - // get the configured resources for the project - Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = - mListener.getConfiguredProjectResources(); - - if (configuredProjectRes != null) { - // get the styles. - Map<String, ResourceValue> styleMap = configuredProjectRes.get( - ResourceType.STYLE); - - if (styleMap != null) { - // collect the themes out of all the styles, ie styles that extend, - // directly or indirectly a platform theme. - for (ResourceValue value : styleMap.values()) { - if (isTheme(value, styleMap, null)) { - String theme = value.getName(); - themes.add(theme); - } - } - - Collections.sort(themes); - - for (String theme : themes) { - if (!theme.startsWith(PREFIX_RESOURCE_REF)) { - theme = STYLE_RESOURCE_PREFIX + theme; - } - mThemeList.add(theme); - } - } - } - themes.clear(); - } - - // get the themes, and languages from the Framework. - if (frameworkRes != null) { - // get the configured resources for the framework - Map<ResourceType, Map<String, ResourceValue>> frameworResources = - frameworkRes.getConfiguredResources(getCurrentConfig()); - - if (frameworResources != null) { - // get the styles. - Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE); - - // collect the themes out of all the styles. - for (ResourceValue value : styles.values()) { - String name = value.getName(); - if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$ - themes.add(value.getName()); - } - } - - // sort them and add them to the combo - Collections.sort(themes); - - for (String theme : themes) { - if (!theme.startsWith(PREFIX_RESOURCE_REF)) { - theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; - } - mThemeList.add(theme); - } - - themes.clear(); - } - } - - // Migration: In the past we didn't store the style prefix in the settings; - // this meant we might lose track of whether the theme is a project style - // or a framework style. For now we need to migrate. Search through the - // theme list until we have a match - if (!mState.theme.startsWith(PREFIX_RESOURCE_REF)) { - String projectStyle = STYLE_RESOURCE_PREFIX + mState.theme; - String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + mState.theme; - for (String theme : mThemeList) { - if (theme.equals(projectStyle)) { - mState.theme = projectStyle; - break; - } else if (theme.equals(frameworkStyle)) { - mState.theme = frameworkStyle; - break; - } - } - } - - // TODO: Handle the case where you have a theme persisted that isn't available?? - // We could look up mState.theme and make sure it appears in the list! And if not, - // picking one. - - selectTheme(mState.theme); - } finally { - mDisableUpdates--; - } - } - - /** Returns the preferred theme, or null */ - @Nullable - String getPreferredTheme() { - if (mListener == null) { - return null; - } - - IProject project = mEditedFile.getProject(); - ManifestInfo manifest = ManifestInfo.get(project); - - // Look up the screen size for the current state - ScreenSize screenSize = null; - if (mState.device != null) { - List<State> states = mState.device.getAllStates(); - for (State state : states) { - ScreenSizeQualifier qualifier = - DeviceConfigHelper.getFolderConfig(state).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(mState.target, screenSize); - - String preferred = defaultTheme; - if (mState.theme == null) { - // If we are rendering a layout in included context, pick the theme - // from the outer layout instead - - String activity = getSelectedActivity(); - if (activity != null) { - Map<String, String> activityThemes = manifest.getActivityThemes(); - preferred = activityThemes.get(activity); - } - if (preferred == null) { - preferred = defaultTheme; - } - mState.theme = preferred; - } - - return preferred; - } - - // ---- getters for the config selection values ---- - - public FolderConfiguration getEditedConfig() { - return mEditedConfig; - } - - public FolderConfiguration getCurrentConfig() { - return mCurrentConfig; - } - - public void getCurrentConfig(FolderConfiguration config) { - config.set(mCurrentConfig); - } - - /** - * Returns the currently selected {@link Density}. This is guaranteed to be non null. - */ - public Density getDensity() { - if (mCurrentConfig != null) { - DensityQualifier qual = mCurrentConfig.getDensityQualifier(); - if (qual != null) { - // just a sanity check - Density d = qual.getValue(); - if (d != Density.NODPI) { - return d; - } - } - } - - // no config? return medium as the default density. - return Density.MEDIUM; - } - - /** - * Returns the current device xdpi. - */ - public float getXDpi() { - if (mState.device != null) { - - State currState = mState.device.getState(mState.stateName); - if (currState == null) { - currState = mState.device.getDefaultState(); - } - float dpi = (float) currState.getHardware().getScreen().getXdpi(); - if (Float.isNaN(dpi) == false) { - return dpi; - } - } - - // get the pixel density as the density. - return getDensity().getDpiValue(); - } - - /** - * Returns the current device ydpi. - */ - public float getYDpi() { - if (mState.device != null) { - - State currState = mState.device.getState(mState.stateName); - if (currState == null) { - currState = mState.device.getDefaultState(); - } - float dpi = (float) currState.getHardware().getScreen().getYdpi(); - if (Float.isNaN(dpi) == false) { - return dpi; - } - } - - // get the pixel density as the density. - return getDensity().getDpiValue(); - } - - public Rect getScreenBounds() { - // get the orientation from the current device config - ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier(); - ScreenOrientation orientation = ScreenOrientation.PORTRAIT; - if (qual != null) { - orientation = qual.getValue(); - } - - // get the device screen dimension - ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier(); - int s1, s2; - if (qual2 != null) { - s1 = qual2.getValue1(); - s2 = qual2.getValue2(); - } else { - s1 = 480; - s2 = 320; - } - - switch (orientation) { - default: - case PORTRAIT: - return new Rect(0, 0, s2, s1); - case LANDSCAPE: - return new Rect(0, 0, s1, s2); - case SQUARE: - return new Rect(0, 0, s1, s1); - } - } - - /** - * Returns the current theme, or null if the combo has no selection. - * - * @return the theme name, or null - */ - @Nullable - public String getThemeName() { - String theme = getSelectedTheme(); - if (theme != null) { - theme = ResourceHelper.styleToTheme(theme); - } - - return theme; - } - - @Nullable - String getSelectedTheme() { - return (String) mThemeCombo.getData(); - } - - /** - * Returns the current device string, or null if the combo has no selection. - * - * @return the device name, or null - */ - public String getDevice() { - Device device = getSelectedDevice(); - if (device != null) { - return device.getName(); - } - - return null; - } - - /** - * Returns whether the current theme selection is a project theme. - * <p/>The returned value is meaningless if {@link #getThemeName()} returns <code>null</code>. - * @return true for project theme, false for framework theme - */ - public boolean isProjectTheme() { - String theme = getSelectedTheme(); - if (theme != null) { - assert theme.startsWith(STYLE_RESOURCE_PREFIX) - || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX); - - return ResourceHelper.isProjectStyle(theme); - } - - return false; - } - - @Nullable - public IAndroidTarget getRenderingTarget() { - return getSelectedTarget(); - } - - /** - * Loads the list of {@link IAndroidTarget} and inits the UI with it. - */ - private void initTargets() { - mTargetList.clear(); - - 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 (mRenderingTarget != null) { - // use equals because the rendering could be from a previous SDK, so - // it may not be the same instance. - if (mRenderingTarget.equals(targets[i])) { - match = targets[i]; - } - } else if (mProjectTarget == targets[i]) { - match = targets[i]; - } - } - } - - if (match == null) { - selectTarget(null); - - // the rendering target is the same as the project. - mRenderingTarget = mProjectTarget; - } else { - selectTarget(match); - - // set the rendering target to the new object. - mRenderingTarget = match; - } - } - } - - /** - * Loads the list of {@link Device}s and inits the UI with it. - */ - private void initDevices() { - final Sdk sdk = Sdk.getCurrent(); - if (sdk != null) { - mDeviceList = sdk.getDevices(); - DeviceManager manager = sdk.getDeviceManager(); - // This method can be called more than once, so avoid duplicate entries - manager.unregisterListener(this); - manager.registerListener(this); - } 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); - } - } - - @Override - public void onDevicesChange() { - final Sdk sdk = Sdk.getCurrent(); - mDeviceList = sdk.getDevices(); - Display.getDefault().asyncExec(new Runnable() { - @Override - public void run() { - if (!mDeviceCombo.isDisposed()) { - addDeviceMenuListener(mDeviceCombo); - } - } - }); - } - - Image getOrientationIcon(ScreenOrientation orientation, boolean flip) { - IconFactory icons = IconFactory.getInstance(); - switch (orientation) { - case LANDSCAPE: - return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); - case SQUARE: - return icons.getIcon(ICON_SQUARE); - case PORTRAIT: - default: - return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); - } - } - - ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) { - IconFactory icons = IconFactory.getInstance(); - switch (orientation) { - case LANDSCAPE: - return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); - case SQUARE: - return icons.getImageDescriptor(ICON_SQUARE); - case PORTRAIT: - default: - return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); - } - } - - @NonNull - ScreenOrientation getOrientation(State state) { - FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state); - ScreenOrientation orientation = null; - if (config != null && config.getScreenOrientationQualifier() != null) { - orientation = config.getScreenOrientationQualifier().getValue(); - } - - if (orientation == null) { - orientation = ScreenOrientation.PORTRAIT; - } - - return orientation; - } - - void selectDeviceState(@Nullable State state) { - mOrientationCombo.setData(state); - - State nextState = getNextDeviceState(state); - mOrientationCombo.setImage(getOrientationIcon(getOrientation(state), - nextState != state)); - } - - State getSelectedDeviceState() { - return (State) mOrientationCombo.getData(); - } - - /** - * Selects a given {@link Device} in the device combo, if it is found. - * @param device the device to select - * @return true if the device was found. - */ - private boolean selectDevice(@Nullable Device device) { - mDeviceCombo.setData(device); - if (device != null) { - mDeviceCombo.setText(getDeviceLabel(device, true)); - } else { - mDeviceCombo.setText("Device"); - } - resizeToolBar(); - - return false; - } - - @Nullable - Device getSelectedDevice() { - return (Device) mDeviceCombo.getData(); - } - - /** - * Selects a state by name. - * @param name the name of the state to select. - */ - private void selectState(String name) { - Device device = getSelectedDevice(); - State state = null; - if (device != null) { - state = device.getState(name); - } - selectDeviceState(state); - } - - /** - * Called when the selection of the device combo changes. - * @param recomputeLayout - */ - private void onDeviceChange(boolean recomputeLayout) { - // because changing the content of a combo triggers a change event, respect the - // mDisableUpdates flag - if (mDisableUpdates > 0) { - return; - } - - String newConfigName = null; - - State prevState = getSelectedDeviceState(); - Device device = getSelectedDevice(); - if (mState.device != null && prevState != null && device != null) { - // get the previous config, so that we can look for a close match - FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState); - newConfigName = getClosestMatch(oldConfig, device.getAllStates()); - } - mState.device = device; - - selectState(newConfigName); - - computeCurrentConfig(); - - if (recomputeLayout) { - onDeviceConfigChange(); - } - } - - /** - * Attempts to find a close state among a list - * @param oldConfig the reference config. - * @param states the list of states to search through - * @return the name of the closest state match, or possibly null if no states are compatible - * (this can only happen if the states don't have a single qualifier that is the same). - */ - private String getClosestMatch(FolderConfiguration oldConfig, List<State> states) { - - // create 2 lists as we're going to go through one and put the - // candidates in the other. - ArrayList<State> list1 = new ArrayList<State>(); - ArrayList<State> list2 = new ArrayList<State>(); - - list1.addAll(states); - - final int count = FolderConfiguration.getQualifierCount(); - for (int i = 0 ; i < count ; i++) { - // compute the new candidate list by only taking states that have - // the same i-th qualifier as the old state - for (State s : list1) { - ResourceQualifier oldQualifier = oldConfig.getQualifier(i); - - FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); - ResourceQualifier newQualifier = folderConfig.getQualifier(i); - - if (oldQualifier == null) { - if (newQualifier == null) { - list2.add(s); - } - } else if (oldQualifier.equals(newQualifier)) { - list2.add(s); - } - } - - // at any moment if the new candidate list contains only one match, its name - // is returned. - if (list2.size() == 1) { - return list2.get(0).getName(); - } - - // if the list is empty, then all the new states failed. It is considered ok, and - // we move to the next qualifier anyway. This way, if a qualifier is different for - // all new states it is simply ignored. - if (list2.size() != 0) { - // move the candidates back into list1. - list1.clear(); - list1.addAll(list2); - list2.clear(); - } - } - - // the only way to reach this point is if there's an exact match. - // (if there are more than one, then there's a duplicate state and it doesn't matter, - // we take the first one). - if (list1.size() > 0) { - return list1.get(0).getName(); - } - - return null; - } - - /** - * Called when the device config selection changes. - */ - void onDeviceConfigChange() { - // because changing the content of a combo triggers a change event, respect the - // mDisableUpdates flag - if (mDisableUpdates > 0) { - return; - } - - if (computeCurrentConfig() && mListener != null) { - mListener.onConfigurationChange(); - mListener.onDevicePostChange(); - } - } - - /** - * Call back for language combo selection - */ - private void onLocaleChange() { - // because mLocaleList triggers onLocaleChange at each modification, the filling - // of the combo with data will trigger notifications, and we don't want that. - if (mDisableUpdates > 0) { - return; - } - - if (computeCurrentConfig() && mListener != null) { - mListener.onConfigurationChange(); - } - - // Store locale project-wide setting - saveRenderState(); - } - - /** - * Call back for api level combo selection - */ - private void onRenderingTargetChange() { - // because mApiCombo triggers onApiLevelChange at each modification, the filling - // of the combo with data will trigger notifications, and we don't want that. - if (mDisableUpdates > 0) { - return; - } - - // tell the listener a new rendering target is being set. Need to do this before updating - // mRenderingTarget. - if (mListener != null && mRenderingTarget != null) { - mListener.onRenderingTargetPreChange(mRenderingTarget); - } - - mRenderingTarget = getRenderingTarget(); - - boolean computeOk = computeCurrentConfig(); - - // force a theme update to reflect the new rendering target. - // This must be done after computeCurrentConfig since it'll depend on the currentConfig - // to figure out the theme list. - updateThemes(); - - // since the state is saved in computeCurrentConfig, we need to resave it since theme - // change could have impacted it. - saveState(); - - if (mListener != null && mRenderingTarget != null) { - mListener.onRenderingTargetPostChange(mRenderingTarget); - } - - // Store project-wide render-target setting - saveRenderState(); - - if (computeOk && mListener != null) { - mListener.onConfigurationChange(); - } - } - - /** - * Saves the current state and the current configuration - * - * @see #saveState() - */ - private boolean computeCurrentConfig() { - saveState(); - - if (mState.device != null) { - // get the device config from the device/state combos. - FolderConfiguration config = - DeviceConfigHelper.getFolderConfig(getSelectedDeviceState()); - - // replace the config with the one from the device - mCurrentConfig.set(config); - - // replace the locale qualifiers with the one coming from the locale combo - ResourceQualifier[] localeQualifiers = getSelectedLocale(); - if (localeQualifiers != null) { - mCurrentConfig.setLanguageQualifier( - (LanguageQualifier)localeQualifiers[LOCALE_LANG]); - mCurrentConfig.setRegionQualifier( - (RegionQualifier)localeQualifiers[LOCALE_REGION]); - } - - // Replace the UiMode with the selected one, if one is selected - UiMode uiMode = getSelectedUiMode(); - if (uiMode != null) { - mCurrentConfig.setUiModeQualifier(new UiModeQualifier(uiMode)); - } - - // Replace the NightMode with the selected one, if one is selected - NightMode night = getSelectedNightMode(); - if (night != null) { - mCurrentConfig.setNightModeQualifier(new NightModeQualifier(night)); - } - - // replace the API level by the selection of the combo - IAndroidTarget target = getRenderingTarget(); - if (target == null) { - target = mProjectTarget; - } - if (target != null) { - mCurrentConfig.setVersionQualifier( - new VersionQualifier(target.getVersion().getApiLevel())); - } - - return true; - } - - return false; - } - - void onThemeChange() { - saveState(); - - String theme = getSelectedTheme(); - if (theme != null && mListener != null) { - mListener.onThemeChange(); - } - } - - /** - * Returns whether the given <var>style</var> is a theme. - * This is done by making sure the parent is a theme. - * @param value the style to check - * @param styleMap the map of styles for the current project. Key is the style name. - * @param seen the map of styles we have already processed (or null if not yet - * initialized). Only the keys are significant (since there is no IdentityHashSet). - * @return True if the given <var>style</var> is a theme. - */ - private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap, - IdentityHashMap<ResourceValue, Boolean> seen) { - if (value instanceof StyleResourceValue) { - StyleResourceValue style = (StyleResourceValue)value; - - boolean frameworkStyle = false; - String parentStyle = style.getParentStyle(); - if (parentStyle == null) { - // if there is no specified parent style we look an implied one. - // For instance 'Theme.light' is implied child style of 'Theme', - // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' - String name = style.getName(); - int index = name.lastIndexOf('.'); - if (index != -1) { - parentStyle = name.substring(0, index); - } - } else { - // remove the useless @ if it's there - if (parentStyle.startsWith("@")) { - parentStyle = parentStyle.substring(1); - } - - // check for framework identifier. - if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) { - frameworkStyle = true; - parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length()); - } - - // at this point we could have the format style/<name>. we want only the name - if (parentStyle.startsWith("style/")) { - parentStyle = parentStyle.substring("style/".length()); - } - } - - if (parentStyle != null) { - if (frameworkStyle) { - // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' - return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); - } else { - // if it's a project style, we check this is a theme. - ResourceValue parentValue = styleMap.get(parentStyle); - - // also prevent stack overflow in case the dev mistakenly declared - // the parent of the style as the style itself. - if (parentValue != null && parentValue.equals(value) == false) { - if (seen == null) { - seen = new IdentityHashMap<ResourceValue, Boolean>(); - seen.put(value, Boolean.TRUE); - } else if (seen.containsKey(parentValue)) { - return false; - } - seen.put(parentValue, Boolean.TRUE); - return isTheme(parentValue, styleMap, seen); - } - } - } - } - - return false; - } - - /** - * Checks whether the current edited file is the best match for a given config. - * <p/> - * This tests against other versions of the same layout in the project. - * <p/> - * The given config must be compatible with the current edited file. - * @param config the config to test. - * @return true if the current edited file is the best match in the project for the - * given config. - */ - private boolean isCurrentFileBestMatchFor(FolderConfiguration config) { - ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), - ResourceFolderType.LAYOUT, config); - - if (match != null) { - return match.getFile().equals(mEditedFile); - } else { - // if we stop here that means the current file is not even a match! - AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config."); - } - - return false; - } - - /** - * Resets the configuration chooser to reflect the given file configuration. This is - * intended to be used by the "Show Included In" functionality where the user has - * picked a non-default configuration (such as a particular landscape layout) and the - * configuration chooser must be switched to a landscape layout. This method will - * trigger a model change. - * <p> - * This will NOT trigger a redraw event! - * <p> - * FIXME: We are currently setting the configuration file to be the configuration for - * the "outer" (the including) file, rather than the inner file, which is the file the - * user is actually editing. We need to refine this, possibly with a way for the user - * to choose which configuration they are editing. And in particular, we should be - * filtering the configuration chooser to only show options in the outer configuration - * that are compatible with the inner included file. - * - * @param file the file to be configured - */ - public void resetConfigFor(IFile file) { - setFile(file); - mEditedConfig = null; - onXmlModelLoaded(); - } - - /** - * Syncs this configuration to the project wide locale and render target settings. The - * locale may ignore the project-wide setting if it is a locale-specific - * configuration. - * - * @return true if one or both of the toggles were changed, false if there were no - * changes - */ - public boolean syncRenderState() { - if (mEditedConfig == null) { - // Startup; ignore - return false; - } - - boolean localeChanged = false; - boolean renderTargetChanged = false; - - // When a page is re-activated, force the toggles to reflect the current project - // state - - Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState(); - - // Only sync the locale if this layout is not already a locale-specific layout! - if (pair != null && !isLocaleSpecificLayout()) { - ResourceQualifier[] locale = pair.getFirst(); - if (locale != null) { - localeChanged = setLocaleCombo(locale[0], locale[1]); - } - } - - // Sync render target - IAndroidTarget target = pair != null ? pair.getSecond() : getSelectedTarget(); - if (target != null) { - if (getRenderingTarget() != target) { - selectTarget(target); - renderTargetChanged = true; - } - } - - if (!renderTargetChanged && !localeChanged) { - return false; - } - - // Update the locale and/or the render target. This code contains a logical - // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined - // such that we don't duplicate work. - - if (renderTargetChanged) { - if (mListener != null && mRenderingTarget != null) { - mListener.onRenderingTargetPreChange(mRenderingTarget); - } - mRenderingTarget = target; - } - - // Compute the new configuration; we want to do this both for locale changes - // and for render targets. - boolean computeOk = computeCurrentConfig(); - - if (renderTargetChanged) { - // force a theme update to reflect the new rendering target. - // This must be done after computeCurrentConfig since it'll depend on the currentConfig - // to figure out the theme list. - updateThemes(); - - if (mListener != null && mRenderingTarget != null) { - mListener.onRenderingTargetPostChange(mRenderingTarget); - } - } - - // For both locale and render target changes - if (computeOk && mListener != null) { - mListener.onConfigurationChange(); - } - - return true; - } - - /** - * Loads the render state (the locale and the render target, which are shared among - * all the layouts meaning that changing it in one will change it in all) and returns - * the current project-wide locale and render target to be used. - * - * @return a pair of locale resource qualifiers and render target - */ - private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() { - IProject project = mEditedFile.getProject(); - if (!project.isAccessible()) { - return null; - } - - try { - String data = project.getPersistentProperty(NAME_RENDER_STATE); - if (data != null) { - ResourceQualifier[] locale = null; - IAndroidTarget target = null; - - String[] values = data.split(SEP); - if (values.length == 2) { - locale = new ResourceQualifier[2]; - String locales[] = values[0].split(SEP_LOCALE); - if (locales.length >= 2) { - if (locales[0].length() > 0) { - locale[0] = new LanguageQualifier(locales[0]); - } - if (locales[1].length() > 0) { - locale[1] = new RegionQualifier(locales[1]); - } - } - target = stringToTarget(values[1]); - - // See if we should "correct" the rendering target to a better version. - // If you're using a pre-release version of the render target, and a - // final release is available and installed, we should switch to that - // one instead. - if (target != null) { - AndroidVersion version = target.getVersion(); - if (version.getCodename() != null && mTargetList != null) { - int targetApiLevel = version.getApiLevel() + 1; - for (IAndroidTarget t : mTargetList) { - if (t.getVersion().getApiLevel() == targetApiLevel - && t.isPlatform()) { - target = t; - break; - } - } - } - } - } - - return Pair.of(locale, target); - } - - ResourceQualifier[] any = new ResourceQualifier[] { - new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), - new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) - }; - - return Pair.of(any, findDefaultRenderTarget()); - } catch (CoreException e) { - AdtPlugin.log(e, null); - } - - return null; - } - - /** Returns true if the current layout is locale-specific */ - private boolean isLocaleSpecificLayout() { - return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null; - } - - /** - * Saves the render state (the current locale and render target settings) into the - * project wide settings storage - */ - private void saveRenderState() { - IProject project = mEditedFile.getProject(); - try { - ResourceQualifier[] locale = getSelectedLocale(); - IAndroidTarget target = getRenderingTarget(); - - // Generate a persistent string from locale+target - StringBuilder sb = new StringBuilder(); - if (locale != null) { - if (locale[0] != null && locale[1] != null) { - // locale[0]/[1] can be null sometimes when starting Eclipse - sb.append(((LanguageQualifier) locale[0]).getValue()); - sb.append(SEP_LOCALE); - sb.append(((RegionQualifier) locale[1]).getValue()); - } - } - sb.append(SEP); - if (target != null) { - sb.append(targetToString(target)); - sb.append(SEP); - } - - String data = sb.toString(); - project.setPersistentProperty(NAME_RENDER_STATE, data); - } catch (CoreException e) { - AdtPlugin.log(e, null); - } - } - - // ---- Implements SelectionListener ---- - - @Override - public void widgetSelected(SelectionEvent e) { - if (mDisableUpdates > 0) { - return; - } - - final Object source = e.getSource(); - if (source == mOrientationCombo) { - gotoNextState(); - } - } - - @Override - public void widgetDefaultSelected(SelectionEvent e) { - } - - /** Returns the file whose rendering is being configured by this configuration composite */ - IFile getEditedFile() { - return mEditedFile; - } - - UiMode getSelectedUiMode() { - return mState.uiMode; - } - - NightMode getSelectedNightMode() { - return mState.night; - } - - void selectNightMode(NightMode night) { - mState.night = night; - if (computeCurrentConfig() && mListener != null) { - mListener.onConfigurationChange(); - } - } - - void selectUiMode(UiMode uiMode) { - mState.uiMode = uiMode; - if (computeCurrentConfig() && mListener != null) { - mListener.onConfigurationChange(); - } - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java new file mode 100644 index 0000000..dc64b36 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java @@ -0,0 +1,797 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.DeviceConfigHelper; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.NightModeQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.common.resources.configuration.UiModeQualifier; +import com.android.ide.common.resources.configuration.VersionQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.resources.Density; +import com.android.resources.NightMode; +import com.android.resources.ResourceFolderType; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenSize; +import com.android.resources.UiMode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.State; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.util.SparseIntArray; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.ui.IEditorPart; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Produces matches for configurations */ +public class ConfigurationMatcher { + private final ConfigurationChooser mConfigChooser; + + ConfigurationMatcher(ConfigurationChooser chooser) { + mConfigChooser = chooser; + } + + // ---- Finding matching configurations ---- + + private static class ConfigBundle { + private final FolderConfiguration config; + private int localeIndex; + private int dockModeIndex; + private int nightModeIndex; + + private ConfigBundle() { + config = new FolderConfiguration(); + } + + private ConfigBundle(ConfigBundle bundle) { + config = new FolderConfiguration(); + config.set(bundle.config); + localeIndex = bundle.localeIndex; + dockModeIndex = bundle.dockModeIndex; + nightModeIndex = bundle.nightModeIndex; + } + } + + private static class ConfigMatch { + final FolderConfiguration testConfig; + final Device device; + final State state; + final ConfigBundle bundle; + + public ConfigMatch(@NonNull FolderConfiguration testConfig, @NonNull Device device, + @NonNull State state, @NonNull ConfigBundle bundle) { + this.testConfig = testConfig; + this.device = device; + this.state = state; + this.bundle = bundle; + } + + @Override + public String toString() { + return device.getName() + " - " + state.getName(); + } + } + + /** + * Checks whether the current edited file is the best match for a given config. + * <p> + * This tests against other versions of the same layout in the project. + * <p> + * The given config must be compatible with the current edited file. + * @param config the config to test. + * @return true if the current edited file is the best match in the project for the + * given config. + */ + boolean isCurrentFileBestMatchFor(FolderConfiguration config) { + ProjectResources resources = mConfigChooser.getResources(); + IFile editedFile = mConfigChooser.getEditedFile(); + ResourceFile match = resources.getMatchingFile(editedFile.getName(), + ResourceFolderType.LAYOUT, config); + + if (match != null) { + return match.getFile().equals(editedFile); + } else { + // if we stop here that means the current file is not even a match! + AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config."); + } + + return false; + } + + /** + * Adapts the current device/config selection so that it's compatible with + * the configuration. + * <p> + * If the current selection is compatible, nothing is changed. + * <p> + * If it's not compatible, configs from the current devices are tested. + * <p> + * If none are compatible, it reverts to + * {@link #findAndSetCompatibleConfig(boolean)} + */ + void adaptConfigSelection(boolean needBestMatch) { + // check the device config (ie sans locale) + boolean needConfigChange = true; // if still true, we need to find another config. + boolean currentConfigIsCompatible = false; + Configuration configuration = mConfigChooser.getConfiguration(); + State selectedState = configuration.getDeviceState(); + FolderConfiguration editedConfig = configuration.getEditedConfig(); + if (selectedState != null) { + FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState); + if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) { + currentConfigIsCompatible = true; // current config is compatible + if (!needBestMatch || isCurrentFileBestMatchFor(currentConfig)) { + needConfigChange = false; + } + } + } + + if (needConfigChange) { + List<Locale> localeList = mConfigChooser.getLocaleList(); + + // if the current state/locale isn't a correct match, then + // look for another state/locale in the same device. + FolderConfiguration testConfig = new FolderConfiguration(); + + // first look in the current device. + State matchState = null; + int localeIndex = -1; + Device device = configuration.getDevice(); + if (device != null) { + mainloop: for (State state : device.getAllStates()) { + testConfig.set(DeviceConfigHelper.getFolderConfig(state)); + + // loop on the locales. + for (int i = 0 ; i < localeList.size() ; i++) { + Locale locale = localeList.get(i); + + // update the test config with the locale qualifiers + testConfig.setLanguageQualifier(locale.language); + testConfig.setRegionQualifier(locale.region); + + if (editedConfig.isMatchFor(testConfig) && + isCurrentFileBestMatchFor(testConfig)) { + matchState = state; + localeIndex = i; + break mainloop; + } + } + } + } + + if (matchState != null) { + configuration.setDeviceState(matchState, true); + Locale locale = localeList.get(localeIndex); + configuration.setLocale(locale, true); + mConfigChooser.selectDeviceState(matchState); + mConfigChooser.selectLocale(locale); + configuration.syncFolderConfig(); + } else { + // no match in current device with any state/locale + // attempt to find another device that can display this + // particular state. + findAndSetCompatibleConfig(currentConfigIsCompatible); + } + } + } + + /** + * Finds a device/config that can display a configuration. + * <p> + * Once found the device and config combos are set to the config. + * <p> + * If there is no compatible configuration, a custom one is created. + * + * @param favorCurrentConfig if true, and no best match is found, don't + * change the current config. This must only be true if the + * current config is compatible. + */ + void findAndSetCompatibleConfig(boolean favorCurrentConfig) { + List<Locale> localeList = mConfigChooser.getLocaleList(); + List<Device> deviceList = mConfigChooser.getDeviceList(); + Configuration configuration = mConfigChooser.getConfiguration(); + FolderConfiguration editedConfig = configuration.getEditedConfig(); + FolderConfiguration currentConfig = configuration.getFullConfig(); + + // list of compatible device/state/locale + List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>(); + + // list of actual best match (ie the file is a best match for the + // device/state) + List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>(); + + // get a locale that match the host locale roughly (may not be exact match on the region.) + int localeHostMatch = getLocaleMatch(); + + // build a list of combinations of non standard qualifiers to add to each device's + // qualifier set when testing for a match. + // These qualifiers are: locale, night-mode, car dock. + List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200); + + // If the edited file has locales, then we have to select a matching locale from + // the list. + // However, if it doesn't, we don't randomly take the first locale, we take one + // matching the current host locale (making sure it actually exist in the project) + int start, max; + if (editedConfig.getLanguageQualifier() != null || localeHostMatch == -1) { + // add all the locales + start = 0; + max = localeList.size(); + } else { + // only add the locale host match + start = localeHostMatch; + max = localeHostMatch + 1; // test is < + } + + for (int i = start ; i < max ; i++) { + Locale l = localeList.get(i); + + ConfigBundle bundle = new ConfigBundle(); + bundle.config.setLanguageQualifier(l.language); + bundle.config.setRegionQualifier(l.region); + + bundle.localeIndex = i; + configBundles.add(bundle); + } + + // add the dock mode to the bundle combinations. + addDockModeToBundles(configBundles); + + // add the night mode to the bundle combinations. + addNightModeToBundles(configBundles); + + addRenderTargetToBundles(configBundles); + + for (Device device : deviceList) { + for (State state : device.getAllStates()) { + + // loop on the list of config bundles to create full + // configurations. + FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state); + for (ConfigBundle bundle : configBundles) { + // create a new config with device config + FolderConfiguration testConfig = new FolderConfiguration(); + testConfig.set(stateConfig); + + // add on top of it, the extra qualifiers from the bundle + testConfig.add(bundle.config); + + if (editedConfig.isMatchFor(testConfig)) { + // this is a basic match. record it in case we don't + // find a match + // where the edited file is a best config. + anyMatches.add(new ConfigMatch(testConfig, device, state, bundle)); + + if (isCurrentFileBestMatchFor(testConfig)) { + // this is what we want. + bestMatches.add(new ConfigMatch(testConfig, device, state, bundle)); + } + } + } + } + } + + if (bestMatches.size() == 0) { + if (favorCurrentConfig) { + // quick check + if (!editedConfig.isMatchFor(currentConfig)) { + AdtPlugin.log(IStatus.ERROR, + "favorCurrentConfig can only be true if the current config is compatible"); + } + + // just display the warning + AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + String.format( + "'%1$s' is not a best match for any device/locale combination.", + editedConfig.toDisplayString()), + String.format( + "Displaying it with '%1$s'", + currentConfig.toDisplayString())); + } else if (anyMatches.size() > 0) { + // select the best device anyway. + ConfigMatch match = selectConfigMatch(anyMatches); + 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); + + mConfigChooser.selectDevice(configuration.getDevice()); + mConfigChooser.selectDeviceState(configuration.getDeviceState()); + mConfigChooser.selectLocale(configuration.getLocale()); + + configuration.syncFolderConfig(); + + // TODO: display a better warning! + AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + String.format( + "'%1$s' is not a best match for any device/locale combination.", + editedConfig.toDisplayString()), + String.format( + "Displaying it with '%1$s' which is compatible, but will " + + "actually be displayed with another more specific version of " + + "the layout.", + currentConfig.toDisplayString())); + + } else { + // TODO: there is no device/config able to display the layout, create one. + // For the base config values, we'll take the first device and state, + // and replace whatever qualifier required by the layout file. + } + } else { + ConfigMatch match = selectConfigMatch(bestMatches); + 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()); + } + } + + private void addRenderTargetToBundles(List<ConfigBundle> configBundles) { + Pair<Locale, IAndroidTarget> state = Configuration.loadRenderState(mConfigChooser); + if (state != null) { + IAndroidTarget target = state.getSecond(); + if (target != null) { + int apiLevel = target.getVersion().getApiLevel(); + for (ConfigBundle bundle : configBundles) { + bundle.config.setVersionQualifier( + new VersionQualifier(apiLevel)); + } + } + } + } + + private void addDockModeToBundles(List<ConfigBundle> addConfig) { + ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); + + // loop on each item and for each, add all variations of the dock modes + for (ConfigBundle bundle : addConfig) { + int index = 0; + for (UiMode mode : UiMode.values()) { + ConfigBundle b = new ConfigBundle(bundle); + b.config.setUiModeQualifier(new UiModeQualifier(mode)); + b.dockModeIndex = index++; + list.add(b); + } + } + + addConfig.clear(); + addConfig.addAll(list); + } + + private void addNightModeToBundles(List<ConfigBundle> addConfig) { + ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); + + // loop on each item and for each, add all variations of the night modes + for (ConfigBundle bundle : addConfig) { + int index = 0; + for (NightMode mode : NightMode.values()) { + ConfigBundle b = new ConfigBundle(bundle); + b.config.setNightModeQualifier(new NightModeQualifier(mode)); + b.nightModeIndex = index++; + list.add(b); + } + } + + addConfig.clear(); + addConfig.addAll(list); + } + + private int getLocaleMatch() { + java.util.Locale defaultLocale = java.util.Locale.getDefault(); + if (defaultLocale != null) { + String currentLanguage = defaultLocale.getLanguage(); + String currentRegion = defaultLocale.getCountry(); + + List<Locale> localeList = mConfigChooser.getLocaleList(); + final int count = localeList.size(); + for (int l = 0; l < count; l++) { + Locale locale = localeList.get(l); + LanguageQualifier langQ = locale.language; + RegionQualifier regionQ = locale.region; + + // there's always a ##/Other or ##/Any (which is the same, the region + // contains FAKE_REGION_VALUE). If we don't find a perfect region match + // we take the fake region. Since it's last in the list, this makes the + // test easy. + if (langQ.getValue().equals(currentLanguage) && + (regionQ.getValue().equals(currentRegion) || + regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) { + return l; + } + } + + // if no locale match the current local locale, it's likely that it is + // the default one which is the last one. + return count - 1; + } + + return -1; + } + + private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { + // API 11-13: look for a x-large device + 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 { + // lets look for a high density device + Collections.sort(matches, new PhoneConfigComparator()); + } + + // Look at the currently active editor to see if it's a layout editor, and if so, + // look up its configuration and if the configuration is in our match list, + // use it. This means we "preserve" the current configuration when you open + // new layouts. + IEditorPart activeEditor = AdtUtils.getActiveEditor(); + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); + if (delegate != null + // (Only do this when the two files are in the same project) + && delegate.getEditor().getProject() == mConfigChooser.getProject()) { + FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); + if (configuration != null) { + for (ConfigMatch match : matches) { + if (configuration.equals(match.testConfig)) { + return match; + } + } + } + } + + // the list has been sorted so that the first item is the best config + return matches.get(0); + } + + /** Return the default render target to use, or null if no strong preference */ + @Nullable + static IAndroidTarget findDefaultRenderTarget(@NonNull IProject project) { + // Default to layoutlib version 5 + Sdk current = Sdk.getCurrent(); + if (current != null) { + IAndroidTarget projectTarget = current.getTarget(project); + int minProjectApi = Integer.MAX_VALUE; + if (projectTarget != null) { + if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) { + // Renderable non-platform targets are all going to be adequate (they + // will have at least version 5 of layoutlib) so use the project + // target as the render target. + return projectTarget; + } + + if (projectTarget.getVersion().isPreview() + && projectTarget.hasRenderingLibrary()) { + // If the project target is a preview version, then just use it + return projectTarget; + } + + minProjectApi = projectTarget.getVersion().getApiLevel(); + } + + // We want to pick a render target that contains at least version 5 (and + // preferably version 6) of the layout library. To do this, we go through the + // targets and pick the -smallest- API level that is both simultaneously at + // least as big as the project API level, and supports layoutlib level 5+. + IAndroidTarget best = null; + int bestApiLevel = Integer.MAX_VALUE; + + for (IAndroidTarget target : current.getTargets()) { + // Non-platform targets are not chosen as the default render target + if (!target.isPlatform()) { + continue; + } + + int apiLevel = target.getVersion().getApiLevel(); + + // Ignore targets that have a lower API level than the minimum project + // API level: + if (apiLevel < minProjectApi) { + continue; + } + + // Look up the layout lib API level. This property is new so it will only + // be defined for version 6 or higher, which means non-null is adequate + // to see if this target is eligible: + String property = target.getProperty(PkgProps.LAYOUTLIB_API); + // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate: + if (property != null || apiLevel >= 11) { + if (apiLevel < bestApiLevel) { + bestApiLevel = apiLevel; + best = target; + } + } + } + + return best; + } + + return null; + } + + /** + * Attempts to find a close state among a list + * + * @param oldConfig the reference config. + * @param states the list of states to search through + * @return the name of the closest state match, or possibly null if no states are compatible + * (this can only happen if the states don't have a single qualifier that is the same). + */ + @Nullable + static String getClosestMatch(@NonNull FolderConfiguration oldConfig, + @NonNull List<State> states) { + + // create 2 lists as we're going to go through one and put the + // candidates in the other. + List<State> list1 = new ArrayList<State>(states.size()); + List<State> list2 = new ArrayList<State>(states.size()); + + list1.addAll(states); + + final int count = FolderConfiguration.getQualifierCount(); + for (int i = 0 ; i < count ; i++) { + // compute the new candidate list by only taking states that have + // the same i-th qualifier as the old state + for (State s : list1) { + ResourceQualifier oldQualifier = oldConfig.getQualifier(i); + + FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); + ResourceQualifier newQualifier = + folderConfig != null ? folderConfig.getQualifier(i) : null; + + if (oldQualifier == null) { + if (newQualifier == null) { + list2.add(s); + } + } else if (oldQualifier.equals(newQualifier)) { + list2.add(s); + } + } + + // at any moment if the new candidate list contains only one match, its name + // is returned. + if (list2.size() == 1) { + return list2.get(0).getName(); + } + + // if the list is empty, then all the new states failed. It is considered ok, and + // we move to the next qualifier anyway. This way, if a qualifier is different for + // all new states it is simply ignored. + if (list2.size() != 0) { + // move the candidates back into list1. + list1.clear(); + list1.addAll(list2); + list2.clear(); + } + } + + // the only way to reach this point is if there's an exact match. + // (if there are more than one, then there's a duplicate state and it doesn't matter, + // we take the first one). + if (list1.size() > 0) { + return list1.get(0).getName(); + } + + return null; + } + + /** + * Returns the layout {@link IFile} which best matches the configuration + * selected in the given configuration chooser. + * + * @param chooser the associated configuration chooser holding project state + * @return the file which best matches the settings + */ + @Nullable + public static IFile getBestFileMatch(ConfigurationChooser chooser) { + // get the resources of the file's project. + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources resources = manager.getProjectResources(chooser.getProject()); + if (resources == null) { + return null; + } + + // From the resources, look for a matching file + String name = chooser.getEditedFile().getName(); + FolderConfiguration config = chooser.getConfiguration().getFullConfig(); + ResourceFile match = resources.getMatchingFile(name, ResourceFolderType.LAYOUT, config); + + if (match != null) { + // In Eclipse, the match's file is always an instance of IFileWrapper + return ((IFileWrapper) match.getFile()).getIFile(); + } + + return null; + } + + /** + * Note: this comparator imposes orderings that are inconsistent with equals. + */ + private static class TabletConfigComparator implements Comparator<ConfigMatch> { + @Override + public int compare(ConfigMatch o1, ConfigMatch o2) { + FolderConfiguration config1 = o1 != null ? o1.testConfig : null; + FolderConfiguration config2 = o2 != null ? o2.testConfig : null; + if (config1 == null) { + if (config2 == null) { + return 0; + } else { + return -1; + } + } else if (config2 == null) { + return 1; + } + + ScreenSizeQualifier size1 = config1.getScreenSizeQualifier(); + ScreenSizeQualifier size2 = config2.getScreenSizeQualifier(); + ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL; + ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL; + + // X-LARGE is better than all others (which are considered identical) + // if both X-LARGE, then LANDSCAPE is better than all others (which are identical) + + if (ss1 == ScreenSize.XLARGE) { + if (ss2 == ScreenSize.XLARGE) { + ScreenOrientationQualifier orientation1 = + config1.getScreenOrientationQualifier(); + ScreenOrientation so1 = orientation1.getValue(); + if (so1 == null) { + so1 = ScreenOrientation.PORTRAIT; + } + ScreenOrientationQualifier orientation2 = + config2.getScreenOrientationQualifier(); + ScreenOrientation so2 = orientation2.getValue(); + if (so2 == null) { + so2 = ScreenOrientation.PORTRAIT; + } + + if (so1 == ScreenOrientation.LANDSCAPE) { + if (so2 == ScreenOrientation.LANDSCAPE) { + return 0; + } else { + return -1; + } + } else if (so2 == ScreenOrientation.LANDSCAPE) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } else if (ss2 == ScreenSize.XLARGE) { + return 1; + } else { + return 0; + } + } + } + + /** + * Note: this comparator imposes orderings that are inconsistent with equals. + */ + private static class PhoneConfigComparator implements Comparator<ConfigMatch> { + + private SparseIntArray mDensitySort = new SparseIntArray(4); + + public PhoneConfigComparator() { + // put the sort order for the density. + mDensitySort.put(Density.HIGH.getDpiValue(), 1); + mDensitySort.put(Density.MEDIUM.getDpiValue(), 2); + mDensitySort.put(Density.XHIGH.getDpiValue(), 3); + mDensitySort.put(Density.LOW.getDpiValue(), 4); + } + + @Override + public int compare(ConfigMatch o1, ConfigMatch o2) { + FolderConfiguration config1 = o1 != null ? o1.testConfig : null; + FolderConfiguration config2 = o2 != null ? o2.testConfig : null; + if (config1 == null) { + if (config2 == null) { + return 0; + } else { + return -1; + } + } else if (config2 == null) { + return 1; + } + + int dpi1 = Density.DEFAULT_DENSITY; + int dpi2 = Density.DEFAULT_DENSITY; + + DensityQualifier dpiQualifier1 = config1.getDensityQualifier(); + if (dpiQualifier1 != null) { + Density value = dpiQualifier1.getValue(); + dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; + } + dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/); + + DensityQualifier dpiQualifier2 = config2.getDensityQualifier(); + if (dpiQualifier2 != null) { + Density value = dpiQualifier2.getValue(); + dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; + } + dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/); + + if (dpi1 == dpi2) { + // portrait is better + ScreenOrientation so1 = ScreenOrientation.PORTRAIT; + ScreenOrientationQualifier orientationQualifier1 = + config1.getScreenOrientationQualifier(); + if (orientationQualifier1 != null) { + so1 = orientationQualifier1.getValue(); + if (so1 == null) { + so1 = ScreenOrientation.PORTRAIT; + } + } + ScreenOrientation so2 = ScreenOrientation.PORTRAIT; + ScreenOrientationQualifier orientationQualifier2 = + config2.getScreenOrientationQualifier(); + if (orientationQualifier2 != null) { + so2 = orientationQualifier2.getValue(); + if (so2 == null) { + so2 = ScreenOrientation.PORTRAIT; + } + } + + if (so1 == ScreenOrientation.PORTRAIT) { + if (so2 == ScreenOrientation.PORTRAIT) { + return 0; + } else { + return -1; + } + } else if (so2 == ScreenOrientation.PORTRAIT) { + return 1; + } else { + return 0; + } + } + + return dpi1 - dpi2; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java new file mode 100644 index 0000000..30f7dc2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java @@ -0,0 +1,157 @@ +/* + * 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.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.internal.editors.IconFactory; +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.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.ui.PartInitException; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@linkplain ConfigurationMenuListener} class is responsible for + * generating the configuration menu in the {@link ConfigurationChooser}. + */ +class ConfigurationMenuListener extends SelectionAdapter { + private static final String ICON_NEW_CONFIG = "newConfig"; //$NON-NLS-1$ + private static final int ACTION_SELECT_CONFIG = 1; + private static final int ACTION_CREATE_CONFIG_FILE = 2; + + private final ConfigurationChooser mConfigChooser; + private final int mAction; + private final IFile mResource; + + ConfigurationMenuListener( + @NonNull ConfigurationChooser configChooser, + int action, + @Nullable IFile resource) { + mConfigChooser = configChooser; + mAction = action; + mResource = resource; + } + + @Override + public void widgetSelected(SelectionEvent e) { + switch (mAction) { + case ACTION_SELECT_CONFIG: { + try { + AdtPlugin.openFile(mResource, null, false); + } catch (PartInitException ex) { + AdtPlugin.log(ex, null); + } + break; + } + case ACTION_CREATE_CONFIG_FILE: { + ConfigurationClient client = mConfigChooser.getClient(); + if (client != null) { + client.createConfigFile(); + } + break; + } + default: assert false : mAction; + } + } + + static void show(ConfigurationChooser chooser, ToolItem combo) { + Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + + // 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); + } + + ResourceManager manager = ResourceManager.getInstance(); + for (final IFile resource : variations) { + MenuItem item = new MenuItem(menu, SWT.CHECK); + + IFolder parent = (IFolder) resource.getParent(); + ResourceFolder parentResource = manager.getResourceFolder(parent); + FolderConfiguration configuration = parentResource.getConfiguration(); + String title = configuration.toDisplayString(); + item.setText(title); + + boolean selected = file.equals(resource); + if (selected) { + item.setSelection(true); + item.setEnabled(false); + } + + item.addSelectionListener(new ConfigurationMenuListener(chooser, + ACTION_SELECT_CONFIG, resource)); + } + + Configuration configuration = chooser.getConfiguration(); + if (!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..."); + 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(); + Point location = new Point(bounds.x, bounds.y + bounds.height); + location = combo.getParent().toDisplay(location); + menu.setLocation(location.x, location.y); + menu.setVisible(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java new file mode 100644 index 0000000..8707c0e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.devices.Device; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * The {@linkplain DeviceMenuListener} class is responsible for generating the device + * menu in the {@link ConfigurationChooser}. + */ +class DeviceMenuListener extends SelectionAdapter { + private final ConfigurationChooser mConfigChooser; + private final Device mDevice; + + DeviceMenuListener( + @NonNull ConfigurationChooser configChooser, + @Nullable Device device) { + mConfigChooser = configChooser; + mDevice = device; + } + + @Override + public void widgetSelected(SelectionEvent e) { + mConfigChooser.selectDevice(mDevice); + mConfigChooser.onDeviceChange(); + } + + static void show(final ConfigurationChooser chooser, ToolItem combo) { + Configuration configuration = chooser.getConfiguration(); + Device current = configuration.getDevice(); + Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + + AvdManager avdManager = Sdk.getCurrent().getAvdManager(); + AvdInfo[] avds = avdManager.getValidAvds(); + List<Device> deviceList = chooser.getDeviceList(); + boolean separatorNeeded = false; + for (AvdInfo avd : avds) { + for (Device device : deviceList) { + if (device.getManufacturer().equals(avd.getDeviceManufacturer()) + && device.getName().equals(avd.getDeviceName())) { + separatorNeeded = true; + MenuItem item = new MenuItem(menu, SWT.CHECK); + item.setText(avd.getName()); + item.setSelection(current == device); + + item.addSelectionListener(new DeviceMenuListener(chooser, device)); + } + } + } + + if (separatorNeeded) { + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + } + + // Group the devices by manufacturer, then put them in the menu + if (!deviceList.isEmpty()) { + Map<String, List<Device>> manufacturers = new TreeMap<String, List<Device>>(); + for (Device device : deviceList) { + List<Device> devices; + if (manufacturers.containsKey(device.getManufacturer())) { + devices = manufacturers.get(device.getManufacturer()); + } else { + devices = new ArrayList<Device>(); + manufacturers.put(device.getManufacturer(), devices); + } + devices.add(device); + } + for (List<Device> devices : manufacturers.values()) { + Menu manufacturerMenu = menu; + if (manufacturers.size() > 1) { + MenuItem item = new MenuItem(menu, SWT.CASCADE); + item.setText(devices.get(0).getManufacturer()); + manufacturerMenu = new Menu(menu); + item.setMenu(manufacturerMenu); + } + for (final Device d : devices) { + MenuItem deviceItem = new MenuItem(manufacturerMenu, SWT.CHECK); + deviceItem.setText(d.getName()); + deviceItem.setSelection(current == d); + + deviceItem.addSelectionListener(new DeviceMenuListener(chooser, d)); + } + } + } + + Rectangle bounds = combo.getBounds(); + Point location = new Point(bounds.x, bounds.y + bounds.height); + location = combo.getParent().toDisplay(location); + menu.setLocation(location.x, location.y); + menu.setVisible(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java index 82bd054..97ff668 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java @@ -50,6 +50,7 @@ public final class LayoutCreatorDialog extends GridDialog { /** * Creates a dialog, and init the UI from a {@link FolderConfiguration}. * @param parentShell the parent {@link Shell}. + * @param fileName the filename associated with the configuration * @param config The starting configuration. */ public LayoutCreatorDialog(Shell parentShell, String fileName, FolderConfiguration config) { @@ -127,6 +128,11 @@ public final class LayoutCreatorDialog extends GridDialog { resetStatus(); } + /** + * Sets the edited configuration on the given configuration parameter + * + * @param config the configuration to apply the current edits to + */ public void getConfiguration(FolderConfiguration config) { config.set(mConfig); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java new file mode 100644 index 0000000..157c8c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import static com.android.ide.common.resources.configuration.LanguageQualifier.FAKE_LANG_VALUE; +import static com.android.ide.common.resources.configuration.RegionQualifier.FAKE_REGION_VALUE; + +import com.android.annotations.NonNull; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.google.common.base.Objects; + +import org.eclipse.swt.graphics.Image; + +/** A language,region pair */ +public class Locale { + /** A special marker region qualifier representing any region */ + public static final RegionQualifier ANY_REGION = new RegionQualifier(FAKE_REGION_VALUE); + + /** A special marker language qualifier representing any language */ + public static final LanguageQualifier ANY_LANGUAGE = new LanguageQualifier(FAKE_LANG_VALUE); + + /** A locale which matches any language and region */ + public static final Locale ANY = new Locale(ANY_LANGUAGE, ANY_REGION); + + /** The language qualifier, or {@link #ANY_LANGUAGE} if this locale matches any language */ + @NonNull + public final LanguageQualifier language; + + /** The language qualifier, or {@link #ANY_REGION} if this locale matches any region */ + @NonNull + public final RegionQualifier region; + + /** + * Constructs a new {@linkplain Locale} matching a given language in a given locale. + * + * @param language the language + * @param region the region + */ + private Locale(@NonNull LanguageQualifier language, @NonNull RegionQualifier region) { + if (language.getValue().equals(FAKE_LANG_VALUE)) { + language = ANY_LANGUAGE; + } + if (region.getValue().equals(FAKE_REGION_VALUE)) { + region = ANY_REGION; + } + this.language = language; + this.region = region; + } + + /** + * Constructs a new {@linkplain Locale} matching a given language in a given specific locale. + * + * @param language the language + * @param region the region + * @return a locale with the given language and region + */ + @NonNull + public static Locale create( + @NonNull LanguageQualifier language, + @NonNull RegionQualifier region) { + return new Locale(language, region); + } + + /** + * Constructs a new {@linkplain Locale} for the given language, matching any regions. + * + * @param language the language + * @return a locale with the given language and region + */ + public static Locale create(@NonNull LanguageQualifier language) { + return new Locale(language, ANY_REGION); + } + + /** + * Returns a flag image to use for this locale + * + * @return a flag image, or a default globe icon + */ + @NonNull + public Image getFlagImage() { + Image image = null; + String languageCode = hasLanguage() ? language.getValue() : null; + String regionCode = hasRegion() ? region.getValue() : null; + LocaleManager icons = LocaleManager.get(); + if (languageCode == null && regionCode == null) { + return LocaleManager.getGlobeIcon(); + } else { + image = icons.getFlag(languageCode, regionCode); + if (image == null) { + image = LocaleManager.getEmptyIcon(); + } + + return image; + } + } + + /** + * Returns true if this locale specifies a specific language. This is true + * for all locales except {@link #ANY}. + * + * @return true if this locale specifies a specific language + */ + public boolean hasLanguage() { + return language != ANY_LANGUAGE; + } + + /** + * Returns true if this locale specifies a specific region + * + * @return true if this locale specifies a region + */ + public boolean hasRegion() { + return region != ANY_REGION; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((language == null) ? 0 : language.hashCode()); + result = prime * result + ((region == null) ? 0 : region.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Locale other = (Locale) obj; + if (language == null) { + if (other.language != null) + return false; + } else if (!language.equals(other.language)) + return false; + if (region == null) { + if (other.region != null) + return false; + } else if (!region.equals(other.region)) + return false; + return true; + } + + @Override + public String toString() { + return Objects.toStringHelper(this).omitNullValues() + .addValue(language.getValue()) + .addValue(region.getValue()) + .toString(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java new file mode 100644 index 0000000..e85f21d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java @@ -0,0 +1,115 @@ +/* + * 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.wizards.newxmlfile.AddTranslationDialog; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.ToolItem; + +import java.util.List; + +/** + * The {@linkplain LocaleMenuListener} class is responsible for generating the locale + * menu in the {@link ConfigurationChooser}. + */ +class LocaleMenuListener extends SelectionAdapter { + private static final int ACTION_SET_LOCALE = 1; + private static final int ACTION_ADD_TRANSLATION = 2; + + private final ConfigurationChooser mConfigChooser; + private final int mAction; + private final Locale mLocale; + + LocaleMenuListener( + @NonNull ConfigurationChooser configChooser, + int action, + @Nullable Locale locale) { + mConfigChooser = configChooser; + mAction = action; + mLocale = locale; + } + + @Override + public void widgetSelected(SelectionEvent e) { + switch (mAction) { + case ACTION_SET_LOCALE: { + mConfigChooser.selectLocale(mLocale); + mConfigChooser.onLocaleChange(); + break; + } + case ACTION_ADD_TRANSLATION: { + IProject project = mConfigChooser.getProject(); + Shell shell = mConfigChooser.getShell(); + AddTranslationDialog dialog = new AddTranslationDialog(shell, project); + dialog.open(); + break; + } + default: assert false : mAction; + } + } + + static void show(final ConfigurationChooser chooser, ToolItem combo) { + Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + Configuration configuration = chooser.getConfiguration(); + List<Locale> locales = chooser.getLocaleList(); + Locale current = configuration.getLocale(); + + for (Locale locale : locales) { + String title = ConfigurationChooser.getLocaleLabel(chooser, locale, false); + MenuItem item = new MenuItem(menu, SWT.CHECK); + item.setText(title); + Image image = locale.getFlagImage(); + item.setImage(image); + + boolean selected = current == locale; + if (selected) { + item.setSelection(true); + } + + LocaleMenuListener listener = new LocaleMenuListener(chooser, ACTION_SET_LOCALE, + locale); + item.addSelectionListener(listener); + } + + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + + MenuItem item = new MenuItem(menu, SWT.PUSH); + item.setText("Add New Translation..."); + LocaleMenuListener listener = new LocaleMenuListener(chooser, + ACTION_ADD_TRANSLATION, null); + item.addSelectionListener(listener); + + Rectangle bounds = combo.getBounds(); + Point location = new Point(bounds.x, bounds.y + bounds.height); + location = combo.getParent().toDisplay(location); + menu.setLocation(location.x, location.y); + menu.setVisible(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java index 0898cdf..5cad29a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java @@ -45,43 +45,46 @@ class OrientationMenuAction extends SubmenuAction { private static final int MENU_NIGHTMODE = 1; private static final int MENU_UIMODE = 2; - private final ConfigurationComposite mConfiguration; + private final ConfigurationChooser mConfigChooser; /** Type of menu; one of the constants {@link #MENU_NIGHTMODE} etc */ private final int mType; - OrientationMenuAction(int type, String title, ConfigurationComposite configuration) { + OrientationMenuAction(int type, String title, ConfigurationChooser configuration) { super(title); mType = type; - mConfiguration = configuration; + mConfigChooser = configuration; } - static void showMenu(ConfigurationComposite configuration, ToolItem combo) { + static void showMenu(ConfigurationChooser configChooser, ToolItem combo) { MenuManager manager = new MenuManager(); - // Show toggles for all the available configurations - State current = configuration.getSelectedDeviceState(); - Device device = configuration.getSelectedDevice(); + // Show toggles for all the available states + + Configuration configuration = configChooser.getConfiguration(); + Device device = configuration.getDevice(); + State current = configuration.getDeviceState(); if (device != null) { List<State> states = device.getAllStates(); if (states.size() > 1 && current != null) { State flip = configuration.getNextDeviceState(current); - manager.add(new DeviceConfigAction(configuration, - String.format("Switch to %1$s", flip.getName()), flip, false, true)); + String flipName = flip != null ? flip.getName() : current.getName(); + manager.add(new DeviceConfigAction(configChooser, + String.format("Switch to %1$s", flipName), flip, false, true)); manager.add(new Separator()); } for (State config : states) { - manager.add(new DeviceConfigAction(configuration, config.getName(), + manager.add(new DeviceConfigAction(configChooser, config.getName(), config, config == current, false)); } manager.add(new Separator()); } - manager.add(new OrientationMenuAction(MENU_UIMODE, "UI Mode", configuration)); + manager.add(new OrientationMenuAction(MENU_UIMODE, "UI Mode", configChooser)); manager.add(new Separator()); - manager.add(new OrientationMenuAction(MENU_NIGHTMODE, "Night Mode", configuration)); + manager.add(new OrientationMenuAction(MENU_NIGHTMODE, "Night Mode", configChooser)); - Menu menu = manager.createContextMenu(configuration.getShell()); + Menu menu = manager.createContextMenu(configChooser.getShell()); Rectangle bounds = combo.getBounds(); Point location = new Point(bounds.x, bounds.y + bounds.height); location = combo.getParent().toDisplay(location); @@ -93,7 +96,7 @@ class OrientationMenuAction extends SubmenuAction { protected void addMenuItems(Menu menu) { switch (mType) { case MENU_NIGHTMODE: { - NightMode selected = mConfiguration.getSelectedNightMode(); + NightMode selected = mConfigChooser.getConfiguration().getNightMode(); for (NightMode mode : NightMode.values()) { boolean checked = mode == selected; SelectNightModeAction action = new SelectNightModeAction(mode, checked); @@ -103,7 +106,7 @@ class OrientationMenuAction extends SubmenuAction { break; } case MENU_UIMODE: { - UiMode selected = mConfiguration.getSelectedUiMode(); + UiMode selected = mConfigChooser.getConfiguration().getUiMode(); for (UiMode mode : UiMode.values()) { boolean checked = mode == selected; SelectUiModeAction action = new SelectUiModeAction(mode, checked); @@ -114,6 +117,7 @@ class OrientationMenuAction extends SubmenuAction { } } + private class SelectNightModeAction extends Action { private final NightMode mMode; @@ -127,7 +131,9 @@ class OrientationMenuAction extends SubmenuAction { @Override public void run() { - mConfiguration.selectNightMode(mMode); + Configuration configuration = mConfigChooser.getConfiguration(); + configuration.setNightMode(mMode, false); + mConfigChooser.notifyFolderConfigChanged(); } } @@ -144,15 +150,16 @@ class OrientationMenuAction extends SubmenuAction { @Override public void run() { - mConfiguration.selectUiMode(mMode); + Configuration configuration = mConfigChooser.getConfiguration(); + configuration.setUiMode(mMode, false); } } private static class DeviceConfigAction extends Action { - private final ConfigurationComposite mConfiguration; + private final ConfigurationChooser mConfiguration; private final State mState; - private DeviceConfigAction(ConfigurationComposite configuration, String title, + private DeviceConfigAction(ConfigurationChooser configuration, String title, State state, boolean checked, boolean flip) { super(title, IAction.AS_RADIO_BUTTON); mConfiguration = configuration; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java index a8f6504..d062849 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java @@ -27,10 +27,10 @@ import org.eclipse.jface.action.IAction; * animation category */ class SelectThemeAction extends Action { - private final ConfigurationComposite mConfiguration; + private final ConfigurationChooser mConfiguration; private final String mTheme; - public SelectThemeAction(ConfigurationComposite configuration, String title, String theme, + public SelectThemeAction(ConfigurationChooser configuration, String title, String theme, boolean selected) { super(title, IAction.AS_RADIO_BUTTON); assert theme.startsWith(STYLE_RESOURCE_PREFIX) diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java new file mode 100644 index 0000000..cae6596 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java @@ -0,0 +1,80 @@ +/* + * 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.sdklib.IAndroidTarget; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; + +import java.util.List; + +/** + * The {@linkplain TargetMenuListener} class is responsible for + * generating the rendering target menu in the {@link ConfigurationChooser}. + */ +class TargetMenuListener extends SelectionAdapter { + private final ConfigurationChooser mConfigChooser; + private final IAndroidTarget mTarget; + + TargetMenuListener( + @NonNull ConfigurationChooser configChooser, + @Nullable IAndroidTarget target) { + mConfigChooser = configChooser; + mTarget = target; + } + + @Override + public void widgetSelected(SelectionEvent e) { + mConfigChooser.selectTarget(mTarget); + mConfigChooser.onRenderingTargetChange(); + } + + static void show(ConfigurationChooser chooser, ToolItem combo) { + Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + Configuration configuration = chooser.getConfiguration(); + IAndroidTarget current = configuration.getTarget(); + List<IAndroidTarget> targets = chooser.getTargetList(); + + for (final IAndroidTarget target : targets) { + String title = ConfigurationChooser.getRenderingTargetLabel(target, false); + MenuItem item = new MenuItem(menu, SWT.CHECK); + item.setText(title); + + boolean selected = current == target; + if (selected) { + item.setSelection(true); + } + + item.addSelectionListener(new TargetMenuListener(chooser, target)); + } + + Rectangle bounds = combo.getBounds(); + Point location = new Point(bounds.x, bounds.y + bounds.height); + location = combo.getParent().toDisplay(location); + menu.setLocation(location.x, location.y); + menu.setVisible(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeChooser.java deleted file mode 100644 index 262d317..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeChooser.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ide.eclipse.adt.internal.editors.layout.configuration; - -import com.android.ide.eclipse.adt.AdtPlugin; - -import org.eclipse.jface.viewers.LabelProvider; -import org.eclipse.jface.window.Window; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog; - -/** - * A dialog to let the user select a theme - */ -public class ThemeChooser extends AbstractElementListSelectionDialog { - /** The return code from the dialog for the user choosing "Clear" */ - public static final int CLEAR_RETURN_CODE = -5; - /** The dialog button ID for the user choosing "Clear" */ - private static final int CLEAR_BUTTON_ID = CLEAR_RETURN_CODE; - - private String mCurrentResource; - private String[] mThemes; - - private ThemeChooser(String[] themes, Shell parent) { - super(parent, new LabelProvider()); - mThemes = themes; - - setTitle("Theme Chooser"); - setMessage(String.format("Choose a theme")); - } - - @Override - protected void createButtonsForButtonBar(Composite parent) { - createButton(parent, CLEAR_BUTTON_ID, "Clear", false /*defaultButton*/); - super.createButtonsForButtonBar(parent); - } - - @Override - protected void buttonPressed(int buttonId) { - super.buttonPressed(buttonId); - - if (buttonId == CLEAR_BUTTON_ID) { - assert CLEAR_RETURN_CODE != Window.OK && CLEAR_RETURN_CODE != Window.CANCEL; - setReturnCode(CLEAR_RETURN_CODE); - close(); - } - } - - private void setCurrentResource(String resource) { - mCurrentResource = resource; - } - - private String getCurrentResource() { - return mCurrentResource; - } - - @Override - protected void computeResult() { - computeResultFromSelection(); - } - - private void computeResultFromSelection() { - if (getSelectionIndex() == -1) { - mCurrentResource = null; - return; - } - - Object[] elements = getSelectedElements(); - if (elements.length == 1 && elements[0] instanceof String) { - String item = (String) elements[0]; - - mCurrentResource = item; - } - } - - @Override - protected Control createDialogArea(Composite parent) { - Composite top = (Composite)super.createDialogArea(parent); - - createMessageArea(top); - - createFilterText(top); - createFilteredList(top); - - setupResourceList(); - selectResourceString(mCurrentResource); - - return top; - } - - /** - * Setups the current list. - */ - private String[] setupResourceList() { - setListElements(mThemes); - fFilteredList.setEnabled(mThemes.length > 0); - - return mThemes; - } - - /** - * Select an item by its name, if possible. - */ - private void selectItemName(String itemName, String[] items) { - if (itemName == null || items == null) { - return; - } - setSelection(new String[] { itemName }); - } - - /** - * Select an item by its full resource string. - * This also selects between project and system repository based on the resource string. - */ - private void selectResourceString(String item) { - String itemName = item; - - // Update the list - String[] items = setupResourceList(); - - // If we have a selection name, select it - if (itemName != null) { - selectItemName(itemName, items); - } - } - - public static String chooseResource( - String[] themes, - String currentTheme) { - Shell shell = AdtPlugin.getDisplay().getActiveShell(); - if (shell == null) { - return null; - } - - ThemeChooser dialog = new ThemeChooser(themes, shell); - dialog.setCurrentResource(currentTheme); - - int result = dialog.open(); - if (result == ThemeChooser.CLEAR_RETURN_CODE) { - return ""; //$NON-NLS-1$ - } else if (result == Window.OK) { - return dialog.getCurrentResource(); - } - - return null; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java index 7d8c487..239f396 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java @@ -73,30 +73,31 @@ class ThemeMenuAction extends SubmenuAction { private static final int MENU_DEVICE_LIGHT = 8; private static final int MENU_ALL = 9; - private final ConfigurationComposite mConfiguration; + private final ConfigurationChooser mConfigChooser; private final List<String> mThemeList; /** Type of menu; one of the constants {@link #MENU_ALL} etc */ private final int mType; - ThemeMenuAction(int type, String title, ConfigurationComposite configuration, + ThemeMenuAction(int type, String title, ConfigurationChooser configuration, List<String> themeList) { super(title); mType = type; - mConfiguration = configuration; + mConfigChooser = configuration; mThemeList = themeList; } - static void showThemeMenu(ConfigurationComposite configuration, ToolItem combo, + static void showThemeMenu(ConfigurationChooser configChooser, ToolItem combo, List<String> themeList) { MenuManager manager = new MenuManager(); // First show the currently selected theme (grayed out since you can't // reselect it) - String currentTheme = configuration.getSelectedTheme(); + Configuration configuration = configChooser.getConfiguration(); + String currentTheme = configuration.getTheme(); String currentName = null; if (currentTheme != null) { currentName = ResourceHelper.styleToTheme(currentTheme); - SelectThemeAction action = new SelectThemeAction(configuration, + SelectThemeAction action = new SelectThemeAction(configChooser, currentName, currentTheme, true /* selected */); @@ -105,16 +106,16 @@ class ThemeMenuAction extends SubmenuAction { manager.add(new Separator()); } - String preferred = configuration.getPreferredTheme(); + String preferred = configChooser.computePreferredTheme(); if (preferred != null && !preferred.equals(currentTheme)) { - manager.add(new SelectThemeAction(configuration, + manager.add(new SelectThemeAction(configChooser, ResourceHelper.styleToTheme(preferred), preferred, false /* selected */)); manager.add(new Separator()); } - IAndroidTarget target = configuration.getRenderingTarget(); - int apiLevel = target.getVersion().getApiLevel(); + IAndroidTarget target = configuration.getTarget(); + int apiLevel = target != null ? target.getVersion().getApiLevel() : 1; boolean hasHolo = apiLevel >= 11; // Honeycomb boolean hasDeviceDefault = apiLevel >= 14; // ICS @@ -123,44 +124,44 @@ class ThemeMenuAction extends SubmenuAction { // Theme.Holo.Wallpaper etc manager.add(new ThemeMenuAction(MENU_PROJECT, "Project Themes", - configuration, themeList)); + configChooser, themeList)); manager.add(new ThemeMenuAction(MENU_MANIFEST, "Manifest Themes", - configuration, themeList)); + configChooser, themeList)); manager.add(new Separator()); if (hasHolo) { manager.add(new ThemeMenuAction(MENU_HOLO, "Holo", - configuration, themeList)); + configChooser, themeList)); manager.add(new ThemeMenuAction(MENU_HOLO_LIGHT, "Holo.Light", - configuration, themeList)); + configChooser, themeList)); } if (hasDeviceDefault) { manager.add(new ThemeMenuAction(MENU_DEVICE, "DeviceDefault", - configuration, themeList)); + configChooser, themeList)); manager.add(new ThemeMenuAction(MENU_DEVICE_LIGHT, "DeviceDefault.Light", - configuration, themeList)); + configChooser, themeList)); } manager.add(new ThemeMenuAction(MENU_THEME, "Theme", - configuration, themeList)); + configChooser, themeList)); manager.add(new ThemeMenuAction(MENU_THEME_LIGHT, "Theme.Light", - configuration, themeList)); + configChooser, themeList)); // TODO: Add generic types like Wallpaper, Dialog, Alert, etc here, with // submenus for picking it within each theme category? manager.add(new Separator()); manager.add(new ThemeMenuAction(MENU_ALL, "All", - configuration, themeList)); + configChooser, themeList)); if (currentTheme != null) { assert currentName != null; manager.add(new Separator()); String title = String.format("Open %1$s Declaration...", currentName); - manager.add(new OpenThemeAction(title, configuration.getEditedFile(), currentTheme)); + manager.add(new OpenThemeAction(title, configChooser.getEditedFile(), currentTheme)); } - Menu menu = manager.createContextMenu(configuration.getShell()); + Menu menu = manager.createContextMenu(configChooser.getShell()); Rectangle bounds = combo.getBounds(); Point location = new Point(bounds.x, bounds.y + bounds.height); @@ -177,10 +178,11 @@ class ThemeMenuAction extends SubmenuAction { break; case MENU_MANIFEST: { - IProject project = mConfiguration.getEditedFile().getProject(); + IProject project = mConfigChooser.getEditedFile().getProject(); ManifestInfo manifest = ManifestInfo.get(project); Map<String, String> activityThemes = manifest.getActivityThemes(); - String activity = mConfiguration.getSelectedActivity(); + Configuration configuration = mConfigChooser.getConfiguration(); + String activity = configuration.getActivity(); if (activity != null) { String theme = activityThemes.get(activity); if (theme != null) { @@ -196,7 +198,7 @@ class ThemeMenuAction extends SubmenuAction { } List<String> sorted = new ArrayList<String>(allThemes); Collections.sort(sorted); - String current = mConfiguration.getSelectedTheme(); + String current = configuration.getTheme(); for (String theme : sorted) { boolean selected = theme.equals(current); addMenuItem(menu, theme, selected); @@ -268,19 +270,19 @@ class ThemeMenuAction extends SubmenuAction { } private void addMenuItems(Menu menu, List<String> themes) { - String current = mConfiguration.getSelectedTheme(); + String current = mConfigChooser.getConfiguration().getTheme(); for (String theme : themes) { addMenuItem(menu, theme, theme.equals(current)); } } private boolean isSelectedTheme(String theme) { - return theme.equals(mConfiguration.getSelectedTheme()); + return theme.equals(mConfigChooser.getConfiguration().getTheme()); } private void addMenuItem(Menu menu, String theme, boolean selected) { String title = ResourceHelper.styleToTheme(theme); - SelectThemeAction action = new SelectThemeAction(mConfiguration, title, theme, selected); + SelectThemeAction action = new SelectThemeAction(mConfigChooser, title, theme, selected); new ActionContributionItem(action).fill(menu, -1); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java new file mode 100644 index 0000000..f0698e6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java @@ -0,0 +1,133 @@ +/* + * 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.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.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.resources.ResourceFolderType; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PartInitException; + +import java.io.IOException; +import java.io.InputStream; + +/** Job which creates a new layout file for a given configuration */ +class CreateNewConfigJob extends Job { + private final GraphicalEditorPart mEditor; + private final IFile mFromFile; + private final FolderConfiguration mConfig; + + CreateNewConfigJob( + @NonNull GraphicalEditorPart editor, + @NonNull IFile fromFile, + @NonNull FolderConfiguration config) { + super("Create Alternate Layout"); + mEditor = editor; + mFromFile = fromFile; + mConfig = config; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + // get the folder name + String folderName = mConfig.getFolderName(ResourceFolderType.LAYOUT); + try { + // look to see if it exists. + // get the res folder + IFolder res = (IFolder) mFromFile.getParent().getParent(); + + IFolder newParentFolder = res.getFolder(folderName); + if (newParentFolder.exists()) { + // this should not happen since aapt would have complained + // before, but if one disables the automatic build, this could + // happen. + String message = String.format("File 'res/%1$s' already exists!", + folderName); + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); + } + + AdtUtils.ensureExists(newParentFolder); + final IFile file = newParentFolder.getFile(mFromFile.getName()); + + InputStream input = mFromFile.getContents(); + file.create(input, false, monitor); + input.close(); + + // Ensure that the project resources updates itself to notice the new configuration. + // In theory, this shouldn't be necessary, but we need to make sure the + // resource manager knows about this immediately such that the call below + // to find the best configuration takes the new folder into account. + ResourceManager resourceManager = ResourceManager.getInstance(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IFolder folder = root.getFolder(newParentFolder.getFullPath()); + resourceManager.getResourceFolder(folder); + + // Switch to the new file + Display display = mEditor.getConfigurationChooser().getDisplay(); + display.asyncExec(new Runnable() { + @Override + public void run() { + // The given old layout has been forked into a new layout + // for a given configuration. This means that the old layout + // is no longer a match for the configuration, which is + // probably what it is still showing. We have to modify + // its configuration to no longer be an impossible + // configuration. + ConfigurationChooser chooser = mEditor.getConfigurationChooser(); + chooser.onAlternateLayoutCreated(); + + // Finally open the new layout + try { + AdtPlugin.openFile(file, null, false); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + } + }); + } catch (IOException e2) { + String message = String.format( + "Failed to create File 'res/%1$s/%2$s' : %3$s", + folderName, mFromFile.getName(), e2.getMessage()); + AdtPlugin.displayError("Layout Creation", message); + + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + message, e2); + } catch (CoreException e2) { + String message = String.format( + "Failed to create File 'res/%1$s/%2$s' : %3$s", + folderName, mFromFile.getName(), e2.getMessage()); + AdtPlugin.displayError("Layout Creation", message); + + return e2.getStatus(); + } + + return Status.OK_STATUS; + } +} 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 0f1b373..726824f 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 @@ -16,28 +16,31 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import static com.android.SdkConstants.FD_GEN_SOURCES; +import static com.android.SdkConstants.ANDROID_PKG; import static com.android.SdkConstants.ANDROID_STRING_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CONTEXT; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.FD_GEN_SOURCES; import static com.android.SdkConstants.GRID_LAYOUT; import static com.android.SdkConstants.SCROLL_VIEW; 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.SdkConstants.ANDROID_PKG; +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; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_COLLAPSED; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_OPEN; 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.common.api.Rect; import com.android.ide.common.layout.BaseLayoutRule; import com.android.ide.common.rendering.LayoutLibrary; @@ -48,7 +51,6 @@ 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.SessionParams.RenderingMode; -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; @@ -59,14 +61,17 @@ import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate; import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; +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.ConfigurationMatcher; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; @@ -76,13 +81,13 @@ import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFa import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; -import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.resources.Density; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; @@ -91,7 +96,6 @@ import com.android.tools.lint.detector.api.LintUtils; import com.android.utils.Pair; import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -168,10 +172,6 @@ import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite; import org.w3c.dom.Element; import org.w3c.dom.Node; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -196,11 +196,12 @@ import java.util.Set; * @since GLE2 */ public class GraphicalEditorPart extends EditorPart - implements IPageImageProvider, INullSelectionListener, IFlyoutListener { + implements IPageImageProvider, INullSelectionListener, IFlyoutListener, + ConfigurationClient { /* * Useful notes: - * To understand Drag'n'drop: + * To understand Drag & drop: * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html * * To understand the site's selection listener, selection provider, and the @@ -241,8 +242,8 @@ public class GraphicalEditorPart extends EditorPart /** Reference to the file being edited. Can also be used to access the {@link IProject}. */ private IFile mEditedFile; - /** The configuration composite at the top of the layout editor. */ - private ConfigurationComposite mConfigComposite; + /** The configuration chooser at the top of the layout editor. */ + private ConfigurationChooser mConfigChooser; /** The sash that splits the palette from the error view. * The error view is shown only when needed. */ @@ -271,7 +272,6 @@ public class GraphicalEditorPart extends EditorPart private ProjectCallback mProjectCallback; private boolean mNeedsRecompute = false; private TargetListener mTargetListener; - private ConfigListener mConfigListener; private ResourceResolver mResourceResolver; private ReloadListener mReloadListener; private int mMinSdkVersion; @@ -291,7 +291,12 @@ public class GraphicalEditorPart extends EditorPart */ private boolean mActive; - public GraphicalEditorPart(LayoutEditorDelegate editorDelegate) { + /** + * Constructs a new {@link GraphicalEditorPart} + * + * @param editorDelegate the associated XML editor delegate + */ + public GraphicalEditorPart(@NonNull LayoutEditorDelegate editorDelegate) { mEditorDelegate = editorDelegate; setPartName("Graphical Layout"); } @@ -341,8 +346,6 @@ public class GraphicalEditorPart extends EditorPart parent.setLayout(gl); gl.marginHeight = gl.marginWidth = 0; - mConfigListener = new ConfigListener(); - // Check whether somebody has requested an initial state for the newly opened file. // The initial state is a serialized version of the state compatible with // {@link ConfigurationComposite#CONFIG_STATE}. @@ -404,9 +407,8 @@ public class GraphicalEditorPart extends EditorPart gridLayout.marginHeight = 0; layoutBarAndCanvas.setLayout(gridLayout); - mConfigComposite = new ConfigurationComposite(mConfigListener, layoutBarAndCanvas, - SWT.NONE /*SWT.BORDER*/, initialState); - mConfigComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mConfigChooser = new ConfigurationChooser(this, layoutBarAndCanvas, initialState); + mConfigChooser.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this); GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); @@ -513,12 +515,12 @@ public class GraphicalEditorPart extends EditorPart /** Shows the embedded (within the layout editor) outline and or properties */ void showStructureViews(final boolean showOutline, final boolean showProperties, final boolean updateLayout) { - Display display = mConfigComposite.getDisplay(); + Display display = mConfigChooser.getDisplay(); if (display.getThread() != Thread.currentThread()) { display.asyncExec(new Runnable() { @Override public void run() { - if (!mConfigComposite.isDisposed()) { + if (!mConfigChooser.isDisposed()) { showStructureViews(showOutline, showProperties, updateLayout); } } @@ -638,367 +640,236 @@ public class GraphicalEditorPart extends EditorPart mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode); } - /** - * Listens to changes from the Configuration UI banner and triggers layout rendering when - * changed. Also provide the Configuration UI with the list of resources/layout to display. - */ - private class ConfigListener implements IConfigListener { + // ---- Implements ConfigurationClient ---- + @Override + public void aboutToChange(int flags) { + if ((flags & CHANGED_RENDER_TARGET) != 0) { + IAndroidTarget oldTarget = mConfigChooser.getConfiguration().getTarget(); + preRenderingTargetChangeCleanUp(oldTarget); + } + } - /** - * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. - * <p/>If there is no match, notify the user. - */ - @Override - public void onConfigurationChange() { - mConfiguredFrameworkRes = mConfiguredProjectRes = null; - mResourceResolver = null; + @Override + public boolean changed(int flags) { + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + mResourceResolver = null; - if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) { - return; - } + if (mEditedFile == null) { + return true; + } + + // 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 + // - another random compatible config was chosen in the config composite. + // The result is that 'match' will not be the file being edited, but because this is not + // due to a config change, we should not trigger opening the actual best match (also, + // because the editor is still opening the MatchingStrategy woudln't answer true + // and the best match file would open in a different editor). + // So the solution is that if the editor is being created, we just call recomputeLayout + // without looking for a better matching layout file. + if (mEditorDelegate.getEditor().isCreatingPages()) { + recomputeLayout(); + } else { + // get the resources of the file's project. + IFile best = ConfigurationMatcher.getBestFileMatch(mConfigChooser); + if (best != null) { + if (!best.equals(mEditedFile)) { + try { + // tell the editor that the next replacement file is due to a config + // change. + mEditorDelegate.setNewFileOnConfigChange(true); + + boolean reuseEditor = AdtPrefs.getPrefs().isSharedLayoutEditor(); + if (!reuseEditor) { + String data = AdtPlugin.getFileProperty(best, NAME_CONFIG_STATE); + if (data == null) { + // Not previously opened: duplicate the current state as + // much as possible + data = mConfigChooser.getConfiguration().toPersistentString(); + AdtPlugin.setFileProperty(best, NAME_CONFIG_STATE, data); + } + } - // 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 - // - another random compatible config was chosen in the config composite. - // The result is that 'match' will not be the file being edited, but because this is not - // due to a config change, we should not trigger opening the actual best match (also, - // because the editor is still opening the MatchingStrategy woudln't answer true - // and the best match file would open in a different editor). - // So the solution is that if the editor is being created, we just call recomputeLayout - // without looking for a better matching layout file. - if (mEditorDelegate.getEditor().isCreatingPages()) { - recomputeLayout(); - } else { - // get the resources of the file's project. - ProjectResources resources = ResourceManager.getInstance().getProjectResources( - mEditedFile.getProject()); - - // from the resources, look for a matching file - ResourceFile match = null; - if (resources != null) { - match = resources.getMatchingFile(mEditedFile.getName(), - ResourceFolderType.LAYOUT, - mConfigComposite.getCurrentConfig()); - } + // ask the IDE to open the replacement file. + IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), best, + CommonXmlEditor.ID); - if (match != null) { - // since this is coming from Eclipse, this is always an instance of IFileWrapper - IFileWrapper iFileWrapper = (IFileWrapper) match.getFile(); - IFile iFile = iFileWrapper.getIFile(); - if (iFile.equals(mEditedFile) == false) { - try { - // tell the editor that the next replacement file is due to a config - // change. - mEditorDelegate.setNewFileOnConfigChange(true); - - // ask the IDE to open the replacement file. - IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), iFile); - - // we're done! - return; - } catch (PartInitException e) { - // FIXME: do something! - } + // we're done! + return reuseEditor; + } catch (PartInitException e) { + // FIXME: do something! } + } - // at this point, we have not opened a new file. + // at this point, we have not opened a new file. - // Store the state in the current file - mConfigComposite.storeState(); + // Store the state in the current file + mConfigChooser.saveConstraints(); - // Even though the layout doesn't change, the config changed, and referenced - // resources need to be updated. - recomputeLayout(); - } else { - // display the error. - FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig(); - displayError( - "No resources match the configuration\n" + - " \n" + - "\t%1$s\n" + - " \n" + - "Change the configuration or create:\n" + - " \n" + - "\tres/%2$s/%3$s\n" + - " \n" + - "You can also click the 'Create New...' item in the configuration dropdown menu above.", - currentConfig.toDisplayString(), - currentConfig.getFolderName(ResourceFolderType.LAYOUT), - mEditedFile.getName()); - } + // Even though the layout doesn't change, the config changed, and referenced + // resources need to be updated. + recomputeLayout(); + } else { + // display the error. + Configuration configuration = mConfigChooser.getConfiguration(); + FolderConfiguration currentConfig = configuration.getFullConfig(); + displayError( + "No resources match the configuration\n" + + " \n" + + "\t%1$s\n" + + " \n" + + "Change the configuration or create:\n" + + " \n" + + "\tres/%2$s/%3$s\n" + + " \n" + + "You can also click the 'Create New...' item in the configuration " + + "dropdown menu above.", + currentConfig.toDisplayString(), + currentConfig.getFolderName(ResourceFolderType.LAYOUT), + mEditedFile.getName()); + } + } + + if ((flags & CHANGED_RENDER_TARGET) != 0) { + Configuration configuration = mConfigChooser.getConfiguration(); + IAndroidTarget target = configuration.getTarget(); + Sdk current = Sdk.getCurrent(); + if (current != null) { + AndroidTargetData targetData = current.getTargetData(target); + updateCapabilities(targetData); + } + } + + if ((flags & (CHANGED_DEVICE | CHANGED_DEVICE_CONFIG)) != 0) { + // When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom + // 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); } - - reloadPalette(); } - @Override - public void onThemeChange() { - // Store the state in the current file - mConfigComposite.storeState(); - mResourceResolver = null; + reloadPalette(); - recomputeLayout(); + return true; + } - reloadPalette(); + @Override + public void setActivity(@NonNull String activity) { + ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); + String pkg = manifest.getPackage(); + if (activity.startsWith(pkg) && activity.length() > pkg.length() + && activity.charAt(pkg.length()) == '.') { + activity = activity.substring(pkg.length()); } + CommonXmlEditor editor = getEditorDelegate().getEditor(); + Element element = editor.getUiRootNode().getXmlDocument().getDocumentElement(); + AdtUtils.setToolsAttribute(editor, + element, "Choose Activity", ATTR_CONTEXT, + activity, false /*reveal*/, false /*append*/); + } - @Override - public void onCreate() { - LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(), - mEditedFile.getName(), mConfigComposite.getCurrentConfig()); - if (dialog.open() == Window.OK) { - final FolderConfiguration config = new FolderConfiguration(); - dialog.getConfiguration(config); + /** + * Returns a {@link ProjectResources} for the framework resources based on the current + * configuration selection. + * @return the framework resources or null if not found. + */ + @Override + @Nullable + public ResourceRepository getFrameworkResources() { + return getFrameworkResources(getRenderingTarget()); + } - createAlternateLayout(config); - } - } + /** + * Returns a {@link ProjectResources} for the framework resources of a given + * target. + * @param target the target for which to return the framework resources. + * @return the framework resources or null if not found. + */ + @Override + @Nullable + public ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target) { + if (target != null) { + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); - @Override - public void onSetActivity(String activity) { - ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); - String pkg = manifest.getPackage(); - if (activity.startsWith(pkg) && activity.length() > pkg.length() - && activity.charAt(pkg.length()) == '.') { - activity = activity.substring(pkg.length()); + if (data != null) { + return data.getFrameworkResources(); } - CommonXmlEditor editor = getEditorDelegate().getEditor(); - Element element = editor.getUiRootNode().getXmlDocument().getDocumentElement(); - AdtUtils.setToolsAttribute(editor, - element, "Choose Activity", ConfigurationComposite.ATTR_CONTEXT, - activity, false /*reveal*/, false /*append*/); - } - - @Override - public void onRenderingTargetPreChange(IAndroidTarget oldTarget) { - preRenderingTargetChangeCleanUp(oldTarget); } - @Override - public void onRenderingTargetPostChange(IAndroidTarget target) { - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - updateCapabilities(targetData); + return null; + } - mPalette.reloadPalette(target); + @Override + @Nullable + public ProjectResources getProjectResources() { + if (mEditedFile != null) { + ResourceManager manager = ResourceManager.getInstance(); + return manager.getProjectResources(mEditedFile.getProject()); } - @Override - public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() { - if (mConfiguredFrameworkRes == null && mConfigComposite != null) { - ResourceRepository frameworkRes = getFrameworkResources(); - - if (frameworkRes == null) { - AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); - } else { - // get the framework resource values based on the current config - mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( - mConfigComposite.getCurrentConfig()); - } - } + return null; + } - return mConfiguredFrameworkRes; - } - @Override - public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() { - if (mConfiguredProjectRes == null && mConfigComposite != null) { - ProjectResources project = getProjectResources(); + @Override + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() { + if (mConfiguredFrameworkRes == null && mConfigChooser != null) { + ResourceRepository frameworkRes = getFrameworkResources(); - // get the project resource values based on the current config - mConfiguredProjectRes = project.getConfiguredResources( - mConfigComposite.getCurrentConfig()); + if (frameworkRes == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); + } else { + // get the framework resource values based on the current config + mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( + mConfigChooser.getConfiguration().getFullConfig()); } - - return mConfiguredProjectRes; - } - - /** - * Returns a {@link ProjectResources} for the framework resources based on the current - * configuration selection. - * @return the framework resources or null if not found. - */ - @Override - public ResourceRepository getFrameworkResources() { - return getFrameworkResources(getRenderingTarget()); } - /** - * Returns a {@link ProjectResources} for the framework resources of a given - * target. - * @param target the target for which to return the framework resources. - * @return the framework resources or null if not found. - */ - @Override - public ResourceRepository getFrameworkResources(IAndroidTarget target) { - if (target != null) { - AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + return mConfiguredFrameworkRes; + } - if (data != null) { - return data.getFrameworkResources(); - } - } + @Override + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() { + if (mConfiguredProjectRes == null && mConfigChooser != null) { + ProjectResources project = getProjectResources(); - return null; + // get the project resource values based on the current config + mConfiguredProjectRes = project.getConfiguredResources( + mConfigChooser.getConfiguration().getFullConfig()); } - @Override - public ProjectResources getProjectResources() { - if (mEditedFile != null) { - ResourceManager manager = ResourceManager.getInstance(); - return manager.getProjectResources(mEditedFile.getProject()); - } + return mConfiguredProjectRes; + } - return null; + @Override + public void createConfigFile() { + LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigChooser.getShell(), + mEditedFile.getName(), mConfigChooser.getConfiguration().getFullConfig()); + if (dialog.open() != Window.OK) { + return; } - /** - * Creates a new layout file from the specified {@link FolderConfiguration}. - */ - private void createAlternateLayout(final FolderConfiguration config) { - new Job("Create Alternate Resource") { - @Override - protected IStatus run(IProgressMonitor monitor) { - // get the folder name - String folderName = config.getFolderName(ResourceFolderType.LAYOUT); - try { - - // look to see if it exists. - // get the res folder - IFolder res = (IFolder)mEditedFile.getParent().getParent(); - String path = res.getLocation().toOSString(); - - File newLayoutFolder = new File(path + File.separator + folderName); - if (newLayoutFolder.isDirectory()) { - // this should not happen since aapt would have complained - // before, but if one disable the automatic build, this could - // happen. - String message = String.format("File 'res/%1$s' already exists!", - folderName); - - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); - } else if (newLayoutFolder.exists() == false) { - // create it. - newLayoutFolder.mkdir(); - } - - // now create the file - File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + - File.separator + mEditedFile.getName()); - - newLayoutFile.createNewFile(); - - InputStream input = mEditedFile.getContents(); - - FileOutputStream fos = new FileOutputStream(newLayoutFile); - - byte[] data = new byte[512]; - int count; - while ((count = input.read(data)) != -1) { - fos.write(data, 0, count); - } - - input.close(); - fos.close(); - - // refreshes the res folder to show up the new - // layout folder (if needed) and the file. - // We use a progress monitor to catch the end of the refresh - // to trigger the edit of the new file. - res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { - @Override - public void done() { - mConfigComposite.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - onConfigurationChange(); - } - }); - } - - @Override - public void beginTask(String name, int totalWork) { - // pass - } - - @Override - public void internalWorked(double work) { - // pass - } - - @Override - public boolean isCanceled() { - // pass - return false; - } - - @Override - public void setCanceled(boolean value) { - // pass - } - - @Override - public void setTaskName(String name) { - // pass - } - - @Override - public void subTask(String name) { - // pass - } + FolderConfiguration config = new FolderConfiguration(); + dialog.getConfiguration(config); - @Override - public void worked(int work) { - // pass - } - }); - - // Switch to the new file as well - IFile file = AdtUtils.fileToIFile(newLayoutFile); - if (file != null) { - AdtPlugin.openFile(file, null, false); - } - } catch (IOException e2) { - String message = String.format( - "Failed to create File 'res/%1$s/%2$s' : %3$s", - folderName, mEditedFile.getName(), e2.getMessage()); - - AdtPlugin.displayError("Layout Creation", message); - - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, - message, e2); - } catch (CoreException e2) { - String message = String.format( - "Failed to create File 'res/%1$s/%2$s' : %3$s", - folderName, mEditedFile.getName(), e2.getMessage()); - - AdtPlugin.displayError("Layout Creation", message); - - return e2.getStatus(); - } - - return Status.OK_STATUS; - - } - }.schedule(); - } - - /** - * When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom - * 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 - */ - @Override - public void onDevicePostChange() { - if (mActionBar.isZoomingAllowed()) { - getCanvasControl().setFitScale(true); - } - } + // Creates a new layout file from the specified {@link FolderConfiguration}. + CreateNewConfigJob job = new CreateNewConfigJob(this, mEditedFile, config); + job.schedule(); + } - @Override - public String getIncludedWithin() { - return mIncludedWithin != null ? mIncludedWithin.getName() : null; - } + /** + * Returns the resource name of the file that is including this current layout, if any + * (may be null) + * + * @return the resource name of an including layout, or null + */ + @Override + public Reference getIncludedWithin() { + return mIncludedWithin; } /** @@ -1034,8 +905,8 @@ public class GraphicalEditorPart extends EditorPart if (currentSdk != null) { IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); if (target != null) { - mConfigComposite.onSdkLoaded(target); - mConfigListener.onConfigurationChange(); + mConfigChooser.onSdkLoaded(target); + changed(CHANGED_FOLDER | CHANGED_RENDER_TARGET); } } } @@ -1159,7 +1030,7 @@ public class GraphicalEditorPart extends EditorPart syncDockingState(); mActionBar.updateErrorIndicator(); - boolean changed = mConfigComposite.syncRenderState(); + boolean changed = mConfigChooser.syncRenderState(); if (changed) { // Will also force recomputeLayout() return; @@ -1236,7 +1107,7 @@ public class GraphicalEditorPart extends EditorPart */ public void openFile(IFile file) { mEditedFile = file; - mConfigComposite.setFile(mEditedFile); + mConfigChooser.setFile(mEditedFile); if (mReloadListener == null) { mReloadListener = new ReloadListener(); @@ -1273,7 +1144,7 @@ public class GraphicalEditorPart extends EditorPart */ public void replaceFile(IFile file) { mEditedFile = file; - mConfigComposite.replaceFile(mEditedFile); + mConfigChooser.replaceFile(mEditedFile); computeSdkVersion(); } @@ -1284,29 +1155,35 @@ public class GraphicalEditorPart extends EditorPart */ public void changeFileOnNewConfig(IFile file) { mEditedFile = file; - mConfigComposite.changeFileOnNewConfig(mEditedFile); + mConfigChooser.changeFileOnNewConfig(mEditedFile); } /** * Responds to a target change for the project of the edited file */ public void onTargetChange() { - AndroidTargetData targetData = mConfigComposite.onXmlModelLoaded(); + AndroidTargetData targetData = mConfigChooser.onXmlModelLoaded(); updateCapabilities(targetData); - mConfigListener.onConfigurationChange(); + changed(CHANGED_FOLDER | CHANGED_RENDER_TARGET); } /** Updates the capabilities for the given target data (which may be null) */ private void updateCapabilities(AndroidTargetData targetData) { if (targetData != null) { LayoutLibrary layoutLib = targetData.getLayoutLibrary(); - if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) { + if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) { showIn(null); } } } + /** + * Returns the {@link CommonXmlDelegate} for this editor + * + * @return the {@link CommonXmlDelegate} for this editor + */ + @NonNull public LayoutEditorDelegate getEditorDelegate() { return mEditorDelegate; } @@ -1332,6 +1209,11 @@ public class GraphicalEditorPart extends EditorPart return null; } + /** + * Returns the {@link UiDocumentNode} for the XML model edited by this editor + * + * @return the associated model + */ public UiDocumentNode getModel() { return mEditorDelegate.getUiRootNode(); } @@ -1361,6 +1243,9 @@ public class GraphicalEditorPart extends EditorPart } } + /** + * Recomputes the layout + */ public void recomputeLayout() { try { if (!ensureFileValid()) { @@ -1401,6 +1286,9 @@ public class GraphicalEditorPart extends EditorPart } } + /** + * Reloads the palette + */ public void reloadPalette() { if (mPalette != null) { IAndroidTarget renderingTarget = getRenderingTarget(); @@ -1427,7 +1315,7 @@ public class GraphicalEditorPart extends EditorPart * @return the bounds of the screen, never null */ public Rect getScreenBounds() { - return mConfigComposite.getScreenBounds(); + return mConfigChooser.getConfiguration().getScreenBounds(); } /** @@ -1437,7 +1325,8 @@ public class GraphicalEditorPart extends EditorPart * @return the scale to multiple layout coordinates with to obtain the dip position */ public float getDipScale() { - return Density.DEFAULT_DENSITY / (float) mConfigComposite.getDensity().getDpiValue(); + float dpi = mConfigChooser.getConfiguration().getDensity().getDpiValue(); + return Density.DEFAULT_DENSITY / dpi; } // --- private methods --- @@ -1573,13 +1462,13 @@ public class GraphicalEditorPart extends EditorPart return null; } - if (mConfigComposite.isDisposed()) { + if (mConfigChooser.isDisposed()) { return null; } - assert mConfigComposite.getDisplay().getThread() == Thread.currentThread(); + assert isUiThread(); // attempt to get a target from the configuration selector. - IAndroidTarget renderingTarget = mConfigComposite.getRenderingTarget(); + IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget(); if (renderingTarget != null) { return renderingTarget; } @@ -1707,19 +1596,19 @@ public class GraphicalEditorPart extends EditorPart */ public ResourceResolver getResourceResolver() { if (mResourceResolver == null) { - String theme = mConfigComposite.getThemeName(); + String theme = mConfigChooser.getThemeName(); if (theme == null) { displayError("Missing theme."); return null; } - boolean isProjectTheme = mConfigComposite.isProjectTheme(); + boolean isProjectTheme = mConfigChooser.getConfiguration().isProjectTheme(); Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = - mConfigListener.getConfiguredProjectResources(); + getConfiguredProjectResources(); // Get the framework resources Map<ResourceType, Map<String, ResourceValue>> frameworkResources = - mConfigListener.getConfiguredFrameworkResources(); + getConfiguredFrameworkResources(); if (configuredProjectRes == null) { displayError("Missing project resources for current configuration."); @@ -1796,10 +1685,10 @@ public class GraphicalEditorPart extends EditorPart */ @Override public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) { - if (mConfigComposite.isDisposed()) { + if (mConfigChooser.isDisposed()) { return; } - Display display = mConfigComposite.getDisplay(); + Display display = mConfigChooser.getDisplay(); display.asyncExec(new Runnable() { @Override public void run() { @@ -1810,10 +1699,10 @@ public class GraphicalEditorPart extends EditorPart /** Reload layout. <b>Must be called on the SWT thread</b> */ private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) { - if (mConfigComposite.isDisposed()) { + if (mConfigChooser.isDisposed()) { return; } - assert mConfigComposite.getDisplay().getThread() == Thread.currentThread(); + assert mConfigChooser.getDisplay().getThread() == Thread.currentThread(); boolean recompute = false; // we only care about the r class of the main project. @@ -1836,7 +1725,7 @@ public class GraphicalEditorPart extends EditorPart // However there's no recompute, as it could not be needed // (for instance a new layout) // If a resource that's not a layout changed this will trigger a recompute anyway. - mConfigComposite.updateLocales(); + mConfigChooser.updateLocales(); } // if a resources was modified. @@ -2809,23 +2698,13 @@ public class GraphicalEditorPart extends EditorPart // Update configuration if (file != null) { - mConfigComposite.resetConfigFor(file); + mConfigChooser.resetConfigFor(file); } } recomputeLayout(); } /** - * Returns the resource name of the file that is including this current layout, if any - * (may be null) - * - * @return the resource name of an including layout, or null - */ - public Reference getIncludedWithin() { - return mIncludedWithin; - } - - /** * Return all resource names of a given type, either in the project or in the * framework. * @@ -2851,7 +2730,7 @@ public class GraphicalEditorPart extends EditorPart * @return the current configuration */ public FolderConfiguration getConfiguration() { - return mConfigComposite.getCurrentConfig(); + return mConfigChooser.getConfiguration().getFullConfig(); } /** @@ -2869,10 +2748,22 @@ public class GraphicalEditorPart extends EditorPart return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion; } - public ConfigurationComposite getConfigurationComposite() { - return mConfigComposite; + /** + * Returns the associated configuration chooser + * + * @return the configuration chooser + */ + @NonNull + public ConfigurationChooser getConfigurationChooser() { + return mConfigChooser; } + /** + * Returns the associated layout actions bar + * + * @return the layout actions bar + */ + @NonNull public LayoutActionBar getLayoutActionBar() { return mActionBar; } 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 285cba2..4368db4 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 @@ -15,11 +15,9 @@ */ 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 com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.ide.common.api.INode; import com.android.ide.common.api.RuleAction; @@ -29,7 +27,8 @@ import com.android.ide.common.api.RuleAction.Toggle; import com.android.ide.common.layout.BaseViewRule; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +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.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; @@ -707,14 +706,14 @@ public class LayoutActionBar extends Composite { boolean computeAndSetRealScale(boolean redraw) { // compute average dpi of X and Y - ConfigurationComposite config = mEditor.getConfigurationComposite(); + ConfigurationChooser chooser = mEditor.getConfigurationChooser(); + Configuration config = chooser.getConfiguration(); float dpi = (config.getXDpi() + config.getYDpi()) / 2.f; // get the monitor dpi float monitor = AdtPrefs.getPrefs().getMonitorDensity(); if (monitor == 0.f) { - ResolutionChooserDialog dialog = new ResolutionChooserDialog( - config.getShell()); + ResolutionChooserDialog dialog = new ResolutionChooserDialog(chooser.getShell()); if (dialog.open() == Window.OK) { monitor = dialog.getDensity(); AdtPrefs.getPrefs().setMonitorDensity(monitor); 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 6eb5f27..86878ac 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 @@ -26,7 +26,7 @@ import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; @@ -1074,7 +1074,7 @@ public class LayoutCanvas extends Canvas { try { // Set initial state of a new file // TODO: Only set rendering target portion of the state - QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE; + QualifiedName qname = ConfigurationChooser.NAME_CONFIG_STATE; String state = AdtPlugin.getFileProperty(leavingFile, qname); xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state); @@ -1528,10 +1528,10 @@ public class LayoutCanvas extends Canvas { */ public Margins getInsets(String fqcn) { if (ViewMetadataRepository.INSETS_SUPPORTED) { - ConfigurationComposite configComposite = - mEditorDelegate.getGraphicalEditor().getConfigurationComposite(); + ConfigurationChooser configComposite = + mEditorDelegate.getGraphicalEditor().getConfigurationChooser(); String theme = configComposite.getThemeName(); - Density density = configComposite.getDensity(); + Density density = configComposite.getConfiguration().getDensity(); return ViewMetadataRepository.getInsets(fqcn, density, theme); } else { return null; 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 a3be5cc..2fbd992 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 @@ -38,7 +38,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; @@ -375,9 +375,9 @@ public class PaletteControl extends Composite { * @param target The target that has just been loaded */ public void reloadPalette(IAndroidTarget target) { - ConfigurationComposite configuration = mEditor.getConfigurationComposite(); - String theme = configuration.getThemeName(); - String device = configuration.getDevice(); + ConfigurationChooser configChooser = mEditor.getConfigurationChooser(); + String theme = configChooser.getThemeName(); + String device = configChooser.getDeviceName(); AndroidTargetData targetData = target != null ? Sdk.getCurrent().getTargetData(target) : null; if (target == mCurrentTarget && targetData == mCurrentTargetData @@ -628,7 +628,8 @@ public class PaletteControl extends Composite { } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) { scale = 0.5f; } - int dpi = mEditor.getConfigurationComposite().getDensity().getDpiValue(); + ConfigurationChooser chooser = mEditor.getConfigurationChooser(); + int dpi = chooser.getConfiguration().getDensity().getDpiValue(); while (dpi > 160) { scale = scale / 2; dpi = dpi / 2; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java index e68c47c..60e9920 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java @@ -16,12 +16,14 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.SdkConstants.DOT_PNG; import static com.android.SdkConstants.FQCN_DATE_PICKER; import static com.android.SdkConstants.FQCN_EXPANDABLE_LIST_VIEW; import static com.android.SdkConstants.FQCN_LIST_VIEW; import static com.android.SdkConstants.FQCN_TIME_PICKER; -import static com.android.SdkConstants.DOT_PNG; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.rendering.api.RenderSession; @@ -477,10 +479,15 @@ public class PreviewIconFactory { /** * Cleans up a name by removing punctuation and whitespace etc to make * it a better filename - * @param name - * @return + * @param name the name to clean + * @return a cleaned up name */ - private static String cleanup(String name) { + @NonNull + private static String cleanup(@Nullable String name) { + if (name == null) { + return ""; + } + // Extract just the characters (no whitespace, parentheses, punctuation etc) // to ensure that the filename is pretty portable StringBuilder sb = new StringBuilder(name.length()); @@ -516,8 +523,11 @@ public class PreviewIconFactory { if (themeName.startsWith(themeNamePrefix)) { themeName = themeName.substring(themeNamePrefix.length()); } - String dirName = String.format("palette-preview-r16b-%s-%s-%s", cleanup(targetName), - cleanup(themeName), cleanup(mPalette.getCurrentDevice())); + targetName = cleanup(targetName); + themeName = cleanup(themeName); + String deviceName = cleanup(mPalette.getCurrentDevice()); + String dirName = String.format("palette-preview-r16b-%s-%s-%s", targetName, + themeName, deviceName); IPath dirPath = pluginState.append(dirName); mImageDir = new File(dirPath.toOSString()); 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 8f6eb56..e0c3add 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 @@ -38,7 +38,8 @@ import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser; import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper; import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +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.gle2.IncludeFinder.Reference; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; @@ -106,11 +107,12 @@ public class RenderService { mProject = editor.getProject(); LayoutCanvas canvas = editor.getCanvasControl(); mImageFactory = canvas.getImageOverlay(); - ConfigurationComposite config = editor.getConfigurationComposite(); + ConfigurationChooser chooser = editor.getConfigurationChooser(); + Configuration config = chooser.getConfiguration(); mDensity = config.getDensity(); mXdpi = config.getXDpi(); mYdpi = config.getYDpi(); - mScreenSize = config.getCurrentConfig().getScreenSizeQualifier(); + mScreenSize = chooser.getConfiguration().getFullConfig().getScreenSizeQualifier(); mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/); mResourceResolver = editor.getResourceResolver(); mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java index 2fb16ff..c5f976f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -40,7 +40,7 @@ import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; @@ -531,15 +531,15 @@ class ClientRulesEngine implements IClientRulesEngine { @Override public int pxToDp(int px) { - ConfigurationComposite config = mRulesEngine.getEditor().getConfigurationComposite(); - float dpi = config.getDensity().getDpiValue(); + ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser(); + float dpi = chooser.getConfiguration().getDensity().getDpiValue(); return (int) (px * 160 / dpi); } @Override public int dpToPx(int dp) { - ConfigurationComposite config = mRulesEngine.getEditor().getConfigurationComposite(); - float dpi = config.getDensity().getDpiValue(); + ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser(); + float dpi = chooser.getConfiguration().getDensity().getDpiValue(); return (int) (dp * dpi / 160); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java index 5d8d700..c2035f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java @@ -35,7 +35,7 @@ import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferen import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; @@ -304,7 +304,7 @@ public abstract class VisualRefactoring extends Refactoring { try { // Duplicate the current state into the newly created file - QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE; + QualifiedName qname = ConfigurationChooser.NAME_CONFIG_STATE; String state = AdtPlugin.getFileProperty(leavingFile, qname); // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current 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 3020851..d624cb7 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 @@ -67,6 +67,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_ATTRIBUTE_SORT = AdtPlugin.PLUGIN_ID + ".attrSort"; //$NON-NLS-1$ 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$ /** singleton instance */ private final static AdtPrefs sThis = new AdtPrefs(); @@ -97,6 +98,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { private boolean mLintOnSave; private boolean mLintOnExport; private AttributeSortOrder mAttributeSort; + private boolean mSharedLayoutEditor; public static enum BuildVerbosity { /** Build verbosity "Always". Those messages are always displayed, even in silent mode */ @@ -246,6 +248,11 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { if (property == null || PREFS_LINT_ON_EXPORT.equals(property)) { mLintOnExport = mStore.getBoolean(PREFS_LINT_ON_EXPORT); } + + if (property == null || PREFS_SHARED_LAYOUT_EDITOR.equals(property)) { + mSharedLayoutEditor = mStore.getBoolean(PREFS_SHARED_LAYOUT_EDITOR); + } + } /** @@ -375,6 +382,32 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { store.setValue(PREFS_LINT_ON_EXPORT, on); } + /** + * Returns whether the layout editor is sharing a single editor for all variations + * of a single resource. The default is false. + * + * @return true if the editor should be shared + */ + public boolean isSharedLayoutEditor() { + return mSharedLayoutEditor; + } + + /** + * Sets whether the layout editor should share a single editor for all variations + * of a single resource + * + * @param on if true, use a single editor + */ + public void setSharedLayoutEditor(boolean on) { + mSharedLayoutEditor = on; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + store.setValue(PREFS_SHARED_LAYOUT_EDITOR, on); + + // TODO: If enabling a shared editor, go and close all editors that are aliasing + // the same resource except for one of them. + } + + public boolean getBuildForceErrorOnNativeLibInJar() { return mBuildForceErrorOnNativeLibInJar; } @@ -457,6 +490,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { //store.setDefault(PREFS_USE_ECLIPSE_INDENT, false); //store.setDefault(PREVS_REMOVE_EMPTY_LINES, false); //store.setDefault(PREFS_FORMAT_ON_SAVE, false); + //store.setDefault(PREFS_SHARED_LAYOUT_EDITOR, false); try { store.setDefault(PREFS_DEFAULT_DEBUG_KEYSTORE, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java index 09261c2..0fcbaa0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java @@ -127,6 +127,10 @@ public class EditorsPage extends FieldEditorPreferencePage implements IWorkbench "Format on Save", parent)); + addField(new BooleanFieldEditor(AdtPrefs.PREFS_SHARED_LAYOUT_EDITOR, + "Use a single layout editor for all configuration variations of a layout", + parent)); + boolean enabled = getPreferenceStore().getBoolean(AdtPrefs.PREFS_USE_CUSTOM_XML_FORMATTER); updateCustomFormattingOptions(enabled); } 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 new file mode 100644 index 0000000..f55cce4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import static com.android.ide.common.resources.configuration.LanguageQualifier.FAKE_LANG_VALUE; +import static com.android.ide.common.resources.configuration.RegionQualifier.FAKE_REGION_VALUE; + +import com.android.ide.common.api.Rect; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.resources.Density; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.utils.StdLogger; + +import java.lang.reflect.Constructor; +import java.util.List; + +import junit.framework.TestCase; + +@SuppressWarnings("javadoc") +public class ConfigurationTest extends TestCase { + private Configuration createConfiguration() throws Exception { + // Using reflection instead since we want to pass null to + // a constructor marked with @NonNull, so the test won't compile. + Constructor<Configuration> constructor = + Configuration.class.getDeclaredConstructor(ConfigurationChooser.class); + constructor.setAccessible(true); + ConfigurationChooser chooser = null; + return constructor.newInstance(chooser); + } + + public void test() 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); + + // Check syncing + FolderConfiguration folderConfig = configuration.getFullConfig(); + assertEquals(FAKE_LANG_VALUE, folderConfig.getLanguageQualifier().getValue()); + assertEquals(FAKE_REGION_VALUE, folderConfig.getRegionQualifier().getValue()); + assertEquals(Locale.ANY, configuration.getLocale()); + + Locale language = Locale.create(new LanguageQualifier("nb")); + configuration.setLocale(language, true /* skipSync */); + assertEquals(FAKE_LANG_VALUE, folderConfig.getLanguageQualifier().getValue()); + assertEquals(FAKE_REGION_VALUE, folderConfig.getRegionQualifier().getValue()); + + configuration.setLocale(language, false /* skipSync */); + assertEquals(FAKE_REGION_VALUE, folderConfig.getRegionQualifier().getValue()); + assertEquals("nb", folderConfig.getLanguageQualifier().getValue()); + + assertEquals("2.7in QVGA::nb-__:+Theme::notnight::", configuration.toPersistentString()); + + configuration.setActivity("foo.bar.FooActivity"); + configuration.setTheme("@android:style/Theme.Holo.Light"); + + assertEquals("2.7in QVGA", + ConfigurationChooser.getDeviceLabel(configuration.getDevice(), true)); + assertEquals("2.7in QVGA", + ConfigurationChooser.getDeviceLabel(configuration.getDevice(), false)); + assertEquals("Light", + ConfigurationChooser.getThemeLabel(configuration.getTheme(), true)); + assertEquals("Theme.Holo.Light", + ConfigurationChooser.getThemeLabel(configuration.getTheme(), false)); + assertEquals("nb", + ConfigurationChooser.getLocaleLabel(null, configuration.getLocale(), true)); + assertEquals("Norwegian Bokm\u00e5l (nb)", + ConfigurationChooser.getLocaleLabel(null, configuration.getLocale(), false)); + + assertEquals("FooActivity", + ConfigurationChooser.getActivityLabel(configuration.getActivity(), true)); + assertEquals("foo.bar.FooActivity", + ConfigurationChooser.getActivityLabel(configuration.getActivity(), false)); + + assertEquals("2.7in QVGA::nb-__:-Theme.Holo.Light::notnight::foo.bar.FooActivity", + configuration.toPersistentString()); + + assertEquals(Density.MEDIUM, configuration.getDensity()); + assertEquals(145.0f, configuration.getXDpi(), 0.001); + assertEquals(145.0f, configuration.getYDpi(), 0.001); + assertEquals(new Rect(0, 0, 320, 480), configuration.getScreenBounds()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleTest.java new file mode 100644 index 0000000..3dcf33a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.configuration; + +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; + +import junit.framework.TestCase; + +@SuppressWarnings("javadoc") +public class LocaleTest extends TestCase { + public void test() { + LanguageQualifier language1 = new LanguageQualifier("nb"); + LanguageQualifier language2 = new LanguageQualifier("no"); + RegionQualifier region1 = new RegionQualifier("NO"); + RegionQualifier region2 = new RegionQualifier("SE"); + + assertEquals(Locale.ANY, Locale.ANY); + assertFalse(Locale.ANY.hasLanguage()); + assertFalse(Locale.ANY.hasRegion()); + assertFalse(Locale.create(new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), + new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)).hasLanguage()); + assertFalse(Locale.create(new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), + new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE)).hasRegion()); + + assertEquals(Locale.create(language1), Locale.create(language1)); + assertTrue(Locale.create(language1).hasLanguage()); + assertFalse(Locale.create(language1).hasRegion()); + assertTrue(Locale.create(language1, region1).hasLanguage()); + assertTrue(Locale.create(language1, region1).hasRegion()); + + assertEquals(Locale.create(language1, region1), Locale.create(language1, region1)); + assertEquals(Locale.create(language1), Locale.create(language1)); + assertTrue(Locale.create(language1).equals(Locale.create(language1))); + assertTrue(Locale.create(language1, region1).equals(Locale.create(language1, region1))); + assertFalse(Locale.create(language1, region1).equals(Locale.create(language1, region2))); + assertFalse(Locale.create(language1).equals(Locale.create(language1, region1))); + assertFalse(Locale.create(language1).equals(Locale.create(language2))); + assertFalse(Locale.create(language1, region1).equals(Locale.create(language2, region1))); + assertEquals("Locale{nb, __}", Locale.create(language1).toString()); + assertEquals("Locale{nb, NO}", Locale.create(language1, region1).toString()); + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/Device.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/Device.java index dcd82a2..cb712f0 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/Device.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/Device.java @@ -287,4 +287,9 @@ public final class Device { hash = 31 * hash + mDefaultState.hashCode(); return hash; } + + @Override + public String toString() { + return mName; + } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/DeviceManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/DeviceManager.java index d5297d3..3662c26 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/DeviceManager.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/DeviceManager.java @@ -246,8 +246,10 @@ public class DeviceManager { try { userDevicesFile = new File(AndroidLocation.getFolder(), SdkConstants.FN_DEVICES_XML); - mUserDevices.addAll(DeviceParser.parse(userDevicesFile)); - notifyListeners(); + if (userDevicesFile.exists()) { + mUserDevices.addAll(DeviceParser.parse(userDevicesFile)); + notifyListeners(); + } } catch (AndroidLocationException e) { mLog.warning("Couldn't load user devices: %1$s", e.getMessage()); } catch (SAXException e) { @@ -263,8 +265,6 @@ public class DeviceManager { userDevicesFile.getAbsolutePath(), renamedConfig.getAbsolutePath()); userDevicesFile.renameTo(renamedConfig); } - } catch (FileNotFoundException e) { - mLog.warning("No user devices found"); } catch (ParserConfigurationException e) { mLog.error(null, "Error parsing %1$s", userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath()); diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/State.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/State.java index 1dc6961..27e5448 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/State.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/devices/State.java @@ -138,4 +138,8 @@ public class State { return hash; } + @Override + public String toString() { + return mName; + } } |