Java tutorial
/* * Copyright (C) 2014 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.editors.theme; import com.android.SdkConstants; import com.android.builder.model.SourceProvider; import com.android.ide.common.rendering.api.ItemResourceValue; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.res2.ResourceItem; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.ResourceUrl; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.VersionQualifier; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.sdklib.IAndroidTarget; import com.android.tools.idea.AndroidTextUtils; import com.android.tools.idea.actions.OverrideResourceAction; import com.android.tools.idea.configurations.Configuration; import com.android.tools.idea.configurations.ConfigurationManager; import com.android.tools.idea.configurations.ResourceResolverCache; import com.android.tools.idea.configurations.ThemeSelectionPanel; import com.android.tools.idea.editors.theme.datamodels.ConfiguredThemeEditorStyle; import com.android.tools.idea.editors.theme.datamodels.EditedStyleItem; import com.android.tools.idea.gradle.project.model.AndroidModuleModel; import com.android.tools.idea.javadoc.AndroidJavaDocRenderer; import com.android.tools.idea.model.AndroidModuleInfo; import com.android.tools.idea.res.*; import com.android.tools.idea.ui.resourcechooser.ChooseResourceDialog; import com.google.common.base.Predicate; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.Result; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.ui.JBColor; import com.intellij.util.Processor; import org.jetbrains.android.dom.attrs.AttributeDefinition; import org.jetbrains.android.dom.attrs.AttributeFormat; import org.jetbrains.android.dom.resources.ResourceElement; import org.jetbrains.android.dom.resources.Style; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.android.util.AndroidResourceUtil; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.List; /** * Utility class for static methods which are used in different classes of theme editor */ public class ThemeEditorUtils { private static final Logger LOG = Logger.getInstance(ThemeEditorUtils.class); private static final Cache<String, String> ourTooltipCache = CacheBuilder.newBuilder().weakValues() .maximumSize(30) // To be able to cache roughly one screen of attributes .build(); private static final Set<String> DEFAULT_THEMES = ImmutableSet.of("Theme.AppCompat.NoActionBar", "Theme.AppCompat.Light.NoActionBar"); private static final Set<String> DEFAULT_THEMES_FALLBACK = ImmutableSet.of("Theme.Material.NoActionBar", "Theme.Material.Light.NoActionBar"); private static final String[] CUSTOM_WIDGETS_JAR_PATHS = { // Bundled path "/plugins/android/lib/androidWidgets/theme-editor-widgets.jar", // Development path "/../adt/idea/android/lib/androidWidgets/theme-editor-widgets.jar", // IDEA plugin Development path "/community/android/android/lib/androidWidgets/theme-editor-widgets.jar", // IDEA plugin Development path (community) "/android/android/lib/androidWidgets/theme-editor-widgets.jar" }; private ThemeEditorUtils() { } @NotNull public static String generateToolTipText(@NotNull final ItemResourceValue resValue, @NotNull final Module module, @NotNull final Configuration configuration) { final LocalResourceRepository repository = AppResourceRepository.getAppResources(module, true); if (repository == null) { return ""; } String tooltipKey = resValue.toString() + module.toString() + configuration.toString() + repository.getModificationCount(); String cachedTooltip = ourTooltipCache.getIfPresent(tooltipKey); if (cachedTooltip != null) { return cachedTooltip; } ResourceUrl url = ResourceUrl.parse(ResolutionUtils.getResourceUrlFromQualifiedName( ResolutionUtils.getQualifiedItemName(resValue), SdkConstants.TAG_ATTR)); assert url != null; String tooltipContents = AndroidJavaDocRenderer.render(module, configuration, url); assert tooltipContents != null; ourTooltipCache.put(tooltipKey, tooltipContents); return tooltipContents; } /** * Returns html that will be displayed in attributes table for a given item. * For example: deprecated attrs will be with a strike through */ @NotNull public static String getDisplayHtml(EditedStyleItem item) { return item.isDeprecated() ? "<html><body><strike>" + item.getQualifiedName() + "</strike></body></html>" : item.getQualifiedName(); } public static boolean isThemeEditorSelected(@NotNull Project project) { for (FileEditor editor : FileEditorManager.getInstance(project).getSelectedEditors()) { if (editor instanceof ThemeEditor) { return true; } } return false; } public static void openThemeEditor(@NotNull final Project project) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { ThemeEditorVirtualFile file = ThemeEditorVirtualFile.getThemeEditorFile(project); OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file); FileEditorManager.getInstance(project).openEditor(descriptor, true); } }); } /** * Finds an ItemResourceValue for a given name in a theme inheritance tree */ @Nullable /*if there is not an item with that name*/ public static ItemResourceValue resolveItemFromParents(@NotNull final ConfiguredThemeEditorStyle theme, @NotNull String name, boolean isFrameworkAttr) { ConfiguredThemeEditorStyle currentTheme = theme; for (int i = 0; (i < ResourceResolver.MAX_RESOURCE_INDIRECTION) && currentTheme != null; i++) { ItemResourceValue item = currentTheme.getItem(name, isFrameworkAttr); if (item != null) { return item; } currentTheme = currentTheme.getParent(); } return null; } @Nullable public static Object extractRealValue(@NotNull final EditedStyleItem item, @NotNull final Class<?> desiredClass) { String value = item.getValue(); if (desiredClass == Boolean.class && ("true".equals(value) || "false".equals(value))) { return Boolean.valueOf(value); } if (desiredClass == Integer.class) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { return value; } } return value; } public static boolean acceptsFormat(@Nullable AttributeDefinition attrDefByName, @NotNull AttributeFormat want) { if (attrDefByName == null) { return false; } return attrDefByName.getFormats().contains(want); } @NotNull private static ImmutableCollection<ConfiguredThemeEditorStyle> findThemes( @NotNull Collection<ConfiguredThemeEditorStyle> themes, final @NotNull Set<String> names) { return ImmutableSet.copyOf(Iterables.filter(themes, new Predicate<ConfiguredThemeEditorStyle>() { @Override public boolean apply(@Nullable ConfiguredThemeEditorStyle theme) { return theme != null && names.contains(theme.getName()); } })); } @NotNull public static ImmutableList<Module> findAndroidModules(@NotNull Project project) { final ModuleManager manager = ModuleManager.getInstance(project); final ImmutableList.Builder<Module> builder = ImmutableList.builder(); for (Module module : manager.getModules()) { final AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null) { builder.add(module); } } return builder.build(); } @NotNull public static ImmutableList<String> getDefaultThemeNames(@NotNull ThemeResolver themeResolver) { Collection<ConfiguredThemeEditorStyle> readOnlyLibThemes = themeResolver.getExternalLibraryThemes(); Collection<ConfiguredThemeEditorStyle> foundThemes = new HashSet<ConfiguredThemeEditorStyle>(); foundThemes.addAll(findThemes(readOnlyLibThemes, DEFAULT_THEMES)); if (foundThemes.isEmpty()) { Collection<ConfiguredThemeEditorStyle> readOnlyFrameworkThemes = themeResolver.getFrameworkThemes(); foundThemes = new HashSet<ConfiguredThemeEditorStyle>(); foundThemes.addAll(findThemes(readOnlyFrameworkThemes, DEFAULT_THEMES_FALLBACK)); if (foundThemes.isEmpty()) { foundThemes.addAll(readOnlyLibThemes); foundThemes.addAll(readOnlyFrameworkThemes); } } Set<String> temporarySet = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER); for (ConfiguredThemeEditorStyle theme : foundThemes) { temporarySet.add(theme.getQualifiedName()); } return ImmutableList.copyOf(temporarySet); } public static int getMinApiLevel(@NotNull Module module) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { return 1; } AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(facet); return moduleInfo.getMinSdkVersion().getApiLevel(); } /** * Returns the URL for the theme editor custom widgets jar */ @Nullable public static URL getCustomWidgetsJarUrl() { String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath()); StringBuilder notFoundPaths = new StringBuilder(); for (String path : CUSTOM_WIDGETS_JAR_PATHS) { String jarPath = homePath + path; VirtualFile root = LocalFileSystem.getInstance() .findFileByPath(FileUtil.toSystemIndependentName(jarPath)); if (root != null) { File rootFile = VfsUtilCore.virtualToIoFile(root); if (rootFile.exists()) { try { LOG.debug("Theme editor custom widgets found at " + jarPath); return rootFile.toURI().toURL(); } catch (MalformedURLException e) { LOG.error(e); } } } else { notFoundPaths.append(jarPath).append('\n'); } } LOG.error("Unable to find theme-editor-widgets.jar in paths:\n" + notFoundPaths.toString()); return null; } /** * Creates a new style * @param project the project where the new style is being created * @param resourceDir the res/ directory where the new style is being created * @param newStyleName the new style name * @param parentStyleName the name of the new style parent * @param fileName name of the xml file where the style will be added (usually "styles.xml") * @param folderNames folder names where the style will be added * @return true if the style was created or false otherwise */ public static boolean createNewStyle(@NotNull final Project project, @NotNull final VirtualFile resourceDir, final @NotNull String newStyleName, final @Nullable String parentStyleName, final @NotNull String fileName, final @NotNull List<String> folderNames) { return new WriteCommandAction<Boolean>(project, "Create new style " + newStyleName) { @Override protected void run(@NotNull Result<Boolean> result) { CommandProcessor.getInstance().markCurrentCommandAsGlobal(project); result.setResult(AndroidResourceUtil.createValueResource(project, resourceDir, newStyleName, null, ResourceType.STYLE, fileName, folderNames, new Processor<ResourceElement>() { @Override public boolean process(ResourceElement element) { assert element instanceof Style; final Style style = (Style) element; if (parentStyleName != null) { style.getParentStyle().setStringValue(parentStyleName); } return true; } })); } }.execute().getResultObject(); } /** * Creates a new style by displaying the dialog of the {@link NewStyleDialog}. * @param defaultParentStyle is used in NewStyleDialog, will be preselected in the parent text field and name will be suggested based on it * @param themeEditorContext current theme editor context * @param isTheme whether theme or style will be created * @param message is used in NewStyleDialog to display message to user * @return the new style name or null if the style wasn't created */ @Nullable public static String showCreateNewStyleDialog(@Nullable ConfiguredThemeEditorStyle defaultParentStyle, @NotNull final ThemeEditorContext themeEditorContext, boolean isTheme, boolean enableParentChoice, @Nullable final String message, @Nullable ThemeSelectionPanel.ThemeChangedListener themeChangedListener) { // if isTheme is true, defaultParentStyle shouldn't be null String defaultParentStyleName = null; if (isTheme && defaultParentStyle == null) { ImmutableList<String> defaultThemes = getDefaultThemeNames(themeEditorContext.getThemeResolver()); defaultParentStyleName = !defaultThemes.isEmpty() ? defaultThemes.get(0) : null; } else if (defaultParentStyle != null) { defaultParentStyleName = defaultParentStyle.getQualifiedName(); } final NewStyleDialog dialog = new NewStyleDialog(isTheme, themeEditorContext, defaultParentStyleName, (defaultParentStyle == null) ? null : defaultParentStyle.getName(), message); dialog.enableParentChoice(enableParentChoice); if (themeChangedListener != null) { dialog.setThemeChangedListener(themeChangedListener); } boolean createStyle = dialog.showAndGet(); if (!createStyle) { return null; } int minModuleApi = getMinApiLevel(themeEditorContext.getCurrentContextModule()); int minAcceptableApi = ResolutionUtils.getOriginalApiLevel( ResolutionUtils.getStyleResourceUrl(dialog.getStyleParentName()), themeEditorContext.getProject()); final String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STYLE); FolderConfiguration config = new FolderConfiguration(); if (minModuleApi < minAcceptableApi) { VersionQualifier qualifier = new VersionQualifier(minAcceptableApi); config.setVersionQualifier(qualifier); } if (fileName == null) { LOG.error("Couldn't find a default filename for ResourceType.STYLE"); return null; } final List<String> dirNames = Collections.singletonList(config.getFolderName(ResourceFolderType.VALUES)); String parentStyleName = dialog.getStyleParentName(); Module module = themeEditorContext.getCurrentContextModule(); AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { LOG.error("Create new style for non-Android module " + module.getName()); return null; } Project project = module.getProject(); VirtualFile resourceDir = facet.getPrimaryResourceDir(); if (resourceDir == null) { AndroidUtils.reportError(project, AndroidBundle.message("check.resource.dir.error", module.getName())); return null; } boolean isCreated = createNewStyle(project, resourceDir, dialog.getStyleName(), parentStyleName, fileName, dirNames); return isCreated ? dialog.getStyleName() : null; } /** * Checks if the selected theme is AppCompat */ public static boolean isSelectedAppCompatTheme(@NotNull ThemeEditorContext context) { ConfiguredThemeEditorStyle currentTheme = context.getCurrentTheme(); return currentTheme != null && isAppCompatTheme(currentTheme); } /** * Checks if a theme is AppCompat */ public static boolean isAppCompatTheme(@NotNull ConfiguredThemeEditorStyle configuredThemeEditorStyle) { ConfiguredThemeEditorStyle currentTheme = configuredThemeEditorStyle; for (int i = 0; (i < ResourceResolver.MAX_RESOURCE_INDIRECTION) && currentTheme != null; i++) { // for loop ensures that we don't run into cyclic theme inheritance. //TODO: This check is not enough. User themes could also start with "Theme.AppCompat" and not be AppCompat if (currentTheme.getName().startsWith("Theme.AppCompat") && currentTheme.getSourceModule() == null) { return true; } currentTheme = currentTheme.getParent(); } return false; } /** * Copies a theme to a values folder with api version apiLevel, * potentially creating the necessary folder or file. * @param apiLevel api level of the folder the theme is copied to * @param toBeCopied theme to be copied */ public static void copyTheme(int apiLevel, @NotNull final XmlTag toBeCopied) { ApplicationManager.getApplication().assertWriteAccessAllowed(); PsiFile file = toBeCopied.getContainingFile(); assert file instanceof XmlFile : file; ResourceFolderType folderType = ResourceHelper.getFolderType(file); assert folderType != null : file; FolderConfiguration config = ResourceHelper.getFolderConfiguration(file); assert config != null : file; VersionQualifier qualifier = new VersionQualifier(apiLevel); config.setVersionQualifier(qualifier); String folder = config.getFolderName(folderType); if (folderType != ResourceFolderType.VALUES) { OverrideResourceAction.forkResourceFile((XmlFile) file, folder, false); } else { XmlTag tag = OverrideResourceAction .getValueTag(PsiTreeUtil.getParentOfType(toBeCopied, XmlTag.class, false)); if (tag != null) { PsiDirectory dir = null; PsiDirectory resFolder = file.getParent(); if (resFolder != null) { resFolder = resFolder.getParent(); } if (resFolder != null) { dir = resFolder.findSubdirectory(folder); if (dir == null) { dir = resFolder.createSubdirectory(folder); } } OverrideResourceAction.forkResourceValue(toBeCopied.getProject(), tag, file, dir, false); } } } /** * Returns version qualifier of FolderConfiguration. * Returns -1, if FolderConfiguration has default version */ public static int getVersionFromConfiguration(@NotNull FolderConfiguration configuration) { VersionQualifier qualifier = configuration.getVersionQualifier(); return (qualifier != null) ? qualifier.getVersion() : -1; } /** * Returns the smallest api level of the folders in folderNames. * Returns Integer.MAX_VALUE if folderNames is empty. */ public static int getMinFolderApi(@NotNull List<String> folderNames, @NotNull Module module) { int minFolderApi = Integer.MAX_VALUE; int minModuleApi = getMinApiLevel(module); for (String folderName : folderNames) { FolderConfiguration folderConfig = FolderConfiguration.getConfigForFolder(folderName); if (folderConfig != null) { VersionQualifier version = folderConfig.getVersionQualifier(); int folderApi = version != null ? version.getVersion() : minModuleApi; minFolderApi = Math.min(minFolderApi, folderApi); } } return minFolderApi; } @NotNull public static Configuration getConfigurationForModule(@NotNull Module module) { Project project = module.getProject(); final AndroidFacet facet = AndroidFacet.getInstance(module); assert facet != null : "moduleComboModel must contain only Android modules"; ConfigurationManager configurationManager = facet.getConfigurationManager(); // Using the project virtual file to set up configuration for the theme editor // That fact is hard-coded in computeBestDevice() method in Configuration.java // BEWARE if attempting to modify to use a different virtual file final VirtualFile projectFile = project.getProjectFile(); assert projectFile != null; return configurationManager.getConfiguration(projectFile); } /** * Given a {@link SourceProvider}, it returns a list of all the available ResourceFolderRepositories */ @NotNull public static List<ResourceFolderRepository> getResourceFolderRepositoriesFromSourceSet( @NotNull AndroidFacet facet, @Nullable SourceProvider provider) { if (provider == null) { return Collections.emptyList(); } Collection<File> resDirectories = provider.getResDirectories(); LocalFileSystem fileSystem = LocalFileSystem.getInstance(); List<ResourceFolderRepository> folders = Lists.newArrayListWithExpectedSize(resDirectories.size()); for (File dir : resDirectories) { VirtualFile virtualFile = fileSystem.findFileByIoFile(dir); if (virtualFile != null) { folders.add(ResourceFolderRegistry.get(facet, virtualFile)); } } return folders; } /** * Returns the color that should be used for the background of the preview panel depending on the background color * of the theme being displayed, so as to always keep some contrast between the two. */ public static JBColor getGoodContrastPreviewBackground(@NotNull ConfiguredThemeEditorStyle theme, @NotNull ResourceResolver resourceResolver) { ItemResourceValue themeColorBackgroundItem = resolveItemFromParents(theme, "colorBackground", true); ResourceValue backgroundResourceValue = resourceResolver.resolveResValue(themeColorBackgroundItem); if (backgroundResourceValue != null) { String colorBackgroundValue = backgroundResourceValue.getValue(); Color colorBackground = ResourceHelper.parseColor(colorBackgroundValue); if (colorBackground != null) { float backgroundDistance = MaterialColorUtils.colorDistance(colorBackground, ThemeEditorComponent.PREVIEW_BACKGROUND); if (backgroundDistance < ThemeEditorComponent.COLOR_DISTANCE_THRESHOLD && backgroundDistance < MaterialColorUtils.colorDistance(colorBackground, ThemeEditorComponent.ALT_PREVIEW_BACKGROUND)) { return ThemeEditorComponent.ALT_PREVIEW_BACKGROUND; } } } return ThemeEditorComponent.PREVIEW_BACKGROUND; } /** * Interface to visit all the available {@link LocalResourceRepository} */ public interface ResourceFolderVisitor { /** * @param resources a repository containing resources * @param moduleName the module name * @param variantName string that identifies the variant used to obtain the resources * @param isSelected true if the current passed repository is in an active source set */ void visitResourceFolder(@NotNull LocalResourceRepository resources, String moduleName, @NotNull String variantName, boolean isSelected); } /** * Visits every ResourceFolderRepository. It visits every resource in order, meaning that the later calls may override resources from * previous ones. */ public static void acceptResourceResolverVisitor(final @NotNull AndroidFacet mainFacet, final @NotNull ResourceFolderVisitor visitor) { // Get all the dependencies of the module in reverse order (first one is the lowest priority one) List<AndroidFacet> dependencies = Lists .reverse(AndroidUtils.getAllAndroidDependencies(mainFacet.getModule(), true)); // The order of iteration here is important since the resources from the mainFacet will override those in the dependencies. for (AndroidFacet dependency : Iterables.concat(dependencies, ImmutableList.of(mainFacet))) { AndroidModuleModel androidModel = AndroidModuleModel.get(dependency); if (androidModel == null) { // For non gradle module, get the main source provider SourceProvider provider = dependency.getMainSourceProvider(); for (LocalResourceRepository resourceRepository : getResourceFolderRepositoriesFromSourceSet( dependency, provider)) { visitor.visitResourceFolder(resourceRepository, dependency.getName(), provider.getName(), true); } } else { // For gradle modules, get all source providers and go through them // We need to iterate the providers in the returned to make sure that they correctly override each other List<SourceProvider> activeProviders = androidModel.getActiveSourceProviders(); for (SourceProvider provider : activeProviders) { for (LocalResourceRepository resourceRepository : getResourceFolderRepositoriesFromSourceSet( dependency, provider)) { visitor.visitResourceFolder(resourceRepository, dependency.getName(), provider.getName(), true); } } // Not go through all the providers that are not in the activeProviders ImmutableSet<SourceProvider> selectedProviders = ImmutableSet.copyOf(activeProviders); for (SourceProvider provider : androidModel.getAllSourceProviders()) { if (!selectedProviders.contains(provider)) { for (LocalResourceRepository resourceRepository : getResourceFolderRepositoriesFromSourceSet( dependency, provider)) { visitor.visitResourceFolder(resourceRepository, dependency.getName(), provider.getName(), false); } } } } } } /** * Returns the list of the qualified names of all the user-defined themes available from a given module */ @NotNull public static ImmutableList<String> getModuleThemeQualifiedNamesList(@NotNull Module module) { AndroidFacet facet = AndroidFacet.getInstance(module); assert facet != null; ConfigurationManager manager = facet.getConfigurationManager(); // We create a new ResourceResolverCache instead of using cache from myConfiguration to optimize memory instead of time/speed, // because we are about to create a lot of instances of ResourceResolver here that won't be used outside of this method final ResourceResolverCache resolverCache = new ResourceResolverCache(manager); final IAndroidTarget target = manager.getTarget(); final Map<ResourceValue, Boolean> cache = new HashMap<ResourceValue, Boolean>(); final Set<String> themeNamesSet = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER); ResourceFolderVisitor visitor = new ResourceFolderVisitor() { @Override public void visitResourceFolder(@NotNull LocalResourceRepository resources, String moduleName, @NotNull String variantName, boolean isSelected) { if (!isSelected) { return; } for (String simpleThemeName : resources.getItemsOfType(ResourceType.STYLE)) { String themeStyleResourceUrl = SdkConstants.STYLE_RESOURCE_PREFIX + simpleThemeName; List<ResourceItem> themeItems = resources.getResourceItem(ResourceType.STYLE, simpleThemeName); assert themeItems != null; for (ResourceItem themeItem : themeItems) { ResourceResolver resolver = resolverCache.getResourceResolver(target, themeStyleResourceUrl, themeItem.getConfiguration()); ResourceValue themeItemResourceValue = themeItem.getResourceValue(false); assert themeItemResourceValue != null; if (resolver.isTheme(themeItemResourceValue, cache)) { themeNamesSet.add(simpleThemeName); break; } } } } }; acceptResourceResolverVisitor(facet, visitor); return ImmutableList.copyOf(themeNamesSet); } @NotNull public static ChooseResourceDialog getResourceDialog(@NotNull EditedStyleItem item, @NotNull ThemeEditorContext context, EnumSet<ResourceType> allowedTypes) { Module module = context.getModuleForResources(); ItemResourceValue itemSelectedValue = item.getSelectedValue(); String value = itemSelectedValue.getValue(); boolean isFrameworkValue = itemSelectedValue.isFramework(); String nameSuggestion = value; ResourceUrl url = ResourceUrl.parse(value, isFrameworkValue); if (url != null) { nameSuggestion = url.name; } nameSuggestion = getDefaultResourceName(context, nameSuggestion); ChooseResourceDialog.ResourceNameVisibility resourceNameVisibility = ChooseResourceDialog.ResourceNameVisibility.FORCE; if (nameSuggestion.startsWith("#")) { nameSuggestion = null; resourceNameVisibility = ChooseResourceDialog.ResourceNameVisibility.SHOW; } ChooseResourceDialog dialog = ChooseResourceDialog.builder().setModule(module).setTypes(allowedTypes) .setCurrentValue(value).setIsFrameworkValue(isFrameworkValue) .setResourceNameVisibility(resourceNameVisibility).setResourceNameSuggestion(nameSuggestion) .build(); dialog.setUseGlobalUndo(true); return dialog; } /** * Build a name for a new resource based on a provided name. * @param initialName a name that result should be based on (that might not be vacant) */ @NotNull private static String getDefaultResourceName(@NotNull ThemeEditorContext context, final @NotNull String initialName) { if (context.getCurrentTheme() == null || !context.getCurrentTheme().isReadOnly()) { // If the currently selected theme is not read-only, then the expected // behaviour of color picker would be to edit the existing resource. return initialName; } final ResourceResolver resolver = context.getResourceResolver(); assert resolver != null; final ResourceValue value = resolver.findResValue(SdkConstants.COLOR_RESOURCE_PREFIX + initialName, false); // Value doesn't exist, safe to use initial guess if (value == null) { return initialName; } // Given value exist, need to add a suffix to initialName to make it unique for (int i = 1; i <= 50; ++i) { final String name = initialName + "_" + i; if (resolver.findResValue(SdkConstants.COLOR_RESOURCE_PREFIX + name, false) == null) { // Found a vacant name return name; } } // Made 50 iterations and still no luck finding a vacant name // Just set a default name to empty string so user have to insert the name manually return ""; } /** * Returns a more user-friendly name of a given theme. * Aimed at framework themes with names of the form Theme.*.Light.* * or Theme.*.* */ @NotNull public static String simplifyThemeName(@NotNull ConfiguredThemeEditorStyle theme) { String result; String name = theme.getQualifiedName(); String[] pieces = name.split("\\."); if (pieces.length > 1 && !"Light".equals(pieces[1])) { result = pieces[1]; } else { result = "Theme"; } ConfiguredThemeEditorStyle parent = theme; while (parent != null) { if ("Theme.Light".equals(parent.getName())) { return result + " Light"; } else { parent = parent.getParent(); } } return result + " Dark"; } /** * Returns a string with the words concatenated into an enumeration w1, w2, ..., w(n-1) and wn */ @NotNull public static String generateWordEnumeration(@NotNull Collection<String> words) { return AndroidTextUtils.generateCommaSeparatedList(words, "and"); } @NotNull public static Font scaleFontForAttribute(@NotNull Font font) { // Use Math.ceil to ensure that the result is a font with an integer point size return font.deriveFont((float) Math.ceil(font.getSize() * ThemeEditorConstants.ATTRIBUTES_FONT_SCALE)); } public static void setInheritsPopupMenuRecursive(JComponent comp) { comp.setInheritsPopupMenu(true); for (Component child : comp.getComponents()) { if (child instanceof JComponent) { setInheritsPopupMenuRecursive((JComponent) child); } } } }