blob: 1c6fa05e4fb5f7f3d899dbab315ef24608a68873 [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.intellij.openapi.actionSystem.impl;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.actionSystem.impl.actionholder.ActionRef;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.SizedIcon;
import com.intellij.ui.plaf.beg.BegMenuItemUI;
import com.intellij.ui.plaf.gtk.GtkMenuItemUI;
import com.intellij.util.PlatformIcons;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.plaf.MenuItemUI;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.Set;
public class ActionMenuItem extends JCheckBoxMenuItem {
private static final Icon ourCheckedIcon = new SizedIcon(PlatformIcons.CHECK_ICON, 18, 18);
private static final Icon ourUncheckedIcon = EmptyIcon.ICON_18;
private final ActionRef<AnAction> myAction;
private final Presentation myPresentation;
private final String myPlace;
private final boolean myInsideCheckedGroup;
private DataContext myContext;
private AnActionEvent myEvent;
private MenuItemSynchronizer myMenuItemSynchronizer;
private final boolean myEnableMnemonics;
private final boolean myToggleable;
private boolean myToggled;
public ActionMenuItem(final AnAction action,
final Presentation presentation,
@NotNull final String place,
@NotNull DataContext context,
final boolean enableMnemonics,
final boolean prepareNow,
final boolean insideCheckedGroup) {
myAction = ActionRef.fromAction(action);
myPresentation = presentation;
myPlace = place;
myContext = context;
myEnableMnemonics = enableMnemonics;
myToggleable = action instanceof Toggleable;
myInsideCheckedGroup = insideCheckedGroup;
myEvent = new AnActionEvent(null, context, place, myPresentation, ActionManager.getInstance(), 0);
addActionListener(new ActionTransmitter());
setBorderPainted(false);
updateUI();
if (prepareNow) {
init();
}
else {
setText("loading...");
}
}
public void prepare() {
init();
installSynchronizer();
}
/**
* We have to make this method public to allow BegMenuItemUI to invoke it.
*/
@Override
public void fireActionPerformed(ActionEvent event) {
super.fireActionPerformed(event);
}
@Override
public void addNotify() {
super.addNotify();
installSynchronizer();
init();
}
@Override
public void removeNotify() {
uninstallSynchronizer();
super.removeNotify();
}
private void installSynchronizer() {
if (myMenuItemSynchronizer == null) {
myMenuItemSynchronizer = new MenuItemSynchronizer();
}
}
private void uninstallSynchronizer() {
if (myMenuItemSynchronizer != null) {
Disposer.dispose(myMenuItemSynchronizer);
myMenuItemSynchronizer = null;
}
}
private void init() {
setVisible(myPresentation.isVisible());
setEnabled(myPresentation.isEnabled());
setMnemonic(myEnableMnemonics ? myPresentation.getMnemonic() : 0);
setText(myPresentation.getText());
final int mnemonicIndex = myEnableMnemonics ? myPresentation.getDisplayedMnemonicIndex() : -1;
if (getText() != null && mnemonicIndex >= 0 && mnemonicIndex < getText().length()) {
setDisplayedMnemonicIndex(mnemonicIndex);
}
AnAction action = myAction.getAction();
updateIcon(action);
String id = ActionManager.getInstance().getId(action);
if (id != null) {
Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(id);
setAcceleratorFromShortcuts(shortcuts);
}
else {
final ShortcutSet shortcutSet = action.getShortcutSet();
if (shortcutSet != null) {
setAcceleratorFromShortcuts(shortcutSet.getShortcuts());
}
}
}
private void setAcceleratorFromShortcuts(final Shortcut[] shortcuts) {
for (Shortcut shortcut : shortcuts) {
if (shortcut instanceof KeyboardShortcut) {
final KeyStroke firstKeyStroke = ((KeyboardShortcut)shortcut).getFirstKeyStroke();
//If action has Enter shortcut, do not add it. Otherwise, user won't be able to chose any ActionMenuItem other than that
if (!isEnterKeyStroke(firstKeyStroke)) {
setAccelerator(firstKeyStroke);
}
break;
}
}
}
private static boolean isEnterKeyStroke(KeyStroke keyStroke) {
return keyStroke.getKeyCode() == KeyEvent.VK_ENTER && keyStroke.getModifiers() == 0;
}
@Override
public void updateUI() {
if (UIUtil.isStandardMenuLAF()) {
super.updateUI();
}
else {
setUI(BegMenuItemUI.createUI(this));
}
}
@Override
public void setUI(final MenuItemUI ui) {
final MenuItemUI newUi = UIUtil.isUnderGTKLookAndFeel() && GtkMenuItemUI.isUiAcceptable(ui) ? new GtkMenuItemUI(ui) : ui;
super.setUI(newUi);
}
/**
* Updates long description of action at the status bar.
*/
@Override
public void menuSelectionChanged(boolean isIncluded) {
super.menuSelectionChanged(isIncluded);
ActionMenu.showDescriptionInStatusBar(isIncluded, this, myPresentation.getDescription());
}
public String getFirstShortcutText() {
return KeymapUtil.getFirstKeyboardShortcutText(myAction.getAction());
}
public void updateContext(@NotNull DataContext context) {
myContext = context;
myEvent = new AnActionEvent(null, context, myPlace, myPresentation, ActionManager.getInstance(), 0);
}
private final class ActionTransmitter implements ActionListener {
/**
* @param component component
* @return whether the component in Swing tree or not. This method is more
* weak then {@link Component#isShowing() }
*/
private boolean isInTree(final Component component) {
if (component instanceof Window) {
return component.isShowing();
}
else {
Window windowAncestor = SwingUtilities.getWindowAncestor(component);
return windowAncestor != null && windowAncestor.isShowing();
}
}
@Override
public void actionPerformed(final ActionEvent e) {
final IdeFocusManager fm = IdeFocusManager.findInstanceByContext(myContext);
final ActionCallback typeAhead = new ActionCallback();
final String id = ActionManager.getInstance().getId(myAction.getAction());
if (id != null) {
FeatureUsageTracker.getInstance().triggerFeatureUsed("context.menu.click.stats." + id.replace(' ', '.'));
}
fm.typeAheadUntil(typeAhead);
fm.runOnOwnContext(myContext, new Runnable() {
@Override
public void run() {
final AnActionEvent event = new AnActionEvent(
new MouseEvent(ActionMenuItem.this, MouseEvent.MOUSE_PRESSED, 0, e.getModifiers(), getWidth() / 2, getHeight() / 2, 1, false),
myContext, myPlace, myPresentation, ActionManager.getInstance(), e.getModifiers()
);
final AnAction action = myAction.getAction();
if (ActionUtil.lastUpdateAndCheckDumb(action, event, false)) {
ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
actionManager.fireBeforeActionPerformed(action, myContext, event);
Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(event.getDataContext());
if (component != null && !isInTree(component)) {
typeAhead.setDone();
return;
}
SimpleTimer.getInstance().setUp(new Runnable() {
@Override
public void run() {
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
fm.doWhenFocusSettlesDown(typeAhead.createSetDoneRunnable());
}
});
}
}, Registry.intValue("actionSystem.typeAheadTimeAfterPopupAction"));
ActionUtil.performActionDumbAware(action, event);
actionManager.queueActionPerformedEvent(action, myContext, event);
}
else {
typeAhead.setDone();
}
}
});
}
}
private void updateIcon(AnAction action) {
if (isToggleable() && (myPresentation.getIcon() == null || myInsideCheckedGroup)) {
action.update(myEvent);
myToggled = Boolean.TRUE.equals(myEvent.getPresentation().getClientProperty(Toggleable.SELECTED_PROPERTY));
if (ActionPlaces.MAIN_MENU.equals(myPlace) && SystemInfo.isMacSystemMenu ||
UIUtil.isUnderNimbusLookAndFeel() ||
UIUtil.isUnderWindowsLookAndFeel() && SystemInfo.isWin7OrNewer) {
setState(myToggled);
}
else if (!(getUI() instanceof GtkMenuItemUI)) {
if (myToggled) {
setIcon(ourCheckedIcon);
setDisabledIcon(IconLoader.getDisabledIcon(ourCheckedIcon));
}
else {
setIcon(ourUncheckedIcon);
setDisabledIcon(IconLoader.getDisabledIcon(ourUncheckedIcon));
}
}
}
else {
if (UISettings.getInstance().SHOW_ICONS_IN_MENUS) {
Icon icon = myPresentation.getIcon();
if (action instanceof ToggleAction && ((ToggleAction)action).isSelected(myEvent)) {
icon = new PoppedIcon(icon, 16, 16);
}
setIcon(icon);
if (myPresentation.getDisabledIcon() != null) {
setDisabledIcon(myPresentation.getDisabledIcon());
}
else {
setDisabledIcon(IconLoader.getDisabledIcon(icon));
}
}
}
}
public boolean isToggleable() {
return myToggleable;
}
@Override
public boolean isSelected() {
return myToggled;
}
private final class MenuItemSynchronizer implements PropertyChangeListener, Disposable {
@NonNls private static final String SELECTED = "selected";
private final Set<String> mySynchronized = new HashSet<String>();
private MenuItemSynchronizer() {
myPresentation.addPropertyChangeListener(this);
}
@Override
public void dispose() {
myPresentation.removePropertyChangeListener(this);
}
@Override
public void propertyChange(PropertyChangeEvent e) {
boolean queueForDispose = getParent() == null;
String name = e.getPropertyName();
if (mySynchronized.contains(name)) return;
mySynchronized.add(name);
try {
if (Presentation.PROP_VISIBLE.equals(name)) {
final boolean visible = myPresentation.isVisible();
if (!visible && SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU)) {
setEnabled(false);
}
else {
setVisible(visible);
}
}
else if (Presentation.PROP_ENABLED.equals(name)) {
setEnabled(myPresentation.isEnabled());
updateIcon(myAction.getAction());
}
else if (Presentation.PROP_MNEMONIC_KEY.equals(name)) {
setMnemonic(myPresentation.getMnemonic());
}
else if (Presentation.PROP_MNEMONIC_INDEX.equals(name)) {
setDisplayedMnemonicIndex(myPresentation.getDisplayedMnemonicIndex());
}
else if (Presentation.PROP_TEXT.equals(name)) {
setText(myPresentation.getText());
}
else if (Presentation.PROP_ICON.equals(name) || Presentation.PROP_DISABLED_ICON.equals(name) || SELECTED.equals(name)) {
updateIcon(myAction.getAction());
}
}
finally {
mySynchronized.remove(name);
if (queueForDispose) {
// later since we cannot remove property listeners inside event processing
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (getParent() == null) {
uninstallSynchronizer();
}
}
});
}
}
}
}
}