com.intellij.ide.actionMacro.ActionMacroManager.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.ide.actionMacro.ActionMacroManager.java

Source

/*
 * Copyright 2000-2009 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()) != 0) {
                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);
        }
    }
}