blob: 2fb5778ab9a580f140ca77d99e1cab45e232d52a [file] [log] [blame]
/*
* 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.uibuilder.palette;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceResolver;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationListener;
import com.android.tools.idea.dependencies.DependencyManager;
import com.android.tools.idea.rendering.ImageUtils;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.tools.idea.uibuilder.editor.NlPreviewForm;
import com.android.tools.idea.uibuilder.model.DnDTransferComponent;
import com.android.tools.idea.uibuilder.model.DnDTransferItem;
import com.android.tools.idea.uibuilder.model.ItemTransferable;
import com.android.tools.idea.uibuilder.surface.ScreenView;
import com.intellij.designer.DesignerEditorPanelFacade;
import com.intellij.designer.LightToolWindowContent;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CopyProvider;
import com.intellij.ide.CutProvider;
import com.intellij.ide.DeleteProvider;
import com.intellij.ide.PasteProvider;
import com.intellij.ide.dnd.DnDAction;
import com.intellij.ide.dnd.DnDDragStartBean;
import com.intellij.ide.dnd.DnDManager;
import com.intellij.ide.dnd.DnDSource;
import com.intellij.ide.dnd.aware.DnDAwareTree;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.ActionManagerImpl;
import com.intellij.openapi.actionSystem.impl.MenuItemPresentationFactory;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.wm.impl.content.ToolWindowContentUi;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.TreeSpeedSearch;
import com.intellij.util.IJSwingUtilities;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import icons.AndroidIcons;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
public class NlPalettePanel extends JPanel implements LightToolWindowContent, ConfigurationListener, LafManagerListener, DataProvider {
private static final Insets INSETS = new Insets(0, 6, 0, 6);
private static final double PREVIEW_SCALE = 0.5;
private static final int ICON_SPACER = 4;
@NonNull private final JTree myTree;
@NonNull private final NlPaletteModel myModel;
@NonNull private final IconPreviewFactory myIconFactory;
@NonNull private final DesignerEditorPanelFacade myDesigner;
@NonNull private final Set<String> myMissingLibraries;
@NonNull private final Disposable myDisposable;
@NonNull private Mode myMode;
@Nullable private ScalableDesignSurface myDesignSurface;
@Nullable private BufferedImage myLastDragImage;
public NlPalettePanel(@NonNull DesignerEditorPanelFacade designer) {
myDesigner = designer;
myModel = NlPaletteModel.get();
myTree = new DnDAwareTree();
myIconFactory = IconPreviewFactory.get();
myMissingLibraries = new HashSet<String>();
myDisposable = Disposer.newDisposable();
myMode = Mode.ICON_AND_TEXT;
initTree();
JScrollPane pane = ScrollPaneFactory.createScrollPane(myTree, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_NEVER);
setLayout(new BorderLayout());
add(pane, BorderLayout.CENTER);
}
@NonNull
public JComponent getFocusedComponent() {
return myTree;
}
public enum Mode {
ICON_AND_TEXT("Show Icon and Text"),
PREVIEW("Show Preview");
private final String myMenuText;
Mode(String menuText) {
myMenuText = menuText;
}
@NonNull
public String getMenuText() {
return myMenuText;
}
}
public void setDesignSurface(@Nullable ScalableDesignSurface designSurface) {
if (myDesignSurface != null) {
Configuration configuration = myDesignSurface.getConfiguration();
if (configuration != null) {
configuration.removeListener(this);
}
}
myDesignSurface = designSurface;
if (myDesignSurface != null) {
Configuration configuration = myDesignSurface.getConfiguration();
if (configuration != null) {
configuration.addListener(this);
}
}
checkForNewMissingDependencies();
setMode(myMode);
}
private void updateColorsAfterColorThemeChange(boolean doUpdate) {
LafManager manager = LafManager.getInstance();
if (doUpdate) {
manager.addLafManagerListener(this);
}
else {
manager.removeLafManagerListener(this);
}
}
@Override
public void lookAndFeelChanged(LafManager source) {
setColors();
}
private void setColors() {
Color background;
Color foreground;
Configuration configuration = null;
if (myDesignSurface != null) {
configuration = myDesignSurface.getConfiguration();
}
ResourceResolver resolver = null;
if (configuration != null) {
resolver = configuration.getResourceResolver();
}
if (resolver == null || myMode != Mode.PREVIEW) {
foreground = UIUtil.getTreeForeground();
background = UIUtil.getTreeBackground();
}
else {
ResourceValue windowBackground = resolver.findItemInTheme("colorBackground", true);
background = ResourceHelper.resolveColor(resolver, windowBackground, configuration.getModule().getProject());
if (background == null) {
background = UIUtil.getTreeBackground();
}
ResourceValue textForeground = resolver.findItemInTheme("colorForeground", true);
foreground = ResourceHelper.resolveColor(resolver, textForeground, configuration.getModule().getProject());
if (foreground == null) {
foreground = UIUtil.getTreeForeground();
}
// Ensure the colors can be differentiated:
if (Math.abs(ImageUtils.getBrightness(background.getRGB()) - ImageUtils.getBrightness(foreground.getRGB())) < 64) {
if (ImageUtils.getBrightness(background.getRGB()) < 128) {
foreground = JBColor.WHITE;
}
else {
foreground = JBColor.BLACK;
}
}
}
myTree.setBackground(background);
myTree.setForeground(foreground);
}
@Override
public Object getData(@NonNls String dataId) {
if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId) ||
PlatformDataKeys.CUT_PROVIDER.is(dataId) ||
PlatformDataKeys.COPY_PROVIDER.is(dataId) ||
PlatformDataKeys.PASTE_PROVIDER.is(dataId)) {
return new ActionHandler();
}
return null;
}
// ---- implements ConfigurationListener ----
@Override
public boolean changed(int flags) {
setMode(myMode);
return true;
}
@NonNull
public AnAction[] getActions() {
return new AnAction[]{new OptionAction()};
}
private class OptionAction extends AnAction {
public OptionAction() {
// todo: Find a set of different icons
Presentation presentation = getTemplatePresentation();
presentation.setIcon(AllIcons.General.ProjectConfigurable);
presentation.setHoveredIcon(AllIcons.General.ProjectConfigurableBanner);
}
@Override
public void actionPerformed(@NonNull AnActionEvent e) {
int x = 0;
int y = 0;
InputEvent inputEvent = e.getInputEvent();
if (inputEvent instanceof MouseEvent) {
x = ((MouseEvent)inputEvent).getX();
y = ((MouseEvent)inputEvent).getY();
}
showOptionPopup(inputEvent.getComponent(), x, y);
}
}
private void showOptionPopup(@NonNull Component component, int x, int y) {
DefaultActionGroup group = new DefaultActionGroup();
group.add(new TogglePaletteModeAction(this, Mode.ICON_AND_TEXT));
group.add(new TogglePaletteModeAction(this, Mode.PREVIEW));
ActionPopupMenu popupMenu = ((ActionManagerImpl)ActionManager.getInstance())
.createActionPopupMenu(ToolWindowContentUi.POPUP_PLACE, group, new MenuItemPresentationFactory(true));
popupMenu.getComponent().show(component, x, y);
}
@NonNull
public Mode getMode() {
return myMode;
}
public void setMode(@NonNull Mode mode) {
myMode = mode;
if (mode == Mode.PREVIEW && myDesignSurface != null) {
Configuration configuration = myDesignSurface.getConfiguration();
if (configuration != null) {
myIconFactory.load(configuration, new Runnable() {
@Override
public void run() {
setColors();
invalidateUI();
}
});
}
}
else {
setColors();
invalidateUI();
}
}
private void invalidateUI() {
// BasicTreeUI keeps a cache of node heights. This will replace the ui and force a new node height computation.
IJSwingUtilities.updateComponentTreeUI(myTree);
}
private void initTree() {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(null);
DefaultTreeModel treeModel = new DefaultTreeModel(rootNode);
myTree.setModel(treeModel);
myTree.setRowHeight(0);
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
myTree.setRootVisible(false);
myTree.setShowsRootHandles(false);
myTree.setBorder(new EmptyBorder(INSETS));
myTree.setToggleClickCount(1);
ToolTipManager.sharedInstance().registerComponent(myTree);
TreeUtil.installActions(myTree);
createCellRenderer(myTree);
addData(myModel, rootNode);
expandAll(myTree, rootNode);
myTree.setSelectionRow(0);
new PaletteSpeedSearch(myTree);
enableDnD();
updateColorsAfterColorThemeChange(true);
enableClickToLoadMissingDependency();
}
private static void expandAll(@NonNull JTree tree, @NonNull DefaultMutableTreeNode rootNode) {
TreePath rootPath = new TreePath(rootNode);
tree.expandPath(rootPath);
TreeNode child = rootNode.getLastChild();
while (child != null) {
tree.expandPath(rootPath.pathByAddingChild(child));
child = rootNode.getChildBefore(child);
}
}
private void createCellRenderer(@NonNull JTree tree) {
ColoredTreeCellRenderer renderer = new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NonNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
Object content = node.getUserObject();
if (content instanceof NlPaletteItem) {
NlPaletteItem item = (NlPaletteItem)content;
BufferedImage image = null;
if (!needsLibraryLoad(item) && myMode == Mode.PREVIEW && myDesignSurface != null && myDesignSurface.getConfiguration() != null) {
image = myIconFactory.getImage(item, myDesignSurface.getConfiguration(), PREVIEW_SCALE);
}
if (image != null) {
setIcon(new ImageIcon(image));
}
else if (needsLibraryLoad(item)) {
Icon icon = item.getIcon();
if (icon == null) {
icon = AndroidIcons.Views.View;
}
Icon download = AllIcons.Actions.Download;
image = UIUtil
.createImage(download.getIconWidth() + icon.getIconWidth() + ICON_SPACER, icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D)image.getGraphics();
icon.paintIcon(myTree, g2, 0, 0);
download.paintIcon(myTree, g2, icon.getIconWidth() + ICON_SPACER, 0);
g2.dispose();
append(item.getTitle());
setIcon(new ImageIcon(image));
}
else {
append(item.getTitle());
setIcon(item.getIcon());
}
}
else if (content instanceof NlPaletteGroup) {
NlPaletteGroup group = (NlPaletteGroup)content;
append(group.getTitle());
setIcon(AllIcons.Nodes.Folder);
}
}
};
renderer.setBorder(BorderFactory.createEmptyBorder(1, 1, 0, 0));
tree.setCellRenderer(renderer);
}
private boolean needsLibraryLoad(@NonNull NlPaletteItem item) {
return !Collections.disjoint(myMissingLibraries, item.getLibraries());
}
private static void addData(@NonNull NlPaletteModel model, @NonNull DefaultMutableTreeNode rootNode) {
for (NlPaletteGroup group : model.getGroups()) {
DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode(group);
for (NlPaletteItem item : group.getItems()) {
DefaultMutableTreeNode itemNode = new DefaultMutableTreeNode(item);
groupNode.add(itemNode);
}
rootNode.add(groupNode);
}
}
private void enableDnD() {
final DnDManager dndManager = DnDManager.getInstance();
dndManager.registerSource(new PaletteDnDSource(), myTree);
}
private void enableClickToLoadMissingDependency() {
myTree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
NlPaletteItem item = getItemForPath(myTree.getPathForLocation(event.getX(), event.getY()));
if (item != null && needsLibraryLoad(item)) {
Module module = getModule();
DependencyManager manager = module != null ? DependencyManager.getDependencyManager(module.getProject()) : null;
if (manager != null) { // todo: remove when DependencyManager has been implemented for other BUILD cases.
manager.ensureLibraryIsIncluded(module, item.getLibraries(), null);
}
}
}
});
ApplicationManager.getApplication().getMessageBus().connect(myDisposable)
.subscribe(ProjectEx.ProjectSaved.TOPIC, new ProjectEx.ProjectSaved() {
@Override
public void saved(@NotNull final Project project) {
Module module = getModule();
if (module != null && module.getProject().equals(project)) {
if (checkForNewMissingDependencies()) {
invalidateUI();
}
}
}
});
}
private boolean checkForNewMissingDependencies() {
Module module = getModule();
DependencyManager manager = module != null ? DependencyManager.getDependencyManager(module.getProject()) : null;
List<String> missing = Collections.emptyList();
if (manager != null) {
missing = manager.findMissingDependencies(module, myModel.getLibrariesUsed());
}
if (missing.size() == myMissingLibraries.size() && myMissingLibraries.containsAll(missing)) {
return false;
}
myMissingLibraries.clear();
myMissingLibraries.addAll(missing);
return true;
}
@Nullable
private Module getModule() {
Configuration configuration = myDesignSurface != null ? myDesignSurface.getConfiguration() : null;
return configuration != null ? configuration.getModule() : null;
}
@Nullable
private static NlPaletteItem getItemForPath(@Nullable TreePath path) {
if (path == null) {
return null;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
Object content = node.getUserObject();
return content instanceof NlPaletteItem ? (NlPaletteItem)content : null;
}
@Override
public void dispose() {
if (myDesignSurface != null) {
Configuration configuration = myDesignSurface.getConfiguration();
if (configuration != null) {
configuration.removeListener(this);
}
}
updateColorsAfterColorThemeChange(false);
Disposer.dispose(myDisposable);
}
private class PaletteDnDSource implements DnDSource {
@Override
public boolean canStartDragging(DnDAction action, Point dragOrigin) {
NlPaletteItem item = getItemForPath(myTree.getPathForLocation(dragOrigin.x, dragOrigin.y));
return item != null && !needsLibraryLoad(item);
}
@Override
public DnDDragStartBean startDragging(DnDAction action, Point dragOrigin) {
TreePath path = myTree.getClosestPathForLocation(dragOrigin.x, dragOrigin.y);
NlPaletteItem item = getItemForPath(path);
assert item != null;
Dimension size = null;
if (myDesignSurface != null) {
ScreenView screenView = myDesignSurface.getCurrentScreenView();
BufferedImage image = screenView != null ? myIconFactory.renderDragImage(item, screenView, 1.0) : null;
if (image != null) {
size = new Dimension(image.getWidth(), image.getHeight());
myLastDragImage = image;
}
}
if (size == null) {
Rectangle bounds = myTree.getPathBounds(path);
size = bounds != null ? bounds.getSize() : new Dimension(200, 100);
if (myDesignSurface != null) {
double scale = myDesignSurface.getScale();
size.setSize(size.getWidth() / scale, size.getHeight() / scale);
}
}
if (myDesigner instanceof NlPreviewForm) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
((NlPreviewForm)myDesigner).minimizePalette();
}
});
}
DnDTransferComponent component = new DnDTransferComponent(item.getId(), item.getRepresentation(), size.width, size.height);
return new DnDDragStartBean(new ItemTransferable(new DnDTransferItem(component)));
}
@Nullable
@Override
public Pair<Image, Point> createDraggedImage(DnDAction action, Point dragOrigin) {
TreePath path = myTree.getClosestPathForLocation(dragOrigin.x, dragOrigin.y);
BufferedImage image = null;
if (myLastDragImage != null && myDesignSurface != null) {
double scale = myDesignSurface.getScale();
image = ImageUtils.scale(myLastDragImage, scale, scale);
myLastDragImage = null;
}
if (image == null) {
int row = myTree.getRowForPath(path);
Component comp =
myTree.getCellRenderer().getTreeCellRendererComponent(myTree, path.getLastPathComponent(), false, true, true, row, false);
comp.setForeground(myTree.getForeground());
comp.setBackground(myTree.getBackground());
comp.setFont(myTree.getFont());
comp.setSize(comp.getPreferredSize());
image = UIUtil.createImage(comp.getWidth(), comp.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D)image.getGraphics();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
comp.paint(g2);
g2.dispose();
}
return Pair.<Image, Point>pair(image, new Point(-image.getWidth() / 2, -image.getHeight() / 2));
}
@Override
public void dragDropEnd() {
myTree.clearSelection();
}
@Override
public void dropActionChanged(int gestureModifiers) {
}
}
private static final class PaletteSpeedSearch extends TreeSpeedSearch {
PaletteSpeedSearch(@NonNull JTree tree) {
super(tree);
}
@Override
protected boolean isMatchingElement(Object element, String pattern) {
if (pattern == null) {
return false;
}
TreePath path = (TreePath)element;
NlPaletteItem item = getItemForPath(path);
return item != null && compare(item.getTitle(), pattern);
}
}
private class ActionHandler implements DeleteProvider, CutProvider, CopyProvider, PasteProvider {
@Override
public void performCopy(@NonNull DataContext dataContext) {
TreePath path = myTree.getSelectionPath();
NlPaletteItem item = getItemForPath(path);
if (item != null && !needsLibraryLoad(item)) {
DnDTransferComponent component = new DnDTransferComponent(item.getId(), item.getRepresentation(), 0, 0);
CopyPasteManager.getInstance().setContents(new ItemTransferable(new DnDTransferItem(component)));
}
}
@Override
public boolean isCopyEnabled(@NonNull DataContext dataContext) {
return true;
}
@Override
public boolean isCopyVisible(@NonNull DataContext dataContext) {
return true;
}
@Override
public void performCut(@NonNull DataContext dataContext) {
}
@Override
public boolean isCutEnabled(@NonNull DataContext dataContext) {
return false;
}
@Override
public boolean isCutVisible(@NonNull DataContext dataContext) {
return false;
}
@Override
public void deleteElement(@NonNull DataContext dataContext) {
}
@Override
public boolean canDeleteElement(@NonNull DataContext dataContext) {
return false;
}
@Override
public void performPaste(@NonNull DataContext dataContext) {
}
@Override
public boolean isPastePossible(@NonNull DataContext dataContext) {
return false;
}
@Override
public boolean isPasteEnabled(@NonNull DataContext dataContext) {
return false;
}
}
}