blob: f9bab8d5065df4c3169044a5d96910dbbff8e842 [file] [log] [blame]
/*
* 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.tools.idea.editors.theme.datamodels.ThemeEditorStyle;
import com.android.tools.idea.editors.theme.ThemeResolver;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.model.ManifestInfo.ActivityAttributes;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.ui.treeStructure.Tree;
import icons.AndroidIcons;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.*;
import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
import static com.android.ide.common.resources.ResourceResolver.THEME_NAME;
/**
* Theme selection dialog.
* <p/>
* TODO: In the future, make it easy to create new themes here, as well as assigning a theme
* to an activity.
*/
public class ThemeSelectionPanel implements TreeSelectionListener, ListSelectionListener, Disposable {
private static final String DEVICE_LIGHT_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault.Light";
private static final String DEVICE_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault";
private static final String HOLO_LIGHT_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo.Light";
private static final String HOLO_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo";
private static final String MATERIAL_LIGHT_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Material.Light";
private static final String MATERIAL_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Material";
private static final String LIGHT_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Light";
private static final String ANDROID_THEME = ANDROID_STYLE_RESOURCE_PREFIX + "Theme";
private static final String ANDROID_THEME_PREFIX = ANDROID_STYLE_RESOURCE_PREFIX + "Theme.";
private static final String PROJECT_THEME = STYLE_RESOURCE_PREFIX + "Theme";
private static final String PROJECT_THEME_PREFIX = STYLE_RESOURCE_PREFIX + "Theme.";
private static final String DIALOG_SUFFIX = ".Dialog";
private static final String DIALOG_PART = ".Dialog.";
private static final SimpleTextAttributes SEARCH_HIGHLIGHT_ATTRIBUTES =
new SimpleTextAttributes(null, JBColor.MAGENTA, null, SimpleTextAttributes.STYLE_BOLD);
@NotNull private final Configuration myConfiguration;
@NotNull private final ThemeSelectionDialog myDialog;
@NotNull private JBList myThemeList;
@NotNull private Tree myCategoryTree;
@NotNull private JPanel myContentPanel;
@NotNull private ThemeFilterComponent myFilter;
@Nullable private List<String> myFrameworkThemes;
@Nullable private List<String> myProjectThemes;
@Nullable private List<String> myLibraryThemes;
@Nullable private static Deque<String> ourRecent;
@Nullable private ThemeCategory myCategory = ThemeCategory.ALL;
@NotNull private Map<ThemeCategory, List<String>> myThemeMap = Maps.newEnumMap(ThemeCategory.class);
@NotNull private ThemeResolver myThemeResolver;
private boolean myIgnore;
public ThemeSelectionPanel(@NotNull ThemeSelectionDialog dialog, @NotNull Configuration configuration) {
myDialog = dialog;
myConfiguration = configuration;
myThemeResolver = new ThemeResolver(configuration);
String currentTheme = configuration.getTheme();
touchTheme(currentTheme);
myCategoryTree.setModel(new CategoryModel());
myCategoryTree.setRootVisible(false);
myCategoryTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
myCategoryTree.addTreeSelectionListener(this);
setInitialSelection(currentTheme);
myThemeList.addListSelectionListener(this);
myThemeList.setCellRenderer(new ColoredListCellRenderer() {
@Override
protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
setIcon(AndroidIcons.Themes);
String style = (String)value;
String filter = myFilter.getFilter();
if (style.startsWith(ANDROID_THEME_PREFIX)) {
style = style.substring(ANDROID_THEME_PREFIX.length());
}
else if (style.startsWith(PROJECT_THEME_PREFIX)) {
style = style.substring(PROJECT_THEME_PREFIX.length());
}
else if (style.startsWith(STYLE_RESOURCE_PREFIX)) {
style = style.substring(STYLE_RESOURCE_PREFIX.length());
}
else if (style.equals(ANDROID_THEME) || style.equals(PROJECT_THEME)) {
style = THEME_NAME;
}
if (!filter.isEmpty()) {
int matchIndex = StringUtil.indexOfIgnoreCase(style, filter, index + 1);
if (matchIndex != -1) {
if (matchIndex > 0) {
append(style.substring(0, matchIndex), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
int matchEnd = matchIndex + filter.length();
append(style.substring(matchIndex, matchEnd), SEARCH_HIGHLIGHT_ATTRIBUTES);
if (matchEnd < style.length()) {
append(style.substring(matchEnd), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
return;
}
}
int lastDot = style.lastIndexOf('.');
if (lastDot > 0) {
append(style.substring(0, lastDot + 1), SimpleTextAttributes.GRAY_ATTRIBUTES);
append(style.substring(lastDot + 1), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
else {
append(style, SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
}
});
}
private void setInitialSelection(@Nullable String currentTheme) {
if (currentTheme == null) {
myCategoryTree.setSelectionRow(0);
return;
}
if (currentTheme.startsWith(HOLO_LIGHT_PREFIX)) {
selectCategory(ThemeCategory.HOLO_LIGHT, true);
}
else if (currentTheme.startsWith(HOLO_PREFIX)) {
selectCategory(ThemeCategory.HOLO, true);
}
if (currentTheme.startsWith(MATERIAL_LIGHT_PREFIX)) {
selectCategory(ThemeCategory.MATERIAL_LIGHT, true);
}
else if (currentTheme.startsWith(MATERIAL_PREFIX)) {
selectCategory(ThemeCategory.MATERIAL, true);
}
else if (currentTheme.startsWith(DEVICE_PREFIX)) {
selectCategory(ThemeCategory.DEVICE, true);
}
else if (currentTheme.startsWith(STYLE_RESOURCE_PREFIX)) {
selectCategory(ThemeCategory.PROJECT, true);
}
else {
selectCategory(ThemeCategory.ALL, true);
}
updateThemeList();
myThemeList.setSelectedValue(currentTheme, true);
}
private void selectCategory(ThemeCategory category, boolean updateList) {
try {
myIgnore = true;
myCategoryTree.setSelectionPath(new TreePath(new ThemeCategory[]{ThemeCategory.ROOT, category}));
myCategory = category;
}
finally {
myIgnore = false;
}
if (updateList) {
updateThemeList();
}
}
@NotNull
public JPanel getContentPanel() {
return myContentPanel;
}
@NotNull
private List<String> getThemes(@Nullable ThemeCategory category) {
if (category == null) {
return Collections.emptyList();
}
List<String> themes = myThemeMap.get(category);
if (themes != null) {
return themes;
}
themes = new ArrayList<String>(50);
switch (category) {
case RECENT:
if (ourRecent != null) {
for (String theme : ourRecent) {
themes.add(theme);
}
}
break;
case HOLO:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(HOLO_PREFIX) && !theme.startsWith(HOLO_LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case HOLO_LIGHT:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(HOLO_LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case MATERIAL:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(MATERIAL_PREFIX) && !theme.startsWith(MATERIAL_LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case MATERIAL_LIGHT:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(MATERIAL_LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case PROJECT:
for (String theme : getProjectThemes()) {
themes.add(theme);
}
break;
case CLASSIC:
for (String theme : getFrameworkThemes()) {
if (!theme.startsWith(HOLO_PREFIX) && !theme.startsWith(DEVICE_PREFIX)) {
themes.add(theme);
}
}
break;
case CLASSIC_LIGHT:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case LIGHT:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(HOLO_LIGHT_PREFIX) || theme.startsWith(LIGHT_PREFIX) || theme.startsWith(DEVICE_LIGHT_PREFIX)
|| theme.startsWith(MATERIAL_LIGHT_PREFIX)) {
themes.add(theme);
}
}
break;
case DEVICE:
for (String theme : getFrameworkThemes()) {
if (theme.startsWith(DEVICE_PREFIX)) {
themes.add(theme);
}
}
break;
case DIALOGS:
for (String theme : getProjectThemes()) {
if (theme.endsWith(DIALOG_SUFFIX) || theme.contains(DIALOG_PART)) {
themes.add(theme);
}
}
for (String theme : getFrameworkThemes()) {
if (theme.endsWith(DIALOG_SUFFIX) || theme.contains(DIALOG_PART)) {
themes.add(theme);
}
}
break;
case MANIFEST: {
ManifestInfo manifest = ManifestInfo.get(myConfiguration.getModule());
Map<String, ActivityAttributes> activityAttributesMap = manifest.getActivityAttributesMap();
/*
TODO: Until we don't sort the theme lists automatically, no need to call out the preferred one first
String activity = myConfiguration.getActivity();
if (activity != null) {
String theme = activityThemes.get(activity);
if (theme != null) {
themes.add(theme);
}
}
*/
String manifestTheme = manifest.getManifestTheme();
Set<String> allThemes = new HashSet<String>();
if (manifestTheme != null) {
allThemes.add(manifestTheme);
}
for (ActivityAttributes info : activityAttributesMap.values()) {
if (info.getTheme() != null) {
allThemes.add(info.getTheme());
}
}
List<String> sorted = new ArrayList<String>(allThemes);
Collections.sort(sorted);
for (String theme : sorted) {
themes.add(theme);
}
break;
}
case ALL:
for (String theme : getProjectThemes()) {
themes.add(theme);
}
for (String theme : getFrameworkThemes()) {
themes.add(theme);
}
for (String theme : getLibraryThemes()) {
themes.add(theme);
}
break;
case ROOT:
default:
assert false : category;
break;
}
myThemeMap.put(category, themes);
return themes;
}
private void updateThemeList() {
if (myCategory == null) {
return;
}
String selected = (String)myThemeList.getSelectedValue();
SortedListModel<String> model = new SortedListModel<String>(String.CASE_INSENSITIVE_ORDER);
String filter = myFilter.getFilter();
List<String> themes = getThemes(myCategory);
for (String theme : themes) {
if (matchesFilter(theme, filter)) {
model.add(theme);
}
}
myThemeList.setModel(model);
if (selected != null) {
myThemeList.setSelectedValue(selected, true /*shouldScroll*/);
}
else if (model.getSize() > 0) {
myThemeList.setSelectedIndex(0);
}
}
private static boolean matchesFilter(String theme, String filter) {
int index = theme.lastIndexOf('/');
return filter.isEmpty() || StringUtil.indexOfIgnoreCase(theme, filter, index + 1) != -1;
}
private List<String> getFrameworkThemes() {
if (myFrameworkThemes == null) {
myFrameworkThemes = getSortedNames(getPublicThemes(myThemeResolver.getFrameworkThemes()));
}
return myFrameworkThemes;
}
private List<String> getProjectThemes() {
if (myProjectThemes == null) {
myProjectThemes = getSortedNames(getPublicThemes(myThemeResolver.getLocalThemes()));
}
return myProjectThemes;
}
private List<String> getLibraryThemes() {
if (myLibraryThemes == null) {
myLibraryThemes = getSortedNames(getPublicThemes(myThemeResolver.getExternalLibraryThemes()));
}
return myLibraryThemes;
}
// ---- Implements ListSelectionListener ----
@Override
public void valueChanged(ListSelectionEvent listSelectionEvent) {
if (myIgnore) {
return;
}
myDialog.checkValidation();
// TODO: Perhaps show the full theme somewhere, perhaps list the theme definitions, perhaps
// enable/disable actions related to the theme: assign to activity, open related definition,
// create new theme, etc.
//
// Perhaps even perform render preview?
}
// ---- Implements TreeSelectionListener ----
@Override
public void valueChanged(TreeSelectionEvent treeSelectionEvent) {
if (myIgnore) {
return;
}
TreePath path = treeSelectionEvent.getPath();
if (path == null) {
return;
}
myCategory = (ThemeCategory)path.getLastPathComponent();
updateThemeList();
if (myThemeList.getModel().getSize() > 0) {
myThemeList.setSelectedIndex(0);
}
}
@Nullable
public String getTheme() {
String selected = (String)myThemeList.getSelectedValue();
touchTheme(selected);
return selected;
}
private static void touchTheme(@Nullable String selected) {
if (selected != null) {
if (ourRecent == null || !ourRecent.contains(selected)) {
if (ourRecent == null) {
ourRecent = new LinkedList<String>();
}
ourRecent.addFirst(selected);
}
}
}
public JComponent getPreferredFocusedComponent() {
return myCategoryTree;
}
@Override
public void dispose() {
myFilter.dispose();
}
private class CategoryModel implements TreeModel {
@NotNull private final Map<ThemeCategory, List<ThemeCategory>> myLabels;
CategoryModel() {
myLabels = Maps.newHashMap();
List<ThemeCategory> topLevel = Lists.newArrayList();
if (ourRecent != null) {
topLevel.add(ThemeCategory.RECENT);
}
if (!getThemes(ThemeCategory.MANIFEST).isEmpty()) {
topLevel.add(ThemeCategory.MANIFEST);
}
if (!getThemes(ThemeCategory.PROJECT).isEmpty()) {
topLevel.add(ThemeCategory.PROJECT);
}
AndroidModuleInfo info = AndroidModuleInfo.get(myConfiguration.getConfigurationManager().getModule());
if (info != null && info.getBuildSdkVersion() != null && info.getBuildSdkVersion().getFeatureLevel() >= 21) {
topLevel.add(ThemeCategory.MATERIAL);
topLevel.add(ThemeCategory.MATERIAL_LIGHT);
}
topLevel.add(ThemeCategory.HOLO);
topLevel.add(ThemeCategory.HOLO_LIGHT);
if (info == null || info.getMinSdkVersion().getFeatureLevel() <= 14) {
topLevel.add(ThemeCategory.CLASSIC);
topLevel.add(ThemeCategory.CLASSIC_LIGHT);
}
topLevel.add(ThemeCategory.DEVICE);
topLevel.add(ThemeCategory.DIALOGS);
topLevel.add(ThemeCategory.LIGHT);
topLevel.add(ThemeCategory.ALL);
myLabels.put(ThemeCategory.ROOT, topLevel);
// TODO: Use tree to add nesting; e.g. add holo light as a category under holo?
//myLabels.put(ThemeCategory.LIGHT, Arrays.asList(ThemeCategory.ALL, ThemeCategory.DIALOGS));
}
@Override
public Object getRoot() {
return ThemeCategory.ROOT;
}
@Override
public Object getChild(Object parent, int index) {
assert parent instanceof ThemeCategory;
return myLabels.get(parent).get(index);
}
@Override
public int getChildCount(Object parent) {
assert parent instanceof ThemeCategory;
List<ThemeCategory> list = myLabels.get(parent);
return list == null ? 0 : list.size();
}
@Override
public boolean isLeaf(Object node) {
assert node instanceof ThemeCategory;
return myLabels.get(node) == null;
}
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
}
@Override
public int getIndexOfChild(Object parent, Object child) {
assert parent instanceof ThemeCategory;
assert child instanceof ThemeCategory;
List<ThemeCategory> list = myLabels.get(parent);
return list == null ? -1 : list.indexOf(child);
}
@Override
public void addTreeModelListener(TreeModelListener l) {
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
}
}
private static List<String> getSortedNames(Collection<ThemeEditorStyle> themesRaw) {
List<String> themes = new ArrayList<String>(themesRaw.size());
for (ThemeEditorStyle theme : themesRaw) {
themes.add(theme.getQualifiedName());
}
Collections.sort(themes);
return themes;
}
/**
* Filters a collection of themes to return a new collection with only the public ones.
*/
private static Collection<ThemeEditorStyle> getPublicThemes(Collection<ThemeEditorStyle> themes) {
HashSet<ThemeEditorStyle> publicThemes = new HashSet<ThemeEditorStyle>();
for (ThemeEditorStyle theme : themes) {
if (theme.isPublic()) {
publicThemes.add(theme);
}
}
return publicThemes;
}
private enum ThemeCategory {
ROOT(""),
RECENT("Recent"),
MANIFEST("Manifest Themes"),
PROJECT("Project Themes"),
MATERIAL_LIGHT("Material Light"),
MATERIAL("Material Dark"),
HOLO_LIGHT("Holo Light"),
HOLO("Holo Dark"),
CLASSIC("Classic"),
CLASSIC_LIGHT("Classic Light"),
DEVICE("Device Default"),
DIALOGS("Dialogs"),
LIGHT("Light"),
ALL("All");
// TODO: Add other logical types here, e.g. Wallpaper, Alert, etc?
ThemeCategory(String name) {
myName = name;
}
private final String myName;
@Override
public String toString() {
return myName;
}
}
public void focus() {
final Project project = myConfiguration.getModule().getProject();
final IdeFocusManager focusManager = project.isDefault() ? IdeFocusManager.getGlobalInstance() : IdeFocusManager.getInstance(project);
focusManager.doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
focusManager.requestFocus(myThemeList, true);
}
});
}
private static boolean haveMatches(String filter, List<String> themes) {
for (String theme : themes) {
if (matchesFilter(theme, filter)) {
return true;
}
}
return false;
}
private boolean haveAnyMatches(String filter) {
return haveMatches(filter, getFrameworkThemes()) || haveMatches(filter, getProjectThemes());
}
private void createUIComponents() {
myFilter = new ThemeFilterComponent("ANDROID_THEME_HISTORY", 10, true);
// Allow arrow up/down to navigate the filtered matches
myFilter.getTextEditor().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_UP) {
myThemeList.dispatchEvent(e);
e.consume();
}
}
});
}
private class ThemeFilterComponent extends FilterComponent {
private ThemeFilterComponent(@NonNls String propertyName, int historySize, boolean onTheFlyUpdate) {
super(propertyName, historySize, onTheFlyUpdate);
}
@Override
public void filter() {
String filter = getFilter();
assert filter != null;
if (myCategory != ThemeCategory.ALL && !haveMatches(filter, getThemes(myCategory)) && haveAnyMatches(filter)) {
// Switch to the All category
selectCategory(ThemeCategory.ALL, false);
}
updateThemeList();
}
@Override
protected void onEscape(KeyEvent e) {
focus();
e.consume();
}
}
}