blob: 651977a6f0b878f57584b374306bee8b0f779666 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.structure;
import com.google.common.collect.Lists;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.roots.ui.configuration.ConfigurationError;
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.Disposer;
import com.intellij.ui.JBColor;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.labels.LinkLabel;
import com.intellij.ui.components.labels.LinkListener;
import com.intellij.util.SystemProperties;
import com.intellij.util.ui.BaseButtonBehavior;
import com.intellij.util.ui.TimedDeadzone;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.util.Collection;
import java.util.List;
class ConfigurationErrorsPanel extends JPanel implements Disposable, ListDataListener {
private static final int MAX_ERRORS_TO_SHOW = SystemProperties.getIntProperty("idea.project.structure.max.errors.to.show", 100);
@NonNls private static final String FIX_ACTION_NAME = "FIX";
@NonNls private static final String NAVIGATE_ACTION_NAME = "NAVIGATE";
private ConfigurationErrorsListModel myListModel;
private ErrorView myCurrentView;
ConfigurationErrorsPanel() {
setLayout(new BorderLayout());
myListModel = new ConfigurationErrorsListModel();
myListModel.addListDataListener(this);
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
revalidate();
repaint();
}
});
ensureCurrentViewIs(ViewType.ONE_LINE);
}
@Override
public void dispose() {
if (myListModel != null) {
myListModel.removeListDataListener(this);
myListModel = null;
}
}
private void ensureCurrentViewIs(ViewType viewType) {
if (viewType == ViewType.ONE_LINE) {
if (myCurrentView instanceof OneLineErrorComponent) {
return;
}
OneLineErrorComponent c = new OneLineErrorComponent(myListModel) {
@Override
public void onViewChange(Object data) {
ensureCurrentViewIs(ViewType.MULTI_LINE);
}
};
if (myCurrentView != null) {
remove(myCurrentView.self());
Disposer.dispose(myCurrentView);
}
myCurrentView = c;
}
else {
if (myCurrentView instanceof MultiLineErrorComponent) {
return;
}
MultiLineErrorComponent c = new MultiLineErrorComponent(myListModel) {
@Override
public void onViewChange(Object data) {
ensureCurrentViewIs(ViewType.ONE_LINE);
}
};
if (myCurrentView != null) {
remove(myCurrentView.self());
Disposer.dispose(myCurrentView);
}
myCurrentView = c;
}
add(myCurrentView.self(), BorderLayout.CENTER);
myCurrentView.updateView();
UIUtil.adjustWindowToMinimumSize(SwingUtilities.getWindowAncestor(this));
revalidate();
repaint();
}
@Override
public void intervalAdded(ListDataEvent e) {
updateCurrentView();
}
@Override
public void intervalRemoved(ListDataEvent e) {
updateCurrentView();
}
@Override
public void contentsChanged(ListDataEvent e) {
updateCurrentView();
}
private void updateCurrentView() {
if (myCurrentView instanceof MultiLineErrorComponent && myListModel.getSize() == 0) {
ensureCurrentViewIs(ViewType.ONE_LINE);
}
myCurrentView.updateView();
}
void addErrors(@NotNull Collection<ProjectConfigurationError> errors) {
if (myListModel != null) {
myListModel.addErrors(errors);
}
}
void removeAllErrors() {
if (myListModel != null) {
myListModel.removeErrors();
}
}
boolean hasCriticalErrors() {
List<ProjectConfigurationError> errors = myListModel.getErrors();
if (!errors.isEmpty()) {
for (ProjectConfigurationError error : errors) {
if (!error.isIgnored()) {
return true;
}
}
}
return false;
}
private interface ErrorView extends Disposable {
void updateView();
void onViewChange(@Nullable Object data);
@NotNull
JComponent self();
}
private abstract static class MultiLineErrorComponent extends JPanel implements ErrorView {
private JList myList = new JBList();
protected MultiLineErrorComponent(@NotNull ConfigurationErrorsListModel model) {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
myList.setModel(model);
myList.setCellRenderer(new ErrorListRenderer(myList));
myList.setBackground(UIUtil.getPanelBackground());
myList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (!e.isPopupTrigger()) {
processListMouseEvent(e, true);
}
}
});
myList.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (!e.isPopupTrigger()) {
processListMouseEvent(e, false);
}
}
});
myList.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
myList.setCellRenderer(new ErrorListRenderer(myList)); // request cell renderer size invalidation
updatePreferredSize();
}
});
add(new JBScrollPane(myList), BorderLayout.CENTER);
add(buildToolbar(), BorderLayout.WEST);
}
@Override
public void dispose() {
}
private void processListMouseEvent(@NotNull MouseEvent e, boolean wasClicked) {
int index = myList.locationToIndex(e.getPoint());
if (index > -1) {
Object value = myList.getModel().getElementAt(index);
if (value instanceof ConfigurationError) {
ConfigurationError error = (ConfigurationError)value;
Component renderer = myList.getCellRenderer().getListCellRendererComponent(myList, value, index, false, false);
if (renderer instanceof ErrorListRenderer) {
Rectangle bounds = myList.getCellBounds(index, index);
renderer.setBounds(bounds);
renderer.doLayout();
Point point = e.getPoint();
point.translate(-bounds.x, -bounds.y);
Component deepestComponentAt = SwingUtilities.getDeepestComponentAt(renderer, point.x, point.y);
if (deepestComponentAt instanceof ToolbarAlikeButton) {
String name = ((ToolbarAlikeButton)deepestComponentAt).getButtonName();
if (wasClicked) {
if (FIX_ACTION_NAME.equals(name)) {
onClickFix(error, (JComponent)deepestComponentAt, e);
}
else if (NAVIGATE_ACTION_NAME.equals(name)) {
error.navigate();
}
}
else {
String toolTip = "";
if (FIX_ACTION_NAME.equals(name)) {
toolTip = "Fix";
}
else if (NAVIGATE_ACTION_NAME.equals(name)) {
toolTip = "Navigate to the problem";
}
myList.setToolTipText(toolTip);
return;
}
}
else if (e.getClickCount() == 2) {
error.navigate();
}
}
}
}
myList.setToolTipText(null);
}
private static void onClickFix(@NotNull ConfigurationError error, @NotNull JComponent component, @NotNull MouseEvent e) {
error.fix(component, new RelativePoint(e));
}
@Override
public void addNotify() {
super.addNotify();
updatePreferredSize();
}
private void updatePreferredSize() {
Window window = SwingUtilities.getWindowAncestor(this);
if (window != null) {
Dimension preferredSize = getPreferredSize();
setPreferredSize(new Dimension(preferredSize.width, 200));
setMinimumSize(getPreferredSize());
}
}
private JComponent buildToolbar() {
JPanel result = new JPanel();
result.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
result.setLayout(new BorderLayout());
result.add(new ToolbarAlikeButton(AllIcons.Actions.Collapseall) {
{
setToolTipText("Collapse");
}
@Override
public void onClick(MouseEvent e) {
onViewChange(null);
}
}, BorderLayout.NORTH);
return result;
}
@Override
public void updateView() {
}
@Override
@NotNull
public JComponent self() {
return this;
}
}
private abstract static class ToolbarAlikeButton extends JComponent {
private BaseButtonBehavior myBehavior;
private Icon myIcon;
private String myName;
private ToolbarAlikeButton(@NotNull Icon icon, @NotNull String name) {
this(icon);
myName = name;
}
private ToolbarAlikeButton(@NotNull Icon icon) {
myIcon = icon;
myBehavior = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
@Override
protected void execute(MouseEvent e) {
onClick(e);
}
};
setOpaque(false);
}
public String getButtonName() {
return myName;
}
public void onClick(MouseEvent e) {
}
@Override
public Insets getInsets() {
return new Insets(2, 2, 2, 2);
}
@Override
public Dimension getPreferredSize() {
return getMinimumSize();
}
@Override
public Dimension getMinimumSize() {
Insets insets = getInsets();
return new Dimension(myIcon.getIconWidth() + insets.left + insets.right, myIcon.getIconHeight() + insets.top + insets.bottom);
}
@Override
public void paint(Graphics g) {
Insets insets = getInsets();
Dimension d = getSize();
int x = (d.width - myIcon.getIconWidth() - insets.left - insets.right) / 2;
int y = (d.height - myIcon.getIconHeight() - insets.top - insets.bottom) / 2;
if (myBehavior.isPressedByMouse()) {
x += 1;
y += 1;
}
myIcon.paintIcon(this, g, x + insets.left, y + insets.top);
}
}
private static class ErrorListRenderer extends JComponent implements ListCellRenderer {
private boolean mySelected;
private boolean myHasFocus;
private JTextPane myText;
private JTextPane myFakeTextPane;
private JViewport myFakeViewport;
private JList myList;
private JPanel myButtonsPanel;
private JPanel myFixGroup;
private ErrorListRenderer(@NotNull JList list) {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
setOpaque(false);
myList = list;
myText = new JTextPane();
myButtonsPanel = new JPanel(new BorderLayout());
myButtonsPanel.setBorder(BorderFactory.createEmptyBorder(5, 3, 5, 3));
myButtonsPanel.setOpaque(false);
JPanel buttons = new JPanel();
buttons.setOpaque(false);
buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
myButtonsPanel.add(buttons, BorderLayout.NORTH);
add(myButtonsPanel, BorderLayout.EAST);
myFixGroup = new JPanel();
myFixGroup.setOpaque(false);
myFixGroup.setLayout(new BoxLayout(myFixGroup, BoxLayout.Y_AXIS));
myFixGroup.add(new ToolbarAlikeButton(AllIcons.Actions.QuickfixBulb, FIX_ACTION_NAME) {
});
myFixGroup.add(Box.createHorizontalStrut(3));
buttons.add(myFixGroup);
buttons.add(new ToolbarAlikeButton(AllIcons.General.AutoscrollToSource, NAVIGATE_ACTION_NAME) {
});
buttons.add(Box.createHorizontalStrut(3));
myFakeTextPane = new JTextPane();
myText.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
myFakeTextPane.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 0));
myText.setOpaque(false);
if (UIUtil.isUnderNimbusLookAndFeel()) {
myText.setBackground(UIUtil.TRANSPARENT_COLOR);
}
myText.setEditable(false);
myFakeTextPane.setEditable(false);
myText.setEditorKit(UIUtil.getHTMLEditorKit());
myFakeTextPane.setEditorKit(UIUtil.getHTMLEditorKit());
myFakeViewport = new JViewport();
myFakeViewport.setView(myFakeTextPane);
add(myText, BorderLayout.CENTER);
}
@Override
public Dimension getPreferredSize() {
Container parent = myList.getParent();
if (parent != null) {
myFakeTextPane.setText(myText.getText());
Dimension size = parent.getSize();
myFakeViewport.setSize(size);
Dimension preferredSize = myFakeTextPane.getPreferredSize();
Dimension buttonsPrefSize = myButtonsPanel.getPreferredSize();
int maxHeight = Math.max(buttonsPrefSize.height, preferredSize.height);
Insets insets = getInsets();
return new Dimension(Math.min(size.width - 20, preferredSize.width), maxHeight + insets.top + insets.bottom);
}
return super.getPreferredSize();
}
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
ConfigurationError error = (ConfigurationError)value;
myList = list;
mySelected = isSelected;
myHasFocus = cellHasFocus;
myFixGroup.setVisible(error.canBeFixed());
myText.setText(error.getDescription());
setBackground(error.isIgnored() ? MessageType.WARNING.getPopupBackground() : MessageType.ERROR.getPopupBackground());
return this;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
Rectangle bounds = getBounds();
Insets insets = getInsets();
GraphicsConfig cfg = new GraphicsConfig(g);
cfg.setAntialiasing(true);
Shape shape = new RoundRectangle2D.Double(insets.left, insets.top, bounds.width - 1 - insets.left - insets.right,
bounds.height - 1 - insets.top - insets.bottom, 6, 6);
if (mySelected) {
g2d.setColor(UIUtil.getListSelectionBackground());
g2d.fillRect(0, 0, bounds.width, bounds.height);
}
g2d.setColor(JBColor.WHITE);
g2d.fill(shape);
Color bgColor = getBackground();
g2d.setColor(bgColor);
g2d.fill(shape);
g2d.setColor(myHasFocus || mySelected ? getBackground().darker().darker() : getBackground().darker());
g2d.draw(shape);
cfg.restore();
super.paintComponent(g);
}
}
private abstract static class OneLineErrorComponent extends JComponent implements ErrorView, LinkListener {
private LinkLabel myErrorsLabel = new LinkLabel("", null);
private JLabel mySingleErrorLabel = new JLabel();
private ConfigurationErrorsListModel myModel;
private OneLineErrorComponent(@NotNull ConfigurationErrorsListModel model) {
myModel = model;
setLayout(new BorderLayout());
setOpaque(true);
updateLabel(myErrorsLabel, MessageType.ERROR.getPopupBackground(), this, "Errors");
updateLabel(mySingleErrorLabel, MessageType.ERROR.getPopupBackground(), null, null);
}
@Override
public void dispose() {
myModel = null;
}
private static void updateLabel(@NotNull JLabel label,
@NotNull Color bgColor,
@Nullable LinkListener listener,
@Nullable Object linkData) {
label.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5));
label.setOpaque(true);
label.setBackground(bgColor);
if (label instanceof LinkLabel) {
//noinspection ConstantConditions
((LinkLabel)label).setListener(listener, linkData);
}
}
@Override
public void updateView() {
if (myModel.getSize() == 0) {
setBorder(null);
}
else {
if (getBorder() == null) {
setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(5, 0, 5, 0, UIUtil.getPanelBackground()),
BorderFactory.createLineBorder(UIUtil.getPanelBackground().darker()))
);
}
}
List<ProjectConfigurationError> errors = myModel.getErrors();
if (errors.size() > 0) {
if (errors.size() == 1) {
mySingleErrorLabel.setText(myModel.getErrors().get(0).getPlainTextTitle());
}
else {
myErrorsLabel.setText(String.format("%s errors found", getErrorsCount(errors.size())));
}
}
removeAll();
if (errors.size() > 0) {
if (errors.size() == 1) {
add(wrapLabel(mySingleErrorLabel, errors.get(0)), BorderLayout.CENTER);
mySingleErrorLabel.setToolTipText(errors.get(0).getDescription());
}
else {
add(myErrorsLabel, BorderLayout.CENTER);
}
}
revalidate();
repaint();
}
private static String getErrorsCount(int size) {
return size < MAX_ERRORS_TO_SHOW ? String.valueOf(size) : MAX_ERRORS_TO_SHOW + "+";
}
private JComponent wrapLabel(@NotNull JLabel label, @NotNull ConfigurationError configurationError) {
JPanel result = new JPanel(new BorderLayout());
result.setBackground(label.getBackground());
result.add(label, BorderLayout.CENTER);
JPanel buttonsPanel = new JPanel();
buttonsPanel.setOpaque(false);
buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
if (configurationError.canBeFixed()) {
buttonsPanel.add(new ToolbarAlikeButton(AllIcons.Actions.QuickfixBulb) {
{
setToolTipText("Fix error");
}
@Override
public void onClick(MouseEvent e) {
Object o = myModel.getElementAt(0);
if (o instanceof ConfigurationError) {
((ConfigurationError)o).fix(this, new RelativePoint(e));
updateView();
Container ancestor = SwingUtilities.getAncestorOfClass(ConfigurationErrorsPanel.class, this);
if (ancestor != null && ancestor instanceof JComponent) {
((JComponent)ancestor).revalidate();
ancestor.repaint();
}
}
}
});
buttonsPanel.add(Box.createHorizontalStrut(3));
}
buttonsPanel.add(new ToolbarAlikeButton(AllIcons.General.AutoscrollToSource) {
{
setToolTipText("Navigate to error");
}
@Override
public void onClick(MouseEvent e) {
Object o = myModel.getElementAt(0);
if (o instanceof ConfigurationError) {
((ConfigurationError)o).navigate();
}
}
});
buttonsPanel.add(Box.createHorizontalStrut(3));
result.add(buttonsPanel, BorderLayout.EAST);
return result;
}
@Override
@NotNull
public JComponent self() {
return this;
}
@Override
public abstract void onViewChange(Object data);
@Override
public void linkSelected(LinkLabel aSource, Object data) {
onViewChange(data);
}
}
private static class ConfigurationErrorsListModel extends AbstractListModel {
private List<ProjectConfigurationError> myAllErrors = Lists.newArrayList();
@Override
public int getSize() {
return Math.min(myAllErrors.size(), MAX_ERRORS_TO_SHOW);
}
@Nullable
@Override
public Object getElementAt(int index) {
return myAllErrors.get(index);
}
void addErrors(@NotNull Collection<ProjectConfigurationError> errors) {
for (ProjectConfigurationError error : errors) {
addError(error);
}
}
private void addError(@NotNull ProjectConfigurationError error) {
if (!myAllErrors.contains(error)) {
myAllErrors.add(error);
int i = myAllErrors.indexOf(error);
if (i != -1 && i < MAX_ERRORS_TO_SHOW) {
fireIntervalAdded(this, i, i);
}
}
}
@NotNull
private List<ProjectConfigurationError> getErrors() {
return myAllErrors;
}
public void removeErrors() {
boolean hadErrors = !myAllErrors.isEmpty();
myAllErrors.clear();
if (hadErrors) {
fireContentsChanged(this, 0, 0);
}
}
}
private enum ViewType {
ONE_LINE, MULTI_LINE
}
}