blob: 29d567145512fa453cb20f9c947c1f6f8391ae81 [file] [log] [blame]
/*
* Copyright 2000-2013 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.ide.actionMacro;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.IdeEventQueue;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.components.ExportableApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.playback.PlaybackContext;
import com.intellij.openapi.ui.playback.PlaybackRunner;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupAdapter;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.NamedJDOMExternalizable;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.CustomStatusBarWidget;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.panels.NonOpaquePanel;
import com.intellij.util.Consumer;
import com.intellij.util.ui.AnimatedIcon;
import com.intellij.util.ui.BaseButtonBehavior;
import com.intellij.util.ui.PositionTracker;
import com.intellij.util.ui.UIUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author max
*/
public class ActionMacroManager implements ExportableApplicationComponent, NamedJDOMExternalizable {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.actionMacro.ActionMacroManager");
private static final String TYPING_SAMPLE = "WWWWWWWWWWWWWWWWWWWW";
private static final String RECORDED = "Recorded: ";
private boolean myIsRecording;
private final ActionManagerEx myActionManager;
private ActionMacro myLastMacro;
private ActionMacro myRecordingMacro;
private ArrayList<ActionMacro> myMacros = new ArrayList<ActionMacro>();
private String myLastMacroName = null;
private boolean myIsPlaying = false;
@NonNls
private static final String ELEMENT_MACRO = "macro";
private final IdeEventQueue.EventDispatcher myKeyProcessor;
private Set<InputEvent> myLastActionInputEvent = new HashSet<InputEvent>();
private ActionMacroManager.Widget myWidget;
private String myLastTyping = "";
public ActionMacroManager(ActionManagerEx actionManagerEx) {
myActionManager = actionManagerEx;
myActionManager.addAnActionListener(new AnActionListener() {
public void beforeActionPerformed(AnAction action, DataContext dataContext, final AnActionEvent event) {
String id = myActionManager.getId(action);
if (id == null) return;
//noinspection HardCodedStringLiteral
if ("StartStopMacroRecording".equals(id)) {
myLastActionInputEvent.add(event.getInputEvent());
}
else if (myIsRecording) {
myRecordingMacro.appendAction(id);
String shortcut = null;
if (event.getInputEvent() instanceof KeyEvent) {
shortcut = KeymapUtil.getKeystrokeText(KeyStroke.getKeyStrokeForEvent((KeyEvent)event.getInputEvent()));
}
notifyUser(id + (shortcut != null ? " (" + shortcut + ")" : ""), false);
myLastActionInputEvent.add(event.getInputEvent());
}
}
public void beforeEditorTyping(char c, DataContext dataContext) {
}
public void afterActionPerformed(final AnAction action, final DataContext dataContext, final AnActionEvent event) {
}
});
myKeyProcessor = new MyKeyPostpocessor();
IdeEventQueue.getInstance().addPostprocessor(myKeyProcessor, null);
}
public void readExternal(Element element) throws InvalidDataException {
myMacros = new ArrayList<ActionMacro>();
final List macros = element.getChildren(ELEMENT_MACRO);
for (final Object o : macros) {
Element macroElement = (Element)o;
ActionMacro macro = new ActionMacro();
macro.readExternal(macroElement);
myMacros.add(macro);
}
registerActions();
}
public String getExternalFileName() {
return "macros";
}
@NotNull
public File[] getExportFiles() {
return new File[]{PathManager.getOptionsFile(this)};
}
@NotNull
public String getPresentableName() {
return IdeBundle.message("title.macros");
}
public void writeExternal(Element element) throws WriteExternalException {
for (ActionMacro macro : myMacros) {
Element macroElement = new Element(ELEMENT_MACRO);
macro.writeExternal(macroElement);
element.addContent(macroElement);
}
}
public static ActionMacroManager getInstance() {
return ApplicationManager.getApplication().getComponent(ActionMacroManager.class);
}
@NotNull
public String getComponentName() {
return "ActionMacroManager";
}
public void initComponent() {
}
public void startRecording(String macroName) {
LOG.assertTrue(!myIsRecording);
myIsRecording = true;
myRecordingMacro = new ActionMacro(macroName);
final StatusBar statusBar = WindowManager.getInstance().getIdeFrame(null).getStatusBar();
myWidget = new Widget(statusBar);
statusBar.addWidget(myWidget);
}
private class Widget implements CustomStatusBarWidget, Consumer<MouseEvent> {
private AnimatedIcon myIcon = new AnimatedIcon("Macro recording",
new Icon[]{
AllIcons.Ide.Macro.Recording_1,
AllIcons.Ide.Macro.Recording_2,
AllIcons.Ide.Macro.Recording_3,
AllIcons.Ide.Macro.Recording_4},
AllIcons.Ide.Macro.Recording_1, 1000);
private StatusBar myStatusBar;
private final WidgetPresentation myPresentation;
private JPanel myBalloonComponent;
private Balloon myBalloon;
private final JLabel myText;
private Widget(StatusBar statusBar) {
myStatusBar = statusBar;
myPresentation = new WidgetPresentation() {
@Override
public String getTooltipText() {
return "Macro is being recorded now";
}
@Override
public Consumer<MouseEvent> getClickConsumer() {
return Widget.this;
}
};
new BaseButtonBehavior(myIcon) {
@Override
protected void execute(MouseEvent e) {
showBalloon();
}
};
myBalloonComponent = new NonOpaquePanel(new BorderLayout());
final AnAction stopAction = ActionManager.getInstance().getAction("StartStopMacroRecording");
final DefaultActionGroup group = new DefaultActionGroup();
group.add(stopAction);
final ActionToolbar tb = ActionManager.getInstance().createActionToolbar(ActionPlaces.STATUS_BAR_PLACE, group, true);
tb.setMiniMode(true);
final NonOpaquePanel top = new NonOpaquePanel(new BorderLayout());
top.add(tb.getComponent(), BorderLayout.WEST);
myText = new JLabel(RECORDED + "..." + TYPING_SAMPLE, SwingConstants.LEFT);
final Dimension preferredSize = myText.getPreferredSize();
myText.setPreferredSize(preferredSize);
myText.setText("Macro recording started...");
myLastTyping = "";
top.add(myText, BorderLayout.CENTER);
myBalloonComponent.add(top, BorderLayout.CENTER);
}
private void showBalloon() {
if (myBalloon != null) {
Disposer.dispose(myBalloon);
return;
}
myBalloon = JBPopupFactory.getInstance().createBalloonBuilder(myBalloonComponent)
.setAnimationCycle(200)
.setCloseButtonEnabled(true)
.setHideOnAction(false)
.setHideOnClickOutside(false)
.setHideOnFrameResize(false)
.setHideOnKeyOutside(false)
.setSmallVariant(true)
.setShadow(true)
.createBalloon();
Disposer.register(myBalloon, new Disposable() {
@Override
public void dispose() {
myBalloon = null;
}
});
myBalloon.addListener(new JBPopupAdapter() {
@Override
public void onClosed(LightweightWindowEvent event) {
if (myBalloon != null) {
Disposer.dispose(myBalloon);
}
}
});
myBalloon.show(new PositionTracker<Balloon>(myIcon) {
@Override
public RelativePoint recalculateLocation(Balloon object) {
return new RelativePoint(myIcon, new Point(myIcon.getSize().width / 2, 4));
}
}, Balloon.Position.above);
}
@Override
public JComponent getComponent() {
return myIcon;
}
@NotNull
@Override
public String ID() {
return "MacroRecording";
}
@Override
public void consume(MouseEvent mouseEvent) {
}
@Override
public WidgetPresentation getPresentation(@NotNull PlatformType type) {
return myPresentation;
}
@Override
public void install(@NotNull StatusBar statusBar) {
showBalloon();
}
@Override
public void dispose() {
myIcon.dispose();
if (myBalloon != null) {
Disposer.dispose(myBalloon);
}
}
public void delete() {
if (myBalloon != null) {
Disposer.dispose(myBalloon);
}
myStatusBar.removeWidget(ID());
}
public void notifyUser(String text) {
myText.setText(text);
myText.revalidate();
myText.repaint();
}
}
public void stopRecording(@Nullable Project project) {
LOG.assertTrue(myIsRecording);
if (myWidget != null) {
myWidget.delete();
myWidget = null;
}
myIsRecording = false;
myLastActionInputEvent.clear();
String macroName;
do {
macroName = Messages.showInputDialog(project,
IdeBundle.message("prompt.enter.macro.name"),
IdeBundle.message("title.enter.macro.name"),
Messages.getQuestionIcon());
if (macroName == null) {
myRecordingMacro = null;
return;
}
if (macroName.isEmpty()) macroName = null;
}
while (macroName != null && !checkCanCreateMacro(macroName));
myLastMacro = myRecordingMacro;
addRecordedMacroWithName(macroName);
registerActions();
}
private void addRecordedMacroWithName(@Nullable String macroName) {
if (macroName != null) {
myRecordingMacro.setName(macroName);
myMacros.add(myRecordingMacro);
myRecordingMacro = null;
}
else {
for (int i = 0; i < myMacros.size(); i++) {
ActionMacro macro = myMacros.get(i);
if (IdeBundle.message("macro.noname").equals(macro.getName())) {
myMacros.set(i, myRecordingMacro);
myRecordingMacro = null;
break;
}
}
if (myRecordingMacro != null) {
myMacros.add(myRecordingMacro);
myRecordingMacro = null;
}
}
}
public void playbackLastMacro() {
if (myLastMacro != null) {
playbackMacro(myLastMacro);
}
}
private void playbackMacro(ActionMacro macro) {
final IdeFrame frame = WindowManager.getInstance().getIdeFrame(null);
assert frame != null;
StringBuffer script = new StringBuffer();
ActionMacro.ActionDescriptor[] actions = macro.getActions();
for (ActionMacro.ActionDescriptor each : actions) {
each.generateTo(script);
}
final PlaybackRunner runner = new PlaybackRunner(script.toString(), new PlaybackRunner.StatusCallback.Edt() {
public void messageEdt(PlaybackContext context, String text, Type type) {
if (type == Type.message || type == Type.error) {
if (context != null) {
frame.getStatusBar().setInfo("Line " + context.getCurrentLine() + ": " + text);
}
else {
frame.getStatusBar().setInfo(text);
}
}
}
}, Registry.is("actionSystem.playback.useDirectActionCall"), true, Registry.is("actionSystem.playback.useTypingTargets"));
myIsPlaying = true;
runner.run()
.doWhenDone(new Runnable() {
public void run() {
frame.getStatusBar().setInfo("Script execution finished");
}
})
.doWhenProcessed(new Runnable() {
public void run() {
myIsPlaying = false;
}
});
}
public boolean isRecording() {
return myIsRecording;
}
public void disposeComponent() {
IdeEventQueue.getInstance().removePostprocessor(myKeyProcessor);
}
public ActionMacro[] getAllMacros() {
return myMacros.toArray(new ActionMacro[myMacros.size()]);
}
public void removeAllMacros() {
if (myLastMacro != null) {
myLastMacroName = myLastMacro.getName();
myLastMacro = null;
}
myMacros = new ArrayList<ActionMacro>();
}
public void addMacro(ActionMacro macro) {
myMacros.add(macro);
if (myLastMacroName != null && myLastMacroName.equals(macro.getName())) {
myLastMacro = macro;
myLastMacroName = null;
}
}
public void playMacro(ActionMacro macro) {
playbackMacro(macro);
myLastMacro = macro;
}
public boolean hasRecentMacro() {
return myLastMacro != null;
}
public void registerActions() {
unregisterActions();
HashSet<String> registeredIds = new HashSet<String>(); // to prevent exception if 2 or more targets have the same name
ActionMacro[] macros = getAllMacros();
for (final ActionMacro macro : macros) {
String actionId = macro.getActionId();
if (!registeredIds.contains(actionId)) {
registeredIds.add(actionId);
myActionManager.registerAction(actionId, new InvokeMacroAction(macro));
}
}
}
public void unregisterActions() {
// unregister Tool actions
String[] oldIds = myActionManager.getActionIds(ActionMacro.MACRO_ACTION_PREFIX);
for (final String oldId : oldIds) {
myActionManager.unregisterAction(oldId);
}
}
public boolean checkCanCreateMacro(String name) {
final ActionManagerEx actionManager = (ActionManagerEx)ActionManager.getInstance();
final String actionId = ActionMacro.MACRO_ACTION_PREFIX + name;
if (actionManager.getAction(actionId) != null) {
if (Messages.showYesNoDialog(IdeBundle.message("message.macro.exists", name),
IdeBundle.message("title.macro.name.already.used"),
Messages.getWarningIcon()) != Messages.YES) {
return false;
}
actionManager.unregisterAction(actionId);
removeMacro(name);
}
return true;
}
private void removeMacro(String name) {
for (int i = 0; i < myMacros.size(); i++) {
ActionMacro macro = myMacros.get(i);
if (name.equals(macro.getName())) {
myMacros.remove(i);
break;
}
}
}
public boolean isPlaying() {
return myIsPlaying;
}
private static class InvokeMacroAction extends AnAction {
private final ActionMacro myMacro;
InvokeMacroAction(ActionMacro macro) {
myMacro = macro;
getTemplatePresentation().setText(macro.getName(), false);
}
public void actionPerformed(AnActionEvent e) {
IdeEventQueue.getInstance().doWhenReady(new Runnable() {
@Override
public void run() {
getInstance().playMacro(myMacro);
}
});
}
public void update(AnActionEvent e) {
super.update(e);
e.getPresentation().setEnabled(!getInstance().isPlaying());
}
}
private class MyKeyPostpocessor implements IdeEventQueue.EventDispatcher {
public boolean dispatch(AWTEvent e) {
if (isRecording() && e instanceof KeyEvent) {
postProcessKeyEvent((KeyEvent)e);
}
return false;
}
public void postProcessKeyEvent(KeyEvent e) {
if (e.getID() != KeyEvent.KEY_PRESSED) return;
if (myLastActionInputEvent.contains(e)) {
myLastActionInputEvent.remove(e);
return;
}
final boolean modifierKeyIsPressed = e.getKeyCode() == KeyEvent.VK_CONTROL ||
e.getKeyCode() == KeyEvent.VK_ALT ||
e.getKeyCode() == KeyEvent.VK_META ||
e.getKeyCode() == KeyEvent.VK_SHIFT;
if (modifierKeyIsPressed) return;
final boolean ready = IdeEventQueue.getInstance().getKeyEventDispatcher().isReady();
final boolean isChar = e.getKeyChar() != KeyEvent.CHAR_UNDEFINED && UIUtil.isReallyTypedEvent(e);
final boolean hasActionModifiers = e.isAltDown() | e.isControlDown() | e.isMetaDown();
final boolean plainType = isChar && !hasActionModifiers;
final boolean isEnter = e.getKeyCode() == KeyEvent.VK_ENTER;
if (plainType && ready && !isEnter) {
myRecordingMacro.appendKeytyped(e.getKeyChar(), e.getKeyCode(), e.getModifiers());
notifyUser(Character.valueOf(e.getKeyChar()).toString(), true);
}
else if ((!plainType && ready) || isEnter) {
final String stroke = KeyStroke.getKeyStrokeForEvent(e).toString();
final int pressed = stroke.indexOf("pressed");
String key = stroke.substring(pressed + "pressed".length());
String modifiers = stroke.substring(0, pressed);
String shortcut = (modifiers.replaceAll("ctrl", "control").trim() + " " + key.trim()).trim();
myRecordingMacro.appendShortcut(shortcut);
notifyUser(KeymapUtil.getKeystrokeText(KeyStroke.getKeyStrokeForEvent(e)), false);
}
}
}
private void notifyUser(String text, boolean typing) {
String actualText = text;
if (typing) {
int maxLength = TYPING_SAMPLE.length();
myLastTyping += text;
if (myLastTyping.length() > maxLength) {
myLastTyping = "..." + myLastTyping.substring(myLastTyping.length() - maxLength);
}
actualText = myLastTyping;
} else {
myLastTyping = "";
}
if (myWidget != null) {
myWidget.notifyUser(RECORDED + actualText);
}
}
}