| /* |
| * 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; |
| |
| import com.android.SdkConstants; |
| import com.android.ide.common.rendering.api.ItemResourceValue; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.tools.idea.configurations.Configuration; |
| import com.android.tools.idea.configurations.ConfigurationListener; |
| import com.android.tools.idea.configurations.DeviceMenuAction; |
| import com.android.tools.idea.configurations.ThemeSelectionDialog; |
| import com.android.tools.idea.editors.theme.attributes.*; |
| import com.android.tools.idea.editors.theme.attributes.editors.*; |
| import com.android.tools.idea.editors.theme.datamodels.EditedStyleItem; |
| import com.android.tools.idea.editors.theme.datamodels.ThemeEditorStyle; |
| import com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor; |
| import com.android.tools.idea.gradle.project.GradleBuildListener; |
| import com.android.tools.idea.gradle.util.BuildMode; |
| import com.android.tools.idea.rendering.RenderLogger; |
| import com.android.tools.idea.rendering.RenderService; |
| import com.android.tools.idea.rendering.RenderTask; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.ActionToolbar; |
| import com.intellij.openapi.actionSystem.DefaultActionGroup; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.Splitter; |
| import com.intellij.util.Processor; |
| import com.intellij.util.messages.MessageBusConnection; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.android.dom.drawable.DrawableDomElement; |
| import org.jetbrains.android.dom.resources.Flag; |
| import org.jetbrains.android.dom.resources.ResourceElement; |
| import org.jetbrains.android.dom.resources.Style; |
| import org.jetbrains.android.dom.resources.StyleItem; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.util.AndroidResourceUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.event.TableModelEvent; |
| import javax.swing.event.TableModelListener; |
| import javax.swing.plaf.PanelUI; |
| import javax.swing.table.DefaultTableCellRenderer; |
| import javax.swing.table.TableRowSorter; |
| import java.awt.*; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.event.ItemEvent; |
| import java.awt.event.ItemListener; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| |
| public class ThemeEditorComponent extends Splitter { |
| private static final Logger LOG = Logger.getInstance(ThemeEditorComponent.class); |
| |
| 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; |
| |
| private Font myHeaderFont; |
| |
| private StyleResolver myStyleResolver; |
| private String myPreviousSelectedTheme; |
| |
| // Points to the current selected substyle within the theme. |
| private ThemeEditorStyle myCurrentSubStyle; |
| |
| // Points to the attribute that original pointed to the substyle. |
| private EditedStyleItem mySubStyleSourceAttribute; |
| |
| // Subcomponents |
| private final Configuration myConfiguration; |
| private final Module myModule; |
| private final AndroidThemePreviewPanel myPreviewPanel; |
| private final StyleAttributesFilter myAttributesFilter; |
| private final AttributesPanel myPanel = new AttributesPanel(); |
| private final ThemeEditorTable myAttributesTable = myPanel.getAttributesTable(); |
| |
| private final AttributeReferenceRendererEditor myStyleEditor; |
| private final AttributeReferenceRendererEditor.ClickListener myClickListener; |
| private final ConfigurationListener myConfigListener = new ConfigurationListener() { |
| @Override |
| public boolean changed(int flags) { |
| |
| //reloads the theme editor preview when device is modified |
| if ((flags & CFG_DEVICE) != 0) { |
| loadStyleAttributes(); |
| myConfiguration.save(); |
| } |
| |
| return true; |
| } |
| }; |
| private ThemeEditorStyle mySelectedTheme; |
| private MessageBusConnection myMessageBusConnection; |
| |
| public ThemeEditorComponent(final Configuration configuration, final Module module) { |
| this.myConfiguration = configuration; |
| this.myModule = module; |
| |
| myConfiguration.addListener(myConfigListener); |
| myStyleResolver = new StyleResolver(myConfiguration); |
| |
| myPreviewPanel = new AndroidThemePreviewPanel(myConfiguration); |
| myPreviewPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); |
| |
| // Setup Javadoc handler. |
| ActionManager actionManager = ActionManager.getInstance(); |
| ShowJavadocAction showJavadoc = new ShowJavadocAction(myAttributesTable, myModule, myConfiguration); |
| showJavadoc.registerCustomShortcutSet(actionManager.getAction(IdeActions.ACTION_QUICK_JAVADOC).getShortcutSet(), myAttributesTable); |
| |
| Project project = myModule.getProject(); |
| ResourcesCompletionProvider completionProvider = new ResourcesCompletionProvider(myConfiguration.getResourceResolver()); |
| myClickListener = new AttributeReferenceRendererEditor.ClickListener() { |
| @Override |
| public void clicked(@NotNull EditedStyleItem value) { |
| if (value.isAttr() && getSelectedStyle() != null && myConfiguration.getResourceResolver() != null) { |
| // We need to resolve the theme attribute. |
| // TODO: Do we need a full resolution or can we just try to get it from the StyleWrapper? |
| ItemResourceValue resourceValue = (ItemResourceValue)myConfiguration.getResourceResolver().findResValue(value.getValue(), false); |
| if (resourceValue == null) { |
| LOG.error("Unable to resolve " + value.getValue()); |
| return; |
| } |
| |
| EditedStyleItem editedStyleItem = new EditedStyleItem(resourceValue, getSelectedStyle()); |
| |
| assert editedStyleItem.getValue() != null; |
| myCurrentSubStyle = myStyleResolver.getStyle(editedStyleItem.getValue()); |
| } |
| else { |
| if (value.getValue() == null) { |
| LOG.error("null value for " + value.getName()); |
| return; |
| } |
| |
| myCurrentSubStyle = myStyleResolver.getStyle(value.getValue()); |
| } |
| mySubStyleSourceAttribute = value; |
| loadStyleAttributes(); |
| } |
| }; |
| myStyleEditor = new AttributeReferenceRendererEditor(project, completionProvider); |
| |
| final AndroidFacet facet = AndroidFacet.getInstance(module); |
| RenderTask renderTask = null; |
| if (facet != null) { |
| final RenderService service = RenderService.get(facet); |
| renderTask = service.createTask(null, configuration, new RenderLogger("ThemeEditorLogger", module), null); |
| } |
| |
| // Get rid of default white background of the table. |
| myAttributesTable.setBackground(null); |
| |
| myAttributesTable.setDefaultRenderer(Color.class, new DelegatingCellRenderer(new ColorRenderer(myConfiguration))); |
| myAttributesTable.setDefaultRenderer(EditedStyleItem.class, new DelegatingCellRenderer(new AttributeReferenceRendererEditor(project, completionProvider))); |
| myAttributesTable.setDefaultRenderer(ThemeEditorStyle.class, new DelegatingCellRenderer(new AttributeReferenceRendererEditor(project, completionProvider))); |
| myAttributesTable.setDefaultRenderer(String.class, new DelegatingCellRenderer(myAttributesTable.getDefaultRenderer(String.class))); |
| myAttributesTable.setDefaultRenderer(Integer.class, new DelegatingCellRenderer(new IntegerRenderer())); |
| myAttributesTable.setDefaultRenderer(Boolean.class, new DelegatingCellRenderer(new BooleanRendererEditor(myModule))); |
| myAttributesTable.setDefaultRenderer(Enum.class, new DelegatingCellRenderer(new EnumRendererEditor())); |
| myAttributesTable.setDefaultRenderer(Flag.class, new DelegatingCellRenderer(new FlagRendererEditor())); |
| myAttributesTable.setDefaultRenderer(AttributesTableModel.ParentAttribute.class, new DelegatingCellRenderer(new ParentRendererEditor(myConfiguration))); |
| myAttributesTable.setDefaultRenderer(DrawableDomElement.class, new DelegatingCellRenderer(new DrawableRenderer(myAttributesTable, renderTask))); |
| myAttributesTable.setDefaultRenderer(TableLabel.class, new DefaultTableCellRenderer() { |
| @Override |
| public Component getTableCellRendererComponent(JTable table, |
| Object value, |
| boolean isSelected, |
| boolean hasFocus, |
| int row, |
| int column) { |
| super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); |
| this.setFont(myHeaderFont); |
| return this; |
| } |
| }); |
| |
| myAttributesTable.setDefaultEditor(Color.class, new DelegatingCellEditor(false, new ColorEditor(myModule, myConfiguration, myPreviewPanel), module, configuration)); |
| myAttributesTable.setDefaultEditor(EditedStyleItem.class, new DelegatingCellEditor(false, new AttributeReferenceRendererEditor(project, completionProvider), module, configuration)); |
| myAttributesTable.setDefaultEditor(String.class, new DelegatingCellEditor(false, myAttributesTable.getDefaultEditor(String.class), module, configuration)); |
| myAttributesTable.setDefaultEditor(Integer.class, new DelegatingCellEditor(myAttributesTable.getDefaultEditor(Integer.class), module, configuration)); |
| myAttributesTable.setDefaultEditor(Boolean.class, new DelegatingCellEditor(false, new BooleanRendererEditor(myModule), module, configuration)); |
| myAttributesTable.setDefaultEditor(Enum.class, new DelegatingCellEditor(false, new EnumRendererEditor(), module, configuration)); |
| myAttributesTable.setDefaultEditor(Flag.class, new DelegatingCellEditor(false, new FlagRendererEditor(), module, configuration)); |
| myAttributesTable.setDefaultEditor(AttributesTableModel.ParentAttribute.class, new DelegatingCellEditor(false, new ParentRendererEditor(myConfiguration), module, configuration)); |
| |
| // We allow to edit style pointers as Strings. |
| myAttributesTable.setDefaultEditor(ThemeEditorStyle.class, new DelegatingCellEditor(false, myStyleEditor, module, configuration)); |
| myAttributesTable.setDefaultEditor(DrawableDomElement.class, new DelegatingCellEditor(false, new DrawableEditor(myModule, myAttributesTable, renderTask), module, configuration)); |
| updateUiParameters(); |
| |
| myAttributesFilter = new StyleAttributesFilter(); |
| |
| myPanel.getBackButton().addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| myCurrentSubStyle = null; |
| loadStyleAttributes(); |
| } |
| }); |
| |
| myPanel.getAdvancedFilterCheckBox().addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| if (myAttributesTable.isEditing()) { |
| myAttributesTable.getCellEditor().cancelCellEditing(); |
| } |
| |
| myAttributesTable.clearSelection(); |
| myPanel.getPalette().clearSelection(); |
| myAttributesFilter.setFilterEnabled(!myPanel.isAdvancedMode()); |
| |
| myAttributesFilter.setAttributesFilter(myAttributesFilter.ATTRIBUTES_DEFAULT_FILTER); |
| |
| ((TableRowSorter)myAttributesTable.getRowSorter()).sort(); |
| myAttributesTable.updateRowHeights(); |
| } |
| }); |
| |
| // We have our own custom renderer that it's not based on the default one. |
| //noinspection GtkPreferredJComboBoxRenderer |
| myPanel.getThemeCombo().setRenderer(new StyleListCellRenderer(facet)); |
| myPanel.getThemeCombo().addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| if (myPanel.isCreateNewThemeSelected()) { |
| if (!createNewTheme()) { |
| // User clicked "cancel", restore previously selected item in themes combo. |
| myPanel.getThemeCombo().getModel().setSelectedItem(mySelectedTheme); |
| } |
| return; |
| } |
| if (myPanel.isShowAllThemesSelected()) { |
| if (!selectNewTheme()) { |
| myPanel.getThemeCombo().getModel().setSelectedItem(mySelectedTheme); |
| } |
| return; |
| } |
| mySelectedTheme = myPanel.getSelectedTheme(); |
| saveCurrentSelectedTheme(); |
| myCurrentSubStyle = null; |
| mySubStyleSourceAttribute = null; |
| |
| loadStyleAttributes(); |
| } |
| }); |
| |
| myPanel.getAttrGroupCombo().setModel(new DefaultComboBoxModel(AttributesGrouper.GroupBy.values())); |
| myPanel.getAttrGroupCombo().addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| loadStyleAttributes(); |
| } |
| }); |
| |
| // Adds the Device selection button |
| DefaultActionGroup group = new DefaultActionGroup(); |
| DeviceMenuAction deviceAction = new DeviceMenuAction(myPreviewPanel); |
| group.add(deviceAction); |
| ActionToolbar actionToolbar = actionManager.createActionToolbar("ThemeToolbar", group, true); |
| actionToolbar.setLayoutPolicy(ActionToolbar.WRAP_LAYOUT_POLICY); |
| JPanel myConfigToolbar = myPanel.getConfigToolbar(); |
| myConfigToolbar.add(actionToolbar.getComponent()); |
| |
| final JScrollPane scroll = myPanel.getAttributesScrollPane(); |
| scroll.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); // the scroll pane should fill all available space |
| |
| setFirstComponent(myPreviewPanel); |
| setSecondComponent(myPanel.getRightPanel()); |
| setShowDividerControls(false); |
| |
| myMessageBusConnection = project.getMessageBus().connect(project); |
| myMessageBusConnection.subscribe(PostProjectBuildTasksExecutor.GRADLE_BUILD_TOPIC, new GradleBuildListener() { |
| @Override |
| public void buildFinished(@NotNull Project project, @Nullable BuildMode mode) { |
| if (project != myModule.getProject()) { |
| return; |
| } |
| |
| // Classes probably have changed so reload the custom components and support library classes. |
| myPreviewPanel.reloadComponents(); |
| myPreviewPanel.revalidate(); |
| myPreviewPanel.repaint(); |
| } |
| }); |
| } |
| |
| /** |
| * Launches dialog to create a new theme based on selected one. |
| * @return whether creation of new theme succeeded. |
| */ |
| private boolean createNewTheme() { |
| ThemeEditorStyle selectedTheme = getSelectedStyle(); |
| String selectedThemeName = selectedTheme == null ? null : selectedTheme.getName(); |
| |
| String newThemeName = createNewStyle(selectedThemeName, null/*message*/, null/*newAttributeName*/, null/*newAttributeValue*/); |
| if (newThemeName != null) { |
| reload(newThemeName); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Launches dialog to choose a theme among all existing ones |
| * @return whether the choice is valid |
| */ |
| private boolean selectNewTheme() { |
| ThemeSelectionDialog dialog = new ThemeSelectionDialog(myConfiguration); |
| if (dialog.showAndGet()) { |
| String newThemeName = dialog.getTheme(); |
| if (newThemeName != null) { |
| reload(newThemeName); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public void goToParent() { |
| ThemeEditorStyle selectedStyle = getSelectedStyle(); |
| if (selectedStyle == null) { |
| LOG.error("No style selected."); |
| return; |
| } |
| |
| ThemeEditorStyle parent = getSelectedStyle().getParent(); |
| assert parent != null; |
| |
| // TODO: This seems like it could be confusing for users, we might want to differentiate parent navigation depending if it's |
| // substyle or theme navigation. |
| if (isSubStyleSelected()) { |
| myCurrentSubStyle = parent; |
| loadStyleAttributes(); |
| } |
| else { |
| myPanel.setSelectedTheme(parent); |
| } |
| } |
| |
| /** |
| * Creates a new theme by displaying the {@link NewStyleDialog}. If newAttributeName is not null, a new attribute will be added to the |
| * style with the value specified in newAttributeValue. |
| * An optional message can be displayed as hint to the user of why the theme is being created. |
| * @return the new style name or null if the style wasn't created. |
| */ |
| @Nullable |
| private String createNewStyle(@Nullable String defaultParentStyleName, |
| @Nullable String message, |
| @Nullable final String newAttributeName, |
| @Nullable final String newAttributeValue) { |
| final NewStyleDialog dialog = new NewStyleDialog(!isSubStyleSelected() /*isTheme*/, |
| myConfiguration, |
| defaultParentStyleName, |
| getSelectedTheme() != null ? getSelectedTheme().getSimpleName() : null, |
| message); |
| boolean createStyle = dialog.showAndGet(); |
| if (!createStyle) { |
| return null; |
| } |
| |
| final String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STYLE); |
| final List<String> dirNames = Collections.singletonList(ResourceFolderType.VALUES.getName()); |
| |
| if (fileName == null) { |
| LOG.error("Couldn't find a default filename for ResourceType.STYLE"); |
| return null; |
| } |
| boolean isCreated = AndroidResourceUtil |
| .createValueResource(myModule, dialog.getStyleName(), ResourceType.STYLE, fileName, dirNames, new Processor<ResourceElement>() { |
| @Override |
| public boolean process(ResourceElement element) { |
| assert element instanceof Style; |
| final Style style = (Style)element; |
| |
| style.getParentStyle().setStringValue(dialog.getStyleParentName()); |
| |
| if (!Strings.isNullOrEmpty(newAttributeName)) { |
| StyleItem newItem = style.addItem(); |
| newItem.getName().setStringValue(newAttributeName); |
| |
| if (!Strings.isNullOrEmpty(newAttributeValue)) { |
| newItem.setStringValue(newAttributeValue); |
| } |
| } |
| |
| return true; |
| } |
| }); |
| |
| if (isCreated) { |
| AndroidFacet facet = AndroidFacet.getInstance(myModule); |
| if (facet != null) { |
| facet.refreshResources(); |
| } |
| } |
| |
| return isCreated ? SdkConstants.STYLE_RESOURCE_PREFIX + dialog.getStyleName() : null; |
| } |
| |
| /** |
| * Save the current selected theme so we can restore it if we need to refresh the data. |
| * If the theme does not exist anymore, the first available theme will be selected. |
| */ |
| private void saveCurrentSelectedTheme() { |
| ThemeEditorStyle selectedTheme = getSelectedStyle(); |
| myPreviousSelectedTheme = selectedTheme == null ? null : selectedTheme.getName(); |
| } |
| |
| @Nullable |
| public String getPreviousSelectedTheme() { |
| return myPreviousSelectedTheme; |
| } |
| |
| @Nullable |
| ThemeEditorStyle getSelectedTheme() { |
| return mySelectedTheme; |
| } |
| |
| //Never null, because DefaultComboBoxModel and fixed list of items rendered |
| @NotNull |
| private AttributesGrouper.GroupBy getSelectedAttrGroup() { |
| return (AttributesGrouper.GroupBy)myPanel.getAttrGroupCombo().getSelectedItem(); |
| } |
| |
| @Nullable |
| private ThemeEditorStyle getSelectedStyle() { |
| if (myCurrentSubStyle != null) { |
| return myCurrentSubStyle; |
| } |
| |
| return getSelectedTheme(); |
| } |
| |
| @Nullable |
| ThemeEditorStyle getCurrentSubStyle() { |
| return myCurrentSubStyle; |
| } |
| |
| private boolean isSubStyleSelected() { |
| return myCurrentSubStyle != null; |
| } |
| |
| /** |
| * Sets a new value to the passed attribute. It will also trigger the reload if a change happened. |
| * @param rv The attribute to set, including the current value. |
| * @param strValue The new value. |
| */ |
| private void createNewThemeWithAttributeValue(@NotNull EditedStyleItem rv, @NotNull String strValue) { |
| if (strValue.equals(rv.getRawXmlValue())) { |
| // No modification required. |
| return; |
| } |
| |
| ThemeEditorStyle selectedStyle = getSelectedStyle(); |
| if (selectedStyle == null) { |
| LOG.error("No style/theme selected."); |
| return; |
| } |
| |
| // The current style is R/O so we need to propagate this change a new style. |
| String newStyleName = createNewStyle(selectedStyle.getName(), String |
| .format("<html>The %1$s '<code>%2$s</code>' is Read-Only.<br/>A new %1$s will be created to modify '<code>%3$s</code>'.<br/></html>", |
| isSubStyleSelected() ? "style" : "theme", |
| selectedStyle.getName(), |
| rv.getName()), rv.getQualifiedName(), strValue); |
| |
| if (newStyleName == null) { |
| return; |
| } |
| |
| if (!isSubStyleSelected()) { |
| // We changed a theme, so we are done. |
| reload(newStyleName); |
| |
| return; |
| } |
| |
| ThemeEditorStyle selectedTheme = getSelectedTheme(); |
| if (selectedTheme == null) { |
| LOG.error("No theme selected."); |
| return; |
| } |
| |
| // Decide what property we need to modify. |
| // If the modified style was pointed by a theme attribute, we need to use that theme attribute value |
| // as property. Otherwise, just update the original property name with the new style. |
| String sourcePropertyName = mySubStyleSourceAttribute.isAttr() ? |
| mySubStyleSourceAttribute.getAttrPropertyName(): |
| mySubStyleSourceAttribute.getQualifiedName(); |
| |
| // We've modified a sub-style so we need to modify the attribute that was originally pointing to this. |
| if (selectedTheme.isReadOnly()) { |
| // The theme pointing to the new style is r/o so create a new theme and then write the value. |
| String newThemeName = createNewStyle(selectedTheme.getName(), String.format( |
| "<html>The style '%1$s' which references to '%2$s' is also Read-Only.<br/>" + |
| "A new theme will be created to point to the modified style '%3$s'.<br/></html>", selectedTheme.getName(), rv.getName(), |
| newStyleName), sourcePropertyName, newStyleName); |
| |
| if (newThemeName != null) { |
| reload(newThemeName); |
| } |
| } else { |
| // The theme pointing to the new style is writable, so go ahead. |
| selectedTheme.setValue(sourcePropertyName, newStyleName); |
| reload(selectedTheme.getName()); |
| } |
| } |
| |
| /** |
| * Reloads the attributes editor. |
| * @param defaultThemeName The name to select from the themes list. |
| */ |
| public void reload(@Nullable final String defaultThemeName) { |
| reload(defaultThemeName, null); |
| } |
| |
| public void reload(@Nullable final String defaultThemeName, @Nullable final String defaultSubStyleName) { |
| |
| // We ran this with invokeLater to allow any PSI rescans to run and update the modification count. |
| // If we don't use invokeLater, it will still work with the previous cached PSI file value. |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| // This is required since the configuration could have a link to a non existent theme (if it was removed). |
| // If the configuration is pointing to a theme that does not exist anymore, the local resource resolution breaks so ThemeResolver |
| // fails to find the local themes. |
| myConfiguration.setTheme(null); |
| |
| myStyleResolver = new StyleResolver(myConfiguration); |
| myCurrentSubStyle = defaultSubStyleName == null ? null : myStyleResolver.getStyle(defaultSubStyleName); |
| mySubStyleSourceAttribute = null; |
| |
| final ThemeResolver themeResolver = new ThemeResolver(myConfiguration, myStyleResolver); |
| myPanel.getThemeCombo().setModel(new ThemesListModel(themeResolver, defaultThemeName)); |
| loadStyleAttributes(); |
| mySelectedTheme = myPanel.getSelectedTheme(); |
| saveCurrentSelectedTheme(); |
| } |
| }); |
| } |
| |
| /** |
| * Loads the theme attributes table for the current selected theme or substyle. |
| */ |
| private void loadStyleAttributes() { |
| mySelectedTheme = myPanel.getSelectedTheme(); |
| final ThemeEditorStyle selectedTheme = getSelectedTheme(); |
| final ThemeEditorStyle selectedStyle = getSelectedStyle(); |
| |
| if (selectedTheme == null || selectedStyle == null) { |
| LOG.error("No style/theme selected"); |
| return; |
| } |
| |
| myPanel.setSubstyleName(myCurrentSubStyle == null ? null : myCurrentSubStyle.getName()); |
| |
| myPanel.getBackButton().setVisible(myCurrentSubStyle != null); |
| myPanel.getPaletteScrollPane().setVisible(myCurrentSubStyle == null); |
| myConfiguration.setTheme(selectedTheme.getName()); |
| |
| assert myConfiguration.getResourceResolver() != null; // ResourceResolver is only null if no theme was set. |
| final AttributesTableModel model = new AttributesTableModel(selectedStyle, getSelectedAttrGroup(), myConfiguration, myModule.getProject()); |
| model.setGoToDefinitionListener(myClickListener); |
| |
| model.addThemePropertyChangedListener(new AttributesTableModel.ThemePropertyChangedListener() { |
| @Override |
| public void attributeChangedOnReadOnlyTheme(final EditedStyleItem attribute, final String newValue) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| createNewThemeWithAttributeValue(attribute, newValue); |
| } |
| }); |
| } |
| }); |
| |
| model.addTableModelListener(new TableModelListener() { |
| @Override |
| public void tableChanged(TableModelEvent e) { |
| if (e.getType() == TableModelEvent.UPDATE) { |
| |
| if (e.getLastRow() == 0) { // Indicates a change in the theme name |
| reload(model.getThemeNameInXml()); |
| } |
| else if (e.getLastRow() == TableModelEvent.HEADER_ROW) { // Indicates a change of the model |
| myAttributesTable.updateRowHeights(); |
| } |
| } |
| |
| if (myPreviewPanel != null) { |
| myPreviewPanel.updateConfiguration(myConfiguration); |
| myPreviewPanel.revalidate(); |
| myPreviewPanel.repaint(); |
| } |
| myAttributesTable.repaint(); |
| } |
| }); |
| |
| myAttributesTable.setRowSorter(null); // Clean any previous row sorters. |
| TableRowSorter<AttributesTableModel> sorter = new TableRowSorter<AttributesTableModel>(model); |
| sorter.setRowFilter(myAttributesFilter); |
| myPanel.setAdvancedMode(!myAttributesFilter.myIsFilterEnabled); |
| |
| ActionListener listener = new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| goToParent(); |
| } |
| }; |
| |
| myAttributesTable.setModel(model); |
| myAttributesTable.setRowSorter(sorter); |
| myAttributesTable.updateRowHeights(); |
| model.parentAttribute.setGotoDefinitionCallback(listener); |
| |
| myPanel.getPalette().setModel(new AttributesModelColorPaletteModel(myConfiguration, model)); |
| myPanel.getPalette().addItemListener(new ItemListener() { |
| @Override |
| public void itemStateChanged(ItemEvent e) { |
| if (e.getStateChange() == ItemEvent.SELECTED) { |
| AttributesModelColorPaletteModel model = (AttributesModelColorPaletteModel)myPanel.getPalette().getModel(); |
| List<EditedStyleItem> references = model.getReferences((Color)e.getItem()); |
| if (references.isEmpty()) { |
| return; |
| } |
| |
| HashSet<String> attributeNames = new HashSet<String>(references.size()); |
| for(EditedStyleItem item : references) { |
| attributeNames.add(item.getQualifiedName()); |
| } |
| myAttributesFilter.setAttributesFilter(attributeNames); |
| myAttributesFilter.setFilterEnabled(true); |
| } else { |
| myAttributesFilter.setFilterEnabled(false); |
| } |
| |
| if (myAttributesTable.isEditing()) { |
| myAttributesTable.getCellEditor().cancelCellEditing(); |
| } |
| ((TableRowSorter)myAttributesTable.getRowSorter()).sort(); |
| myPanel.getAdvancedFilterCheckBox().getModel().setSelected(!myAttributesFilter.myIsFilterEnabled); |
| } |
| }); |
| |
| //We calling this to trigger tableChanged, which will calculate row heights and rePaint myPreviewPanel |
| model.fireTableStructureChanged(); |
| } |
| |
| @Override |
| public void dispose() { |
| myConfiguration.removeListener(myConfigListener); |
| myMessageBusConnection.disconnect(); |
| myMessageBusConnection = null; |
| super.dispose(); |
| } |
| |
| class StyleAttributesFilter extends RowFilter<AttributesTableModel, Integer> { |
| // TODO: This is just a random list of attributes. Replace with a possibly dynamic list of simple attributes. |
| public final Set<String> ATTRIBUTES_DEFAULT_FILTER = ImmutableSet |
| .of("android:colorPrimary", |
| "android:colorPrimaryDark", |
| "android:colorAccent", |
| "android:colorForeground", |
| "android:textColorPrimary", |
| "android:textColorSecondary", |
| "android:textColorPrimaryInverse", |
| "android:textColorSecondaryInverse", |
| "android:colorBackground", |
| "android:windowBackground", |
| "android:navigationBarColor"); |
| private boolean myIsFilterEnabled = true; |
| private Set<String> filterAttributes = ATTRIBUTES_DEFAULT_FILTER; |
| |
| public void setFilterEnabled(boolean enabled) { |
| this.myIsFilterEnabled = enabled; |
| } |
| |
| /** |
| * Set the attribute names we want to display. |
| */ |
| public void setAttributesFilter(@NotNull Set<String> attributeNames) { |
| filterAttributes = ImmutableSet.copyOf(attributeNames); |
| } |
| |
| @Override |
| public boolean include(Entry<? extends AttributesTableModel, ? extends Integer> entry) { |
| if (!myIsFilterEnabled) { |
| return true; |
| } |
| int row = entry.getIdentifier().intValue(); |
| if (entry.getModel().isSpecialRow(row)) { |
| return true; |
| } |
| |
| // We use the column 1 because it's the one that contains the ItemResourceValueWrapper. |
| Object value = entry.getModel().getValueAt(row, 1); |
| if (value instanceof TableLabel) { |
| return false; |
| } |
| |
| String attributeName; |
| if (value instanceof EditedStyleItem) { |
| attributeName = ((EditedStyleItem)value).getQualifiedName(); |
| } |
| else { |
| attributeName = value.toString(); |
| } |
| |
| ThemeEditorStyle selectedTheme = getSelectedStyle(); |
| if (selectedTheme == null) { |
| LOG.error("No theme selected."); |
| return false; |
| } |
| |
| return filterAttributes.contains(attributeName); |
| } |
| } |
| |
| @Override |
| public void setUI(PanelUI ui) { |
| super.setUI(ui); |
| updateUiParameters(); |
| } |
| |
| private void updateUiParameters() { |
| Font regularFont = UIUtil.getLabelFont(); |
| |
| int regularFontSize = getFontMetrics(regularFont).getHeight(); |
| myHeaderFont = regularFont.deriveFont(regularFontSize * HEADER_FONT_SCALE); |
| if (myAttributesTable == null) { |
| return; |
| } |
| |
| int headerFontSize = getFontMetrics(myHeaderFont).getHeight(); |
| |
| // Big cells contain two lines of text, and we want some space between them |
| // (thus multiplier is 2.8 rather than 2). Also, we need some padding on top and bottom. |
| int bigCellSize = (int) Math.floor(2.8f * regularFontSize) + LARGE_CELL_PADDING; |
| |
| myAttributesTable.setClassHeights(ImmutableMap.of( |
| Object.class, regularFontSize + REGULAR_CELL_PADDING, |
| Color.class, bigCellSize, |
| DrawableDomElement.class, bigCellSize, |
| TableLabel.class, headerFontSize + LARGE_CELL_PADDING |
| )); |
| } |
| } |