blob: 32d4f19c587a5a4cd2771c515f6f15494c2a26db [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.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.UIUtil;
import icons.AndroidIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
/**
* This is a copy of {@link com.intellij.openapi.actionSystem.ex.ComboBoxAction}, with a couple of crucial changes for ADT:
* <ul>
* <li>
* The visual appearance has changed: there is no background gradient, and no border, on the button
* until the mouse is actually over the button. This makes the button "flatter" and more suitable
* for use as a toolbar button.
* </li>
* <li>
* The action has a {@link #handleIconClicked} method which
* an action subclass can override to handle a click on the icon. Some actions will use this to
* handle the action directly and not pop open the action menu. Clicking on the optional text or
* arrow will always open up the menu.
* </li>
* <li>
* The arrow icons have been changed to be less visually prominent (smaller, lighter)
* </li>
* <li>
* The smallVariant flag from ComboBoxAction was removed; this action only supports smallVariant.
* </li>
* <p/>
* </ul>
* Originally, I had this just as a subclass of ComboBoxAction, only overriding the
* paint method, but that became uglier and uglier as I had to counteract padding etc
* done in the parent class by overriding and subtracting in getPreferredSize, getInsets, etc.
* I also had to register my own mouse listener to find out whether the mouse is hovering
* since myMouseInside was private.
* <p/>
* And it became impossible when I tried to override the click behavior for clicking on icons;
* I simply did not have the hooks to prevent the menu from popping up.
* <p/>
* So, I forked.
* <p/>
* I at first tried to keep the original code with small deltas, but the deltas got uglier
* and uglier so I finally went and cleaned up the code, removed a lot of dead code, etc.
*/
public abstract class FlatComboAction extends AnAction implements CustomComponentAction {
private static final Icon ARROW_DOWN = AndroidIcons.ArrowDown;
private static final Icon DISABLED_ARROW_ICON = IconLoader.getDisabledIcon(ARROW_DOWN);
private DataContext myDataContext;
protected FlatComboAction() {
}
/**
* Invoked if the user clicks on the icon, not the text or arrow part. Subclasses of the action
* can use this to for example perform immediate actions.
*
* @return true if the action has been handled; this will cancel opening the menu, and
* otherwise return false to have the default handling (which pops open a menu)
*/
protected boolean handleIconClicked() {
return false;
}
@Override
public void actionPerformed(AnActionEvent e) {
}
@Override
public JComponent createCustomComponent(Presentation presentation) {
JPanel panel = new JPanel(new GridBagLayout());
FlatComboButton button = createComboBoxButton(presentation);
panel.add(button,
new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
return panel;
}
protected FlatComboButton createComboBoxButton(Presentation presentation) {
return new FlatComboButton(presentation);
}
@Override
public void update(AnActionEvent e) {
super.update(e);
myDataContext = e.getDataContext();
}
@NotNull
protected abstract DefaultActionGroup createPopupActionGroup(JComponent button);
protected int getMaxRows() {
return 30;
}
protected int getMinHeight() {
return 1;
}
protected int getMinWidth() {
return 1;
}
protected class FlatComboButton extends JButton {
private final Presentation myPresentation;
private boolean myForcePressed = false;
private PropertyChangeListener myButtonSynchronizer;
private boolean myMouseInside = false;
private JBPopup myPopup;
public FlatComboButton(Presentation presentation) {
myPresentation = presentation;
setModel(new MyButtonModel());
setHorizontalAlignment(LEFT);
setFocusable(false);
Insets margins = getMargin();
setMargin(new Insets(margins.top, 2, margins.bottom, 2));
setBorder(IdeBorderFactory.createEmptyBorder(0, 2, 0, 2));
if (!UIUtil.isUnderGTKLookAndFeel()) {
setFont(UIUtil.getLabelFont().deriveFont(11.0f));
}
addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (!myForcePressed) {
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
final Icon icon = getIcon();
if (icon != null && isShowing()) {
Point location = MouseInfo.getPointerInfo().getLocation();
Point current = getLocationOnScreen();
int x = location.x - current.x;
// 3 is from painting code. I need to clean this crap up !
if (x < icon.getIconWidth() + 3) {
if (handleIconClicked()) {
return;
}
}
}
showPopup();
}
});
}
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
myMouseInside = true;
repaint();
}
@Override
public void mouseExited(MouseEvent e) {
myMouseInside = false;
repaint();
}
@Override
public void mousePressed(final MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
e.consume();
doClick();
}
}
@Override
public void mouseReleased(MouseEvent e) {
dispatchEventToPopup(e);
}
});
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
mouseMoved(
new MouseEvent(e.getComponent(), MouseEvent.MOUSE_MOVED, e.getWhen(), e.getModifiers(), e.getX(), e.getY(), e.getClickCount(),
e.isPopupTrigger(), e.getButton()));
}
@Override
public void mouseMoved(MouseEvent e) {
dispatchEventToPopup(e);
}
});
}
// Event forwarding. We need it if user does press-and-drag gesture for opening popup and
// choosing item there.
// It works in JComboBox, here we provide the same behavior
private void dispatchEventToPopup(MouseEvent e) {
if (myPopup != null && myPopup.isVisible()) {
JComponent content = myPopup.getContent();
Rectangle rectangle = content.getBounds();
Point location = rectangle.getLocation();
SwingUtilities.convertPointToScreen(location, content);
Point eventPoint = e.getLocationOnScreen();
rectangle.setLocation(location);
if (rectangle.contains(eventPoint)) {
MouseEvent event = SwingUtilities.convertMouseEvent(e.getComponent(), e, myPopup.getContent());
Component component = SwingUtilities.getDeepestComponentAt(content, event.getX(), event.getY());
if (component != null) component.dispatchEvent(event);
}
}
}
public void showPopup() {
myForcePressed = true;
repaint();
Runnable onDispose = new Runnable() {
@Override
public void run() {
// give button chance to handle action listener
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
myForcePressed = false;
myPopup = null;
}
});
repaint();
}
};
myPopup = createPopup(onDispose);
myPopup.show(new RelativePoint(this, new Point(0, this.getHeight() - 1)));
}
@Nullable
@Override
public String getToolTipText() {
return myForcePressed ? null : super.getToolTipText();
}
protected JBPopup createPopup(Runnable onDispose) {
DefaultActionGroup group = createPopupActionGroup(this);
DataContext context = getDataContext();
myDataContext = null;
final ListPopup popup = JBPopupFactory.getInstance()
.createActionGroupPopup(null, group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true, onDispose, getMaxRows());
popup.setMinimumSize(new Dimension(getMinWidth(), getMinHeight()));
return popup;
}
protected DataContext getDataContext() {
return myDataContext == null || PlatformDataKeys.CONTEXT_COMPONENT.getData(myDataContext) == null
? DataManager.getInstance().getDataContext(this) : myDataContext;
}
@Override
public void removeNotify() {
if (myButtonSynchronizer != null) {
myPresentation.removePropertyChangeListener(myButtonSynchronizer);
myButtonSynchronizer = null;
}
super.removeNotify();
}
@Override
public void addNotify() {
super.addNotify();
if (myButtonSynchronizer == null) {
myButtonSynchronizer = new MyButtonSynchronizer();
myPresentation.addPropertyChangeListener(myButtonSynchronizer);
}
initButton();
}
private void initButton() {
setIcon(myPresentation.getIcon());
setEnabled(myPresentation.isEnabled());
setText(myPresentation.getText());
updateTooltipText(myPresentation.getDescription());
updateButtonSize();
}
private void updateTooltipText(String description) {
String tooltip = KeymapUtil.createTooltipText(description, FlatComboAction.this);
setToolTipText(!tooltip.isEmpty() ? tooltip : null);
}
@Override
public void updateUI() {
super.updateUI();
if (!UIUtil.isUnderGTKLookAndFeel()) {
setBorder(UIUtil.getButtonBorder());
}
}
protected class MyButtonModel extends DefaultButtonModel {
@Override
public boolean isPressed() {
return myForcePressed || super.isPressed();
}
@Override
public boolean isArmed() {
return myForcePressed || super.isArmed();
}
}
private class MyButtonSynchronizer implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if (Presentation.PROP_TEXT.equals(propertyName)) {
setText((String)evt.getNewValue());
updateButtonSize();
}
else if (Presentation.PROP_DESCRIPTION.equals(propertyName)) {
updateTooltipText((String)evt.getNewValue());
}
else if (Presentation.PROP_ICON.equals(propertyName)) {
setIcon((Icon)evt.getNewValue());
updateButtonSize();
}
else if (Presentation.PROP_ENABLED.equals(propertyName)) {
setEnabled(((Boolean)evt.getNewValue()).booleanValue());
}
}
}
@Override
public Insets getInsets() {
final Insets insets = super.getInsets();
return new Insets(insets.top, insets.left, insets.bottom, insets.right + ARROW_DOWN.getIconWidth());
}
@Override
public Insets getInsets(Insets insets) {
final Insets result = super.getInsets(insets);
result.right += ARROW_DOWN.getIconWidth();
return result;
}
@Override
public boolean isOpaque() {
return false;
}
@Override
public Dimension getPreferredSize() {
final boolean isEmpty = getIcon() == null && StringUtil.isEmpty(getText());
int width = isEmpty ? 10 + ARROW_DOWN.getIconWidth() : super.getPreferredSize().width;
// See ActionToolBarImpl: For a horizontal toolbar, the preferred height is 24
return new Dimension(width, 24);
}
@Override
public void paint(Graphics g) {
GraphicsUtil.setupAntialiasing(g);
boolean textEmpty = StringUtil.isEmpty(getText());
final boolean isEmpty = getIcon() == null && textEmpty;
final Dimension size = getSize();
{
final Graphics2D g2 = (Graphics2D)g;
Color controlColor = getParent().getBackground();
g2.setColor(controlColor);
final int w = getWidth();
final int h = getHeight();
if (getModel().isArmed() && getModel().isPressed()) {
g2.setPaint(new GradientPaint(0, 0, controlColor, 0, h, ColorUtil.shift(controlColor, 0.8)));
}
else {
if (myMouseInside) {
g2.setPaint(new GradientPaint(0, 0, ColorUtil.shift(controlColor, 1.1), 0, h,
ColorUtil.shift(controlColor, 0.9)));
}
}
g2.fillRect(1, 1, w - 2, h - 2);
GraphicsUtil.setupAntialiasing(g2);
if (myMouseInside) {
if (!UIUtil.isUnderDarcula()) {
g2.setPaint(new GradientPaint(0, 0, UIUtil.getBorderColor().darker(), 0, h, UIUtil.getBorderColor().darker().darker()));
}
else {
g2.setPaint(new GradientPaint(0, 0, ColorUtil.shift(controlColor, 1.4), 0, h,
ColorUtil.shift(controlColor, 1.5)));
}
g2.drawRect(0, 0, w - 1, h - 1);
}
final Icon icon = getIcon();
int x = 2;
if (icon != null) {
icon.paintIcon(null, g, x, (size.height - icon.getIconHeight()) / 2 - 1);
x += icon.getIconWidth() + 1;
}
if (!textEmpty) {
final Font font = getFont();
g2.setFont(font);
g2.setColor(UIManager.getColor("Panel.foreground"));
g2.drawString(getText(), x, (size.height + font.getSize()) / 2 - 1);
}
}
final Insets insets = super.getInsets();
final Icon icon;
if (isEnabled()) {
icon = ARROW_DOWN;
}
else {
icon = DISABLED_ARROW_ICON;
}
final int x;
if (isEmpty) {
x = (size.width - icon.getIconWidth()) / 2;
}
else {
x = size.width - icon.getIconWidth() - insets.right - (textEmpty ? 1 : 4);
}
icon.paintIcon(null, g, x, (size.height - icon.getIconHeight()) / 2);
g.setPaintMode();
}
protected void updateButtonSize() {
invalidate();
repaint();
}
}
}