/*
* Copyright 2003-2016 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 jetbrains.mps.nodeEditor;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.application.RuntimeInterruptedException;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.ui.awt.RelativePoint;
import jetbrains.mps.editor.runtime.cells.ReadOnlyUtil;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.ide.actions.MPSActions;
import jetbrains.mps.intentions.IntentionsManager;
import jetbrains.mps.intentions.IntentionsManager.QueryDescriptor;
import jetbrains.mps.intentions.LightBulbMenu;
import jetbrains.mps.intentions.icons.Icons;
import jetbrains.mps.intentions.icons.IntentionIconProvider;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.intentions.IntentionExecutable;
import jetbrains.mps.openapi.intentions.Kind;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import jetbrains.mps.util.Pair;
import jetbrains.mps.workbench.action.BaseAction;
import jetbrains.mps.workbench.action.BaseGroup;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.ModelAccess;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
public class IntentionsSupport {
static final long INTENTION_SHOW_DELAY = 1000;
private AbstractAction myShowIntentionsAction;
private Point myLightBulbLocation = new Point();
private LightBulbMenu myLightBulb;
private AtomicReference<Thread> myShowIntentionsThread = new AtomicReference<Thread>();
@NotNull
private EditorComponent myEditor;
public IntentionsSupport(@NotNull EditorComponent editor) {
myEditor = editor;
myLightBulb = new LightBulbMenu() {
@Override
public void activate() {
getModelAccess().runReadAction(() -> checkAndShowMenu());
}
};
myEditor.getViewport().addChangeListener(e -> adjustLightBulbLocation());
myShowIntentionsAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
getModelAccess().runReadAction(() -> checkAndShowMenu());
}
};
myEditor.registerKeyboardAction(myShowIntentionsAction, KeyStroke.getKeyStroke("alt ENTER"), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myEditor.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
updateIntentionsStatus();
}
@Override
public void focusLost(FocusEvent e) {
hideLightBulb();
}
});
myEditor.getSelectionManager().addSelectionListener((editorComponent, oldSelection, newSelection) -> {
if (oldSelection == newSelection) {
return;
}
if (!((EditorComponent) editorComponent).isFocusOwner()) {
return;
}
updateIntentionsStatus();
});
}
private void checkAndShowMenu() {
if (isInconsistentEditor()) {
return;
}
if (ReadOnlyUtil.isSelectionReadOnlyInEditor(myEditor) || SModelOperations.isReadOnly(myEditor.getSelectedNode().getModel())) {
return;
}
showIntentionsMenu();
}
private void updateIntentionsStatus() {
Thread thread = myShowIntentionsThread.get();
if (thread != null) {
thread.interrupt();
}
hideLightBulb();
myShowIntentionsThread.set(new Thread("Intentions") {
@Override
public void run() {
try {
Thread.sleep(IntentionsSupport.INTENTION_SHOW_DELAY);
if (interrupted()) {
return;
}
final Kind intentionKind = new ModelAccessHelper(getModelAccess()).runReadAction(() -> {
if (isInconsistentEditor() || ReadOnlyUtil.isSelectionReadOnlyInEditor(myEditor)) {
return null;
}
// TODO check for ActionsAsIntentions
return TypeContextManager.getInstance().runTypeCheckingComputation(myEditor.getTypecheckingContextOwner(), myEditor.getEditedNode(),
context -> IntentionsManager.getInstance()
.getHighestAvailableBaseIntentionType(
myEditor.getSelectedNode(),
myEditor.getEditorContext()));
});
if (intentionKind == null || interrupted()) {
return;
}
getModelAccess().runReadInEDT(() -> {
if (isInconsistentEditor() || ReadOnlyUtil.isSelectionReadOnlyInEditor(myEditor) || interrupted()) {
return;
}
if (myEditor.getSelectedCell() == null) {
hideLightBulb();
} else {
adjustLightBulbLocation();
showLightBulbComponent(intentionKind == Kind.NORMAL ? Icons.INTENTION : new IntentionIconProvider(intentionKind).getIcon());
}
});
} catch (InterruptedException e) {
} catch (RuntimeInterruptedException e) {
} finally {
myShowIntentionsThread.compareAndSet(this, null);
}
}
});
myShowIntentionsThread.get().start();
}
private boolean isInconsistentEditor() {
return myEditor.isDisposed() || myEditor.getEditedNode() == null || !myEditor.hasValidSelectedNode();
}
private void adjustLightBulbLocation() {
EditorCell selectedCell = myEditor.getSelectedCell();
if (selectedCell == null) {
return;
}
Point p = getLightBulbLocation(selectedCell);
myLightBulbLocation.setLocation(p);
myLightBulb.setLocation(myLightBulbLocation);
}
private void showLightBulbComponent(Icon icon) {
myLightBulb.setIcon(icon);
myEditor.add(myLightBulb);
myLightBulb.setLocation(myLightBulbLocation);
myEditor.repaintExternalComponent();
}
private void hideLightBulb() {
myEditor.remove(myLightBulb);
}
@NotNull
private Point getInsertedPosition(@NotNull Rectangle parentView, @NotNull Dimension childDim, @NotNull Point preferredLoc) {
Point p = new Point(preferredLoc);
p.x = Math.max(p.x, parentView.x + 2);
p.y = Math.max(p.y, parentView.y + 2);
p.x = Math.min(p.x, parentView.x + parentView.width - 2 - childDim.width);
p.y = Math.min(p.y, parentView.y + parentView.height - 2 - childDim.height);
return p;
}
@NotNull
private Point getLightBulbLocation(@NotNull EditorCell selectedCell) {
int x = myEditor.getRootCell().getX() - myEditor.getShiftX();// - myLightBulb.getWidth() - 6;
int y = selectedCell.getY();
Rectangle viewRect = myEditor.getViewport().getViewRect();
return getInsertedPosition(viewRect, myLightBulb.getPreferredSize(), new Point(x, y));
}
private void executeIntention(final IntentionExecutable intention, final SNode node) {
getModelAccess().executeCommandInEDT(new EditorCommand(myEditor) {
@Override
public void doExecute() {
intention.execute(node, myEditor.getEditorContext());
}
});
}
private AnAction getIntentionGroup(final IntentionExecutable intention, final SNode node) {
Icon icon = new IntentionIconProvider(intention.getDescriptor().getKind()).getIcon();
String text = intention.getDescription(node, myEditor.getEditorContext());
List<AnAction> intentionActions = new ArrayList<>();
for (IntentionActionsProvider provider : Extensions.getExtensions(IntentionActionsProvider.EP_NAME)) {
for (AnAction action : provider.getIntentionActions(intention)) {
intentionActions.add(action);
}
}
if (intentionActions.isEmpty()) {
return new BaseAction(text, null, icon) {
@Override
protected void doExecute(AnActionEvent e, Map<String, Object> params) {
executeIntention(intention, node);
}
};
} else {
DefaultActionGroup intentionActionGroup = new DefaultActionGroup(text, true) {
@Override
public boolean canBePerformed(DataContext c) {
return true;
}
@Override
public void actionPerformed(AnActionEvent e) {
executeIntention(intention, node);
}
};
intentionActionGroup.addAll(intentionActions);
intentionActionGroup.getTemplatePresentation().setIcon(icon);
return intentionActionGroup;
}
}
private BaseGroup getIntentionsGroup(final DataContext dataContext) {
// intentions
List<Pair<IntentionExecutable, SNode>> groupItems = new ArrayList<>();
groupItems.addAll(getEnabledIntentions());
// actions as intentions
List<AnAction> actions = new ArrayList<>();
collectActionsAsIntentions(ActionManager.getInstance().getAction(MPSActions.ACTIONS_AS_INTENTIONS_GROUP), actions, dataContext);
if (groupItems.isEmpty() && actions.isEmpty()) {
return null;
}
// TODO sort actions & intentions together
groupItems.sort((o1, o2) -> {
IntentionExecutable intention1 = o1.o1;
IntentionExecutable intention2 = o2.o1;
SNode node1 = o1.o2;
SNode node2 = o2.o2;
EditorContext context = myEditor.getEditorContext();
return intention1.getDescription(node1, context).compareTo(intention2.getDescription(node2, context));
});
BaseGroup group = new BaseGroup("");
for (final Pair<IntentionExecutable, SNode> pair : groupItems) {
group.add(getIntentionGroup(pair.o1, pair.o2));
}
group.addAll(actions);
return group;
}
private void collectActionsAsIntentions(AnAction action, List<AnAction> actions, DataContext dataContext) {
if (action instanceof ActionGroup) {
for (AnAction child : ((ActionGroup) action).getChildren(null)) {
collectActionsAsIntentions(child, actions, dataContext);
}
} else if (action instanceof BaseAction) {
Presentation presentation = action.getTemplatePresentation();
if (presentation.getIcon() == null) {
presentation.setIcon(Icons.REAL_INTENTION);
}
action.update(new AnActionEvent(null, dataContext, "", presentation, ActionManager.getInstance(), 0));
if (presentation.isVisible()) {
actions.add(action);
}
}
}
private void showIntentionsMenu() {
final EditorContext editorContext = myEditor.getEditorContext();
ListPopup popup = new ModelAccessHelper(getModelAccess()).runReadAction(() -> {
DataContext dataContext = DataManager.getInstance().getDataContext(editorContext.getNodeEditorComponent());
BaseGroup group = getIntentionsGroup(dataContext);
if (group == null) {
return null;
}
return JBPopupFactory.getInstance().createActionGroupPopup(
"Intentions",
group,
dataContext,
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
false);
});
if (popup == null) {
return;
}
final EditorCell selectedCell = editorContext.getSelectedCell();
int x = selectedCell.getX();
int y = selectedCell.getY();
if (selectedCell instanceof EditorCell_Label) {
y += selectedCell.getHeight();
}
RelativePoint relativePoint = new RelativePoint(editorContext.getNodeEditorComponent(), new Point(x, y));
popup.show(relativePoint);
}
private Set<Pair<IntentionExecutable, SNode>> getEnabledIntentions() {
final Set<Pair<IntentionExecutable, SNode>> result = new LinkedHashSet<>();
final SNode node = myEditor.getSelectedNode();
final EditorContext editorContext = myEditor.getEditorContext();
if (node != null) {
final QueryDescriptor query = new QueryDescriptor();
query.setEnabledOnly(true);
final Collection<Pair<IntentionExecutable, SNode>> availableIntentions =
TypeContextManager.getInstance().runTypeCheckingComputation(myEditor.getTypecheckingContextOwner(), myEditor.getEditedNode(),
context -> IntentionsManager.getInstance()
.getAvailableIntentions(query, node, editorContext));
result.addAll(availableIntentions);
}
return result;
}
public boolean isLightBulbVisible() {
return myLightBulb.isVisible();
}
private ModelAccess getModelAccess() {
return myEditor.getRepository().getModelAccess();
}
}