| /* |
| * 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)); |
| } |
| }); |
| } |
| |
| } |