/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.tools.idea.configurations; import com.android.ide.common.resources.LocaleManager; 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.tools.idea.rendering.LocalResourceRepository; import com.android.tools.idea.rendering.Locale; import com.android.tools.idea.rendering.ProjectResourceRepository; import com.android.tools.idea.rendering.ResourceHelper; import com.android.tools.idea.rendering.multi.RenderPreviewManager; import com.android.tools.idea.rendering.multi.RenderPreviewMode; import com.google.common.collect.Lists; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.module.Module; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.ListPopup; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.awt.RelativePoint; import icons.AndroidIcons; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.util.*; import java.util.List; public class LocaleMenuAction extends FlatComboAction { private final RenderContext myRenderContext; public LocaleMenuAction(RenderContext renderContext) { myRenderContext = renderContext; Presentation presentation = getTemplatePresentation(); presentation.setDescription("Locale to render layout with inside the IDE"); updatePresentation(presentation); } @Override @NotNull protected DefaultActionGroup createPopupActionGroup(JComponent button) { DefaultActionGroup group = new DefaultActionGroup(null, true); // TODO: Offer submenus, lazily populated, which offer languages either by code or by name. // However, this doesn't currently work for the JBPopup dialog we're using as part // of the combo action (and using the JBPopup dialog rather than a Swing menu has some // other advantages: fitting in with the overall IDE look and feel (e.g. dark colors), // allowing typing to filter, etc. List<Locale> locales = getRelevantLocales(); Configuration configuration = myRenderContext.getConfiguration(); if (configuration != null && locales.size() > 0) { group.add(new SetLocaleAction(myRenderContext, getLocaleLabel(Locale.ANY, false), Locale.ANY)); group.addSeparator(); Collections.sort(locales, Locale.LANGUAGE_CODE_COMPARATOR); for (Locale locale : locales) { String title = getLocaleLabel(locale, false); VirtualFile better = ConfigurationMatcher.getBetterMatch(configuration, null, null, locale, null); if (better != null) { title = ConfigurationAction.getBetterMatchLabel(getLocaleLabel(locale, true), better, configuration.getFile()); } group.add(new SetLocaleAction(myRenderContext, title, locale)); } group.addSeparator(); } if (locales.size() > 0) { group.add(new EditTranslationAction()); } group.add(new AddTranslationAction()); group.addSeparator(); RenderPreviewMode currentMode = RenderPreviewMode.getCurrent(); if (currentMode != RenderPreviewMode.LOCALES && currentMode != RenderPreviewMode.RTL) { if (locales.size() >= 1) { ConfigurationMenuAction.addLocalePreviewAction(myRenderContext, group, true); } ConfigurationMenuAction.addRtlPreviewAction(myRenderContext, group); } else { ConfigurationMenuAction.addRemovePreviewsAction(myRenderContext, group); } return group; } /** * Like {@link ConfigurationManager#getLocales} but filters out locales not compatible * with language and region qualifiers in the current configuration's folder config * * @return the list of relevant locales in the project */ @NotNull private List<Locale> getRelevantLocales() { List<Locale> locales = new ArrayList<Locale>(); Configuration configuration = myRenderContext.getConfiguration(); if (configuration == null) { return Collections.emptyList(); } Module module = configuration.getConfigurationManager().getModule(); LanguageQualifier specificLanguage = configuration.getEditedConfig().getLanguageQualifier(); RegionQualifier specificRegion = configuration.getEditedConfig().getRegionQualifier(); // If the layout exists in a non-locale specific folder, then offer all locales, since // the user should be able to switch from this layout to some other version. We // only lock down this layout to the current locale if the layout only exists for this // locale. if (specificLanguage != null || specificRegion != null) { List<VirtualFile> variations = ResourceHelper.getResourceVariations(configuration.getFile(), false); for (VirtualFile variation : variations) { FolderConfiguration config = FolderConfiguration.getConfigForFolder(variation.getParent().getName()); if (config != null && config.getLanguageQualifier() == null) { specificLanguage = null; specificRegion = null; break; } } } LocalResourceRepository projectResources = ProjectResourceRepository.getProjectResources(module, true); Set<String> languages = projectResources != null ? projectResources.getLanguages() : Collections.<String>emptySet(); for (String language : languages) { if (specificLanguage != null && !language.equals(specificLanguage.getValue())) { continue; } LanguageQualifier languageQualifier = new LanguageQualifier(language); locales.add(Locale.create(languageQualifier)); if (projectResources != null) { SortedSet<String> regions = projectResources.getRegions(language); for (String region : regions) { if (specificRegion != null && !region.equals(specificRegion.getValue())) { continue; } locales.add(Locale.create(languageQualifier, new RegionQualifier(region))); } } } return locales; } @NotNull public static List<Locale> getAllLocales() { Set<String> languageCodes = LocaleManager.getLanguageCodes(); List<String> sorted = new ArrayList<String>(languageCodes); Collections.sort(sorted); List<Locale> locales = new ArrayList<Locale>(languageCodes.size()); for (String language : languageCodes) { Locale locale = Locale.create(new LanguageQualifier(language)); locales.add(locale); } return locales; } @Override public void update(AnActionEvent e) { super.update(e); updatePresentation(e.getPresentation()); } private void updatePresentation(Presentation presentation) { Configuration configuration = myRenderContext.getConfiguration(); boolean visible = configuration != null; if (visible) { // TEMPORARY WORKAROUND: // We don't properly sync the project locale to layouts yet, so in the mean time // show the actual locale being used rather than the intended locale, so as not // to be totally confusing: //Locale locale = configuration.isLocaleSpecificLayout() // ? configuration.getLocale() : configuration.getConfigurationManager().getLocale(); Locale locale = configuration.getLocale(); if (locale == Locale.ANY) { presentation.setIcon(AndroidIcons.Globe); } else { presentation.setIcon(locale.getFlagImage()); } String brief = getLocaleLabel(locale, true); presentation.setText(brief); } else { presentation.setIcon(AndroidIcons.Globe); } if (visible != presentation.isVisible()) { presentation.setVisible(visible); } } @Override protected int getMaxRows() { return 10; } /** * Returns a suitable label to use to display the given locale * * @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 */ @NotNull public static String getLocaleLabel(@Nullable Locale locale, boolean brief) { if (locale == null) { return ""; } if (!locale.hasLanguage()) { if (brief) { // Just use the icon return ""; } return "Default"; } 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); } } private static class SetLocaleAction extends ConfigurationAction { private final Locale myLocale; public SetLocaleAction(RenderContext renderContext, String title, @NotNull Locale locale) { // TODO: Rather than passing in the title, update the code to implement update() instead; that // way we can lazily compute the label as part of the list rendering super(renderContext, title, locale.getFlagImage()); myLocale = locale; } @Override protected void updateConfiguration(@NotNull Configuration configuration, boolean commit) { if (commit) { setProjectWideLocale(); } else { // The locale can affect the direction qualifier: don't constrain best match // search to the current direction configuration.getEditedConfig().setLayoutDirectionQualifier(null); configuration.setLocale(myLocale); } } @Override protected void pickedBetterMatch(@NotNull VirtualFile file, @NotNull VirtualFile old) { super.pickedBetterMatch(file, old); Configuration configuration = myRenderContext.getConfiguration(); if (configuration != null) { // Save project-wide configuration; not done by regular listening scheme since the previous configuration was not switched setProjectWideLocale(); } } private void setProjectWideLocale() { Configuration configuration = myRenderContext.getConfiguration(); if (configuration != null) { // Also set the project-wide locale, since locales (and rendering targets) are project wide configuration.getConfigurationManager().setLocale(myLocale); myRenderContext.requestRender(); } } } private class AddTranslationAction extends AnAction { public AddTranslationAction() { super("Add Translation...", null, AndroidIcons.Globe); } @Override public void actionPerformed(AnActionEvent e) { DataContext context = e.getDataContext(); // List known locales, except those already present List<Locale> locales = getAllLocales(); locales.removeAll(getRelevantLocales()); Collections.sort(locales, Locale.LANGUAGE_NAME_COMPARATOR); DefaultActionGroup group = new DefaultActionGroup(null, true); for (Locale locale : locales) { String title = getLocaleLabel(locale, false); group.add(new CreateLocaleAction(title, locale)); } JBPopupFactory factory = JBPopupFactory.getInstance(); ListPopup popup = factory.createActionGroupPopup("Select language to create (type to filter)", group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true, null/*onDispose*/, 10); popup.setMinimumSize(new Dimension(getMinWidth(), getMinHeight())); JComponent content = popup.getContent(); Dimension preferredSize = content.getPreferredSize(); if (preferredSize.height > 300) { preferredSize.height = 300; content.setPreferredSize(preferredSize); } RelativePoint relativePoint = JBPopupFactory.getInstance().guessBestPopupLocation(context); popup.show(relativePoint); // To use a popup menu instead: //ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.EDITOR_POPUP, group); } } private class EditTranslationAction extends ActionGroup { public EditTranslationAction() { super("Edit Translation", true); } @NotNull @Override public AnAction[] getChildren(@Nullable AnActionEvent e) { List<AnAction> children = Lists.newArrayList(); List<Locale> locales = getRelevantLocales(); for (final Locale locale : locales) { children.add(new AnAction(getLocaleLabel(locale, true), null, locale.getFlagImage()) { @Override public void actionPerformed(AnActionEvent e) { Module module = myRenderContext.getModule(); if (module == null) { return; } AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { return; } TranslationDialog dialog = new TranslationDialog(facet, locale, false); dialog.show(); if (dialog.isOK()) { if (dialog.createTranslation()) { new SetLocaleAction(myRenderContext, "", locale).actionPerformed(e); } RenderPreviewManager.bumpRevision(); } } }); } return children.toArray(new AnAction[children.size()]); } } private class CreateLocaleAction extends AnAction { private final Locale myLocale; public CreateLocaleAction(String title, @NotNull Locale locale) { super(title, null, locale.getFlagImage()); myLocale = locale; } @Override public void actionPerformed(AnActionEvent e) { Module module = myRenderContext.getModule(); if (module == null) { return; } AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { return; } TranslationDialog dialog = new TranslationDialog(facet, myLocale, true); dialog.show(); if (dialog.isOK()) { if (dialog.createTranslation()) { // Switch to the newly created translation. Reuse the SetLocaleAction // such that we don't just set the locale on the configuration, but project-wide // as well . new SetLocaleAction(myRenderContext, "", myLocale).actionPerformed(e); } RenderPreviewManager.bumpRevision(); } } } }