diff options
5 files changed, 250 insertions, 70 deletions
diff --git a/android/guiTestSrc/com/android/tools/idea/tests/gui/theme/ThemeEditorTableTest.java b/android/guiTestSrc/com/android/tools/idea/tests/gui/theme/ThemeEditorTableTest.java index e24965dc055..ef9bae30ed1 100644 --- a/android/guiTestSrc/com/android/tools/idea/tests/gui/theme/ThemeEditorTableTest.java +++ b/android/guiTestSrc/com/android/tools/idea/tests/gui/theme/ThemeEditorTableTest.java @@ -209,12 +209,12 @@ public class ThemeEditorTableTest extends GuiTestCase { ThemeEditorFixture themeEditor = ThemeEditorTestUtils.openThemeEditor(projectFrame); ThemeEditorTableFixture themeEditorTable = themeEditor.getPropertiesTable(); - TableCell cell = row(3).column(0); + TableCell cell = row(1).column(0); Component colorRenderer = themeEditorTable.getRendererComponent(cell); assertNotNull(colorRenderer); ResourceComponentFixture resourceComponent = new ResourceComponentFixture(myRobot, (ResourceComponent)colorRenderer); - assertEquals("colorBackground", resourceComponent.getAttributeName()); + assertEquals("android:colorBackground", resourceComponent.getAttributeName()); assertEquals(Font.PLAIN, resourceComponent.getValueFont().getStyle()); assertEquals("@android:color/background_holo_light", resourceComponent.getValueString()); @@ -231,7 +231,7 @@ public class ThemeEditorTableTest extends GuiTestCase { assertNotNull(colorRenderer); resourceComponent = new ResourceComponentFixture(myRobot, (ResourceComponent)colorRenderer); - assertEquals("colorBackground", resourceComponent.getAttributeName()); + assertEquals("android:colorBackground", resourceComponent.getAttributeName()); assertEquals(ResourceHelper.colorToString(color), resourceComponent.getColorValue()); assertEquals(Font.BOLD, resourceComponent.getValueFont().getStyle()); assertEquals("@color/background_holo_light", resourceComponent.getValueString()); diff --git a/android/src/com/android/tools/idea/editors/theme/ThemeEditorComponent.java b/android/src/com/android/tools/idea/editors/theme/ThemeEditorComponent.java index ae60ae82e44..053c3a6a6a2 100644 --- a/android/src/com/android/tools/idea/editors/theme/ThemeEditorComponent.java +++ b/android/src/com/android/tools/idea/editors/theme/ThemeEditorComponent.java @@ -50,6 +50,7 @@ import com.android.tools.idea.rendering.ResourceNotificationManager.ResourceChan import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Ordering; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; import com.intellij.openapi.actionSystem.DefaultActionGroup; @@ -84,6 +85,8 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.RowFilter; +import javax.swing.RowSorter; +import javax.swing.SortOrder; import javax.swing.event.DocumentEvent; import javax.swing.plaf.PanelUI; import javax.swing.table.DefaultTableCellRenderer; @@ -99,6 +102,7 @@ import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -113,6 +117,28 @@ public class ThemeEditorComponent extends Splitter { private static final JBColor PREVIEW_BACKGROUND = new JBColor(new Color(0xFAFAFA), new Color(0x343739)); + /** + * Comparator used for simple mode attribute sorting + */ + private static final Comparator SIMPLE_MODE_COMPARATOR = new Comparator() { + @Override + public int compare(Object o1, Object o2) { + // The parent attribute goes always first + if (o1 instanceof String) { + return -1; + } else if (o2 instanceof String) { + return 1; + } + + if (o1 instanceof EditedStyleItem && o2 instanceof EditedStyleItem) { + return ((EditedStyleItem)o1).compareTo((EditedStyleItem)o2); + } + + // Fall-back for other comparisons + return Ordering.usingToString().compare(o1, o2); + } + }; + public static final float HEADER_FONT_SCALE = 1.3f; public static final int REGULAR_CELL_PADDING = 4; public static final int LARGE_CELL_PADDING = 10; @@ -499,10 +525,12 @@ public class ThemeEditorComponent extends Splitter { if (myPanel.isAdvancedMode()) { myAttributesFilter.setFilterEnabled(false); myAttributesSorter.setRowFilter(myAttributesFilter); + myAttributesSorter.setSortKeys(null); } else { mySimpleModeFilter.configure(myModel.getDefinedAttributes(), ThemeEditorUtils.isAppCompatTheme( myThemeEditorContext.getConfiguration())); myAttributesSorter.setRowFilter(mySimpleModeFilter); + myAttributesSorter.setSortKeys(ImmutableList.of(new RowSorter.SortKey(0, SortOrder.ASCENDING))); } } @@ -774,6 +802,9 @@ public class ThemeEditorComponent extends Splitter { myAttributesTable.setRowSorter(null); // Clean any previous row sorters. myAttributesSorter = new TableRowSorter<AttributesTableModel>(myModel); + // This is only used when the sort keys are set (only set in simple mode). + myAttributesSorter.setComparator(0, SIMPLE_MODE_COMPARATOR); + configureFilter(); myAttributesTable.setModel(myModel); diff --git a/android/src/com/android/tools/idea/editors/theme/attributes/AttributesGrouper.java b/android/src/com/android/tools/idea/editors/theme/attributes/AttributesGrouper.java index bcde2764173..7090222ab3a 100644 --- a/android/src/com/android/tools/idea/editors/theme/attributes/AttributesGrouper.java +++ b/android/src/com/android/tools/idea/editors/theme/attributes/AttributesGrouper.java @@ -16,9 +16,17 @@ package com.android.tools.idea.editors.theme.attributes; import com.android.tools.idea.editors.theme.datamodels.EditedStyleItem; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; public class AttributesGrouper { private AttributesGrouper() { } @@ -28,7 +36,7 @@ public class AttributesGrouper { TYPE("By Type"); final private String myText; - + GroupBy(String text) { myText = text; } @@ -43,7 +51,13 @@ public class AttributesGrouper { * Helper data structure to hold information for (temporary) algorithm for splitting attributes * to labelled group. */ - private static class Group { + private enum Group { + STYLES("Styles", ImmutableList.of("style", "theme")), + COLORS("Colors", ImmutableList.of("color")), + DRAWABLES("Drawables", ImmutableList.of("drawable")), + METRICS("Metrics", ImmutableList.of("size", "width", "height")), + OTHER("Everything Else", Collections.<String>emptyList()); + /** * Group name, as appears on properties panel */ @@ -54,88 +68,61 @@ public class AttributesGrouper { */ public final List<String> markers; - public Group(String name, List<String> markers) { + Group(@NotNull String name, @NotNull List<String> markers) { this.name = name; this.markers = markers; } - public static Group of(String name, String... markers) { - return new Group(name, Arrays.asList(markers)); + private static Group getGroupFromName(String name) { + for (Group group : Group.values()) { + for (final String marker : group.markers) { + if (StringUtil.containsIgnoreCase(name, marker)) { + return group; + } + } + } + return OTHER; } } - private static final List<Group> GROUPS = Arrays.asList( - Group.of("Styles", "style", "theme"), - Group.of("Colors", "color"), - Group.of("Drawables", "drawable"), - Group.of("Metrics", "size", "width", "height") - ); - @SuppressWarnings("unchecked") - public static List<TableLabel> generateLabelsForType(final List<EditedStyleItem> source, final List<EditedStyleItem> sink) { + @NotNull + private static List<TableLabel> generateLabelsForType(@NotNull final List<EditedStyleItem> source, @NotNull final List<EditedStyleItem> sink) { + final Multimap<Group, EditedStyleItem> classes = HashMultimap.create(); - final List<EditedStyleItem>[] classes = new List[GROUPS.size() + 1]; - final int otherGroupIndex = GROUPS.size(); - - for (int i = 0; i < classes.length; i++) { - classes[i] = new ArrayList<EditedStyleItem>(); - } - - outer: for (final EditedStyleItem item : source) { final String name = item.getName(); - - for (int index = 0; index < GROUPS.size(); index++) { - final Group group = GROUPS.get(index); - for (final String marker : group.markers) { - if (StringUtil.containsIgnoreCase(name, marker)) { - classes[index].add(item); - continue outer; - } - } - } - - // haven't found any group, will put the item into "Other" - classes[otherGroupIndex].add(item); + classes.put(Group.getGroupFromName(name), item); } final List<TableLabel> labels = new ArrayList<TableLabel>(); int offset = 0; - for (int index = 0; index < GROUPS.size(); index++) { - final Group group = GROUPS.get(index); - final int size = classes[index].size(); - - if (size != 0) { + for (Group group : Group.values()) { + Collection<EditedStyleItem> elements = classes.get(group); + + boolean addHeader = !elements.isEmpty(); + if (addHeader && group == Group.OTHER) { + // Adding "Everything else" label only in case when there are at least one other label, + // because having "Everything else" as the only label present looks quite silly + addHeader = offset != 0; + } + if (addHeader) { labels.add(new TableLabel(group.name, offset)); } - offset += size; - } - - final int otherGroupSize = classes[otherGroupIndex].size(); - // Adding "Everything else" label only in case when there are at least one other label, - // because having "Everything else" as the only label present looks quite silly - if (otherGroupSize != 0 && labels.size() > 0) { - labels.add(new TableLabel("Everything Else", offset)); - } + sink.addAll(elements); - for (final List<EditedStyleItem> list : classes) { - for (final EditedStyleItem item : list) { - sink.add(item); - } + offset += elements.size(); } return labels; } - private static List<TableLabel> generateLabelsForGroup(final List<EditedStyleItem> source, final List<EditedStyleItem> sink) { - Map<String, List<EditedStyleItem>> classes = new TreeMap<String, List<EditedStyleItem>>(); + static List<TableLabel> generateLabelsForGroup(final List<EditedStyleItem> source, final List<EditedStyleItem> sink) { + TreeMultimap<String, EditedStyleItem> classes = TreeMultimap.create(); for (EditedStyleItem item : source){ String group = item.getAttrGroup(); - if (!classes.containsKey(group)) { - classes.put(group, new ArrayList<EditedStyleItem>()); - } - classes.get(group).add(item); + classes.put(group, item); } final List<TableLabel> labels = new ArrayList<TableLabel>(); @@ -152,13 +139,16 @@ public class AttributesGrouper { return labels; } - public static List<TableLabel> generateLabels(GroupBy group, final List<EditedStyleItem> source, final List<EditedStyleItem> sink) { - if (group == GroupBy.TYPE) { - return generateLabelsForType(source, sink); - } - else if (group == GroupBy.GROUP) { - return generateLabelsForGroup(source, sink); + @NotNull + public static List<TableLabel> generateLabels(@NotNull GroupBy group, final List<EditedStyleItem> source, final List<EditedStyleItem> sink) { + switch(group) { + case TYPE: + return generateLabelsForType(source, sink); + case GROUP: + return generateLabelsForGroup(source, sink); + + default: + throw new IllegalArgumentException(); } - return null; } } diff --git a/android/src/com/android/tools/idea/editors/theme/datamodels/EditedStyleItem.java b/android/src/com/android/tools/idea/editors/theme/datamodels/EditedStyleItem.java index be7d89ea8d1..2ae49c99b87 100644 --- a/android/src/com/android/tools/idea/editors/theme/datamodels/EditedStyleItem.java +++ b/android/src/com/android/tools/idea/editors/theme/datamodels/EditedStyleItem.java @@ -40,7 +40,7 @@ import java.util.Collections; * <p/> * If the attribute is declared locally in multiple resource folders, this class also contains the alternative values for the attribute. */ -public class EditedStyleItem { +public class EditedStyleItem implements Comparable<EditedStyleItem> { private final static Logger LOG = Logger.getInstance(EditedStyleItem.class); private final static String DEPRECATED = "deprecated"; @@ -187,4 +187,9 @@ public class EditedStyleItem { return androidTargetData.isResourcePublic(ResourceType.ATTR.getName(), getName()); } + + @Override + public int compareTo(EditedStyleItem that) { + return getName().compareTo(that.getName()); + } } diff --git a/android/testSrc/com/android/tools/idea/editors/theme/attributes/AttributesGrouperTest.java b/android/testSrc/com/android/tools/idea/editors/theme/attributes/AttributesGrouperTest.java new file mode 100644 index 00000000000..d1c3eaff255 --- /dev/null +++ b/android/testSrc/com/android/tools/idea/editors/theme/attributes/AttributesGrouperTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2015 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.attributes; + +import com.android.tools.idea.editors.theme.datamodels.EditedStyleItem; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Index.atIndex; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AttributesGrouperTest { + /** + * Creates a list of {@link EditedStyleItem} mocks from the given list of pairs (attribute name, attribute group) + */ + @NotNull + private static List<EditedStyleItem> fromList(String... args) { + assert args.length % 2 == 0; + + ImmutableList.Builder<EditedStyleItem> builder = ImmutableList.builder(); + for (int i = 0; i < args.length; i += 2) { + String attributeName = args[i]; + String attributeGroup = args[i + 1]; + + EditedStyleItem styleItem = mock(EditedStyleItem.class); + when(styleItem.getName()).thenReturn(attributeName); + when(styleItem.getAttrGroup()).thenReturn(attributeGroup); + builder.add(styleItem); + } + + return builder.build(); + } + + /** + * Extracts the table label strings from the list of {@link TableLabel} objects + */ + @NotNull + private static List<String> getTableNames(@NotNull List<TableLabel> tableLabels) { + return Lists.transform(tableLabels, new Function<TableLabel, String>() { + @Nullable + @Override + public String apply(@Nullable TableLabel input) { + assertNotNull(input); + return input.getLabelName(); + } + }); + } + + @Test + public void testEmptySource() { + ArrayList<EditedStyleItem> sink = Lists.newArrayList(); + List<TableLabel> tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.GROUP, Collections.<EditedStyleItem>emptyList(), sink); + assertThat(sink).isEmpty(); + assertThat(tableLabels).isEmpty(); + + tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.TYPE, Collections.<EditedStyleItem>emptyList(), sink); + assertThat(sink).isEmpty(); + assertThat(tableLabels).isEmpty(); + } + + @Test + public void testGenerateLabels() { + ArrayList<EditedStyleItem> sink = Lists.newArrayList(); + List<EditedStyleItem> source = fromList( + "colorItem", "Colors", + "itemColor", "Colors", + "theDrawableThing", "Drawables", + "drawableStyle", "Styles", + "otherThing", "Styles" + ); + + // Group by Attribute Group + List<TableLabel> tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.GROUP, source, sink); + assertThat(tableLabels).hasSize(3); + // Labels must be alphabetically sorted + assertThat(getTableNames(tableLabels)) + .contains("Colors", atIndex(0)) + .contains("Drawables", atIndex(1)) + .contains("Styles", atIndex(2)); + assertThat(tableLabels.get(0).getRowPosition()).isEqualTo(0); + assertThat(tableLabels.get(1).getRowPosition()).isEqualTo(2); // Only 1 drawable + assertThat(tableLabels.get(2).getRowPosition()).isEqualTo(3); + sink.clear(); + + // Group by Type + tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.TYPE, source, sink); + assertThat(tableLabels).hasSize(4); + // Labels are sorted with an arbitrary order + assertThat(getTableNames(tableLabels)) + .contains("Styles", atIndex(0)) + .contains("Colors", atIndex(1)) + .contains("Drawables", atIndex(2)) + .contains("Everything Else", atIndex(3)); + assertThat(tableLabels.get(0).getRowPosition()).isEqualTo(0); + assertThat(tableLabels.get(1).getRowPosition()).isEqualTo(1); + assertThat(tableLabels.get(2).getRowPosition()).isEqualTo(3); + assertThat(tableLabels.get(3).getRowPosition()).isEqualTo(4); + } + + @Test + public void testEverythingElse() { + ArrayList<EditedStyleItem> sink = Lists.newArrayList(); + List<EditedStyleItem> source = fromList( + "item1", "Colors", + "item2", "Colors", + "item3", "Drawables", + "otherThing", "Styles" + ); + + List<TableLabel> tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.TYPE, source, sink); + // Everything is in the "Everything Else" category so we just do not have headers + assertThat(tableLabels).isEmpty(); + assertThat(sink).hasSize(4); + sink.clear(); + + source = fromList( + "itemColor1", "Colors", + "item2", "Colors", + "item3", "Drawables", + "otherThing", "Styles" + ); + tableLabels = AttributesGrouper.generateLabels(AttributesGrouper.GroupBy.TYPE, source, sink); + // Now we have one color so two labels should be generated + assertThat(tableLabels).hasSize(2); + assertThat(getTableNames(tableLabels)) + .contains("Colors", atIndex(0)) + .contains("Everything Else", atIndex(1)); + assertThat(sink).hasSize(4); + } +}
\ No newline at end of file |