blob: 20c2a7ae110c507edcf7a64ba70d6633dabb8701 [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.ui.debugger.extensions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileElement;
import com.intellij.openapi.fileChooser.ex.FileChooserKeys;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.playback.PlaybackContext;
import com.intellij.openapi.ui.playback.PlaybackRunner;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.encoding.EncodingRegistry;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.openapi.wm.impl.IdeFrameImpl;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.debugger.UiDebuggerExtension;
import com.intellij.util.WaitFor;
import com.intellij.util.ui.PlatformColors;
import com.intellij.util.ui.UIUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
public class PlaybackDebugger implements UiDebuggerExtension, PlaybackRunner.StatusCallback {
private static final Logger LOG = Logger.getInstance("#com.intellij.ui.debugger.extensions.PlaybackDebugger");
private static final Color ERROR_COLOR = JBColor.RED;
private static final Color MESSAGE_COLOR = Color.BLACK;
private static final Color CODE_COLOR = PlatformColors.BLUE;
private static final Color TEST_COLOR = JBColor.GREEN.darker();
private JPanel myComponent;
private PlaybackRunner myRunner;
private JEditorPane myLog;
private final JTextField myScriptsPath = new JTextField();
private static final String EXT = "ijs";
private static final String DOT_EXT = "." + EXT;
private final JTextField myCurrentScript = new JTextField();
private VirtualFileAdapter myVfsListener;
private boolean myChanged;
private PlaybackDebuggerState myState;
private static final FileChooserDescriptor FILE_DESCRIPTOR = new ScriptFileChooserDescriptor();
private JTextArea myCodeEditor;
private void initUi() {
myComponent = new JPanel(new BorderLayout());
myLog = new JEditorPane();
myLog.setEditorKit(new StyledEditorKit());
myLog.setEditable(false);
myState = ServiceManager.getService(PlaybackDebuggerState.class);
final DefaultActionGroup controlGroup = new DefaultActionGroup();
controlGroup.add(new RunOnFameActivationAction());
controlGroup.add(new ActivateFrameAndRun());
controlGroup.add(new StopAction());
JPanel north = new JPanel(new BorderLayout());
north.add(ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, controlGroup, true).getComponent(), BorderLayout.WEST);
final JPanel right = new JPanel(new BorderLayout());
right.add(myCurrentScript, BorderLayout.CENTER);
myCurrentScript.setText(myState.currentScript);
myCurrentScript.setEditable(false);
final DefaultActionGroup fsGroup = new DefaultActionGroup();
SaveAction saveAction = new SaveAction();
saveAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke("control S")), myComponent);
fsGroup.add(saveAction);
SetScriptFileAction setScriptFileAction = new SetScriptFileAction();
setScriptFileAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke("control O")), myComponent);
fsGroup.add(setScriptFileAction);
AnAction newScriptAction = new NewScriptAction();
newScriptAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke("control N")), myComponent);
fsGroup.add(newScriptAction);
final ActionToolbar tb = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, fsGroup, true);
tb.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY);
right.add(tb.getComponent(), BorderLayout.EAST);
north.add(right, BorderLayout.CENTER);
myComponent.add(north, BorderLayout.NORTH);
myCodeEditor = new JTextArea();
myCodeEditor.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
myChanged = true;
}
@Override
public void removeUpdate(DocumentEvent e) {
myChanged = true;
}
@Override
public void changedUpdate(DocumentEvent e) {
myChanged = true;
}
});
if (pathToFile() != null) {
loadFrom(pathToFile());
}
final Splitter script2Log = new Splitter(true);
script2Log.setFirstComponent(ScrollPaneFactory.createScrollPane(myCodeEditor));
script2Log.setSecondComponent(ScrollPaneFactory.createScrollPane(myLog));
myComponent.add(script2Log, BorderLayout.CENTER);
myVfsListener = new VirtualFileAdapter() {
@Override
public void contentsChanged(@NotNull VirtualFileEvent event) {
final VirtualFile file = pathToFile();
if (file != null && file.equals(event.getFile())) {
loadFrom(event.getFile());
}
}
};
LocalFileSystem.getInstance().addVirtualFileListener(myVfsListener);
}
private class SaveAction extends AnAction {
private SaveAction() {
super("Save", "", AllIcons.Actions.Menu_saveall);
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myChanged);
}
@Override
public void actionPerformed(AnActionEvent e) {
if (pathToFile() == null) {
VirtualFile selectedFile = FileChooser.chooseFile(FILE_DESCRIPTOR, myComponent, getEventProject(e), null);
if (selectedFile != null) {
myState.currentScript = selectedFile.getPresentableUrl();
myCurrentScript.setText(myState.currentScript);
}
else {
Messages.showErrorDialog("File to save is not selected.", "Cannot save script");
return;
}
}
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
save();
}
});
}
}
private static class ScriptFileChooserDescriptor extends FileChooserDescriptor {
public ScriptFileChooserDescriptor() {
super(true, false, false, false, false, false);
putUserData(FileChooserKeys.NEW_FILE_TYPE, UiScriptFileType.getInstance());
putUserData(FileChooserKeys.NEW_FILE_TEMPLATE_TEXT, "");
}
@Override
public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
if (!showHiddenFiles && FileElement.isFileHidden(file)) return false;
return file.getExtension() != null && file.getExtension().equalsIgnoreCase(UiScriptFileType.myExtension)
|| super.isFileVisible(file, showHiddenFiles) && file.isDirectory();
}
}
private class SetScriptFileAction extends AnAction {
private SetScriptFileAction() {
super("Set Script File", "", AllIcons.Actions.Menu_open);
}
@Override
public void actionPerformed(AnActionEvent e) {
VirtualFile selectedFile = FileChooser.chooseFile(FILE_DESCRIPTOR, myComponent, getEventProject(e), pathToFile());
if (selectedFile != null) {
myState.currentScript = selectedFile.getPresentableUrl();
loadFrom(selectedFile);
myCurrentScript.setText(myState.currentScript);
}
}
}
private class NewScriptAction extends AnAction {
private NewScriptAction() {
super("New Script", "", AllIcons.Actions.New);
}
@Override
public void actionPerformed(AnActionEvent e) {
myState.currentScript = "";
myCurrentScript.setText(myState.currentScript);
fillDocument("");
}
}
private void fillDocument(final String text) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
myCodeEditor.setText(text == null ? "" : text);
}
});
}
@Nullable
private VirtualFile pathToFile() {
if (myState.currentScript.length() == 0) {
return null;
}
return LocalFileSystem.getInstance().findFileByPath(myState.currentScript);
}
private void save() {
try {
VirtualFile file = pathToFile();
final String toWrite = myCodeEditor.getText();
String text = toWrite != null ? toWrite : "";
VfsUtil.saveText(file, text);
myChanged = false;
}
catch (IOException e) {
Messages.showErrorDialog(e.getMessage(), "Cannot save script");
}
}
private void loadFrom(@NotNull VirtualFile file) {
try {
final String text = CharsetToolkit.bytesToString(file.contentsToByteArray(), EncodingRegistry.getInstance().getDefaultCharset());
fillDocument(text);
myChanged = false;
}
catch (IOException e) {
Messages.showErrorDialog(e.getMessage(), "Cannot load file");
}
}
private File getScriptsFile() {
final String text = myScriptsPath.getText();
if (text == null) return null;
final File file = new File(text);
return file.exists() ? file : null;
}
private class StopAction extends AnAction {
private StopAction() {
super("Stop", null, AllIcons.Actions.Suspend);
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myRunner != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
if (myRunner != null) {
myRunner.stop();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myRunner = null;
}
});
}
}
}
private class ActivateFrameAndRun extends AnAction {
private ActivateFrameAndRun() {
super("Activate Frame And Run", "", AllIcons.Nodes.Deploy);
}
@Override
public void actionPerformed(AnActionEvent e) {
activateAndRun();
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myRunner == null);
}
}
private class RunOnFameActivationAction extends AnAction {
private RunOnFameActivationAction() {
super("Run On Frame Activation", "", AllIcons.General.Run);
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myRunner == null);
}
@Override
public void actionPerformed(AnActionEvent e) {
runOnFrame();
}
}
private void activateAndRun() {
assert myRunner == null;
myLog.setText(null);
final IdeFrameImpl frame = getFrame();
final Component c = ((WindowManagerEx)WindowManager.getInstance()).getFocusedComponent(frame);
if (c != null) {
c.requestFocus();
} else {
frame.requestFocus();
}
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
startWhenFrameActive();
}
});
}
private IdeFrameImpl getFrame() {
final Frame[] all = Frame.getFrames();
for (Frame each : all) {
if (each instanceof IdeFrame) {
return (IdeFrameImpl)each;
}
}
throw new IllegalStateException("Cannot find IdeFrame to run on");
}
private void runOnFrame() {
assert myRunner == null;
startWhenFrameActive();
}
private void startWhenFrameActive() {
myLog.setText(null);
addInfo("Waiting for IDE frame activation", -1, MESSAGE_COLOR, 0);
myRunner = new PlaybackRunner(myCodeEditor.getText(), this, false, true, false);
VirtualFile file = pathToFile();
if (file != null) {
VirtualFile scriptDir = file.getParent();
if (scriptDir != null) {
myRunner.setScriptDir(new File(scriptDir.getPresentableUrl()));
}
}
new Thread() {
@Override
public void run() {
new WaitFor(60000) {
@Override
protected boolean condition() {
return KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() instanceof IdeFrame || myRunner == null;
}
};
if (myRunner == null) {
message(null, "Script stopped", -1, Type.message, true);
return;
}
message(null, "Starting script...", -1, Type.message, true);
try {
sleep(1000);
}
catch (InterruptedException e) {}
if (myRunner == null) {
message(null, "Script stopped", -1, Type.message, true);
return;
}
final PlaybackRunner runner = myRunner;
myRunner.run().doWhenProcessed(new Runnable() {
@Override
public void run() {
if (runner == myRunner) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myRunner = null;
}
});
}
}
});
}
}.start();
}
@Override
public void message(@Nullable final PlaybackContext context, final String text, final Type type) {
message(context, text, context != null ? context.getCurrentLine() : -1, type, false);
}
private void message(@Nullable final PlaybackContext context, final String text, final int currentLine, final Type type, final boolean forced) {
final int depth = context != null ? context.getCurrentStageDepth() : 0;
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (!forced && (context != null && context.isDisposed())) return;
switch (type) {
case message:
addInfo(text, currentLine, MESSAGE_COLOR, depth);
break;
case error:
addInfo(text, currentLine, ERROR_COLOR, depth);
break;
case code:
addInfo(text, currentLine, CODE_COLOR, depth);
break;
case test:
addInfo(text, currentLine, TEST_COLOR, depth);
break;
}
}
});
}
@Override
public JComponent getComponent() {
if (myComponent == null) {
initUi();
}
return myComponent;
}
@Override
public String getName() {
return "Playback";
}
public void dispose() {
disposeUiResources();
}
@State(
name = "PlaybackDebugger",
storages = {
@Storage(
file = StoragePathMacros.APP_CONFIG + "/other.xml")}
)
public static class PlaybackDebuggerState implements PersistentStateComponent<Element> {
private static final String ATTR_CURRENT_SCRIPT = "currentScript";
public String currentScript = "";
@Override
public Element getState() {
final Element element = new Element("playback");
element.setAttribute(ATTR_CURRENT_SCRIPT, currentScript);
return element;
}
@Override
public void loadState(Element state) {
final String path = state.getAttributeValue(ATTR_CURRENT_SCRIPT);
if (path != null) {
currentScript = path;
}
}
}
@Override
public void disposeUiResources() {
myComponent = null;
LocalFileSystem.getInstance().removeVirtualFileListener(myVfsListener);
myCurrentScript.setText("");
myLog.setText(null);
}
private void addInfo(String text, int line, Color fg, int depth) {
if (text == null || text.length() == 0) return;
String inset = StringUtil.repeat(" ", depth);
Document doc = myLog.getDocument();
SimpleAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setFontFamily(attr, UIManager.getFont("Label.font").getFontName());
StyleConstants.setFontSize(attr, UIManager.getFont("Label.font").getSize());
StyleConstants.setForeground(attr, fg);
try {
doc.insertString(doc.getLength(), inset + text + "\n", attr);
}
catch (BadLocationException e) {
LOG.error(e);
}
scrollToLast();
}
private void scrollToLast() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (myLog.getDocument().getLength() == 0) return;
Rectangle bounds = myLog.getBounds();
myLog.scrollRectToVisible(new Rectangle(0, (int)bounds.getMaxY() - 1, (int)bounds.getWidth(), 1));
}
});
}
}