package org.netbeans.modules.ruby.rubyproject;
import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.PipedInputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import javax.swing.BorderFactory;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.JTextComponent;
import org.jruby.Ruby;
import org.jruby.RubyInstanceConfig;
import org.jruby.demo.TextAreaReadline;
import org.jruby.internal.runtime.ValueAccessor;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.openide.ErrorManager;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
/**
* IRB window.
* This class is heavily based on IRBConsole in the JRuby distribution,
* but changed since IRBConsole extends from JFrame and we want to extend
* TopComponent (which is a JPanel).
*
* @todo Use the equivalent of "jirb -rirb/completion" to get autocompletion?
* (include "irb/completion"). See http://jira.codehaus.org/browse/JRUBY-389?page=all
* @todo It might be interesting to set the mime type of the embedded
* text pane to Ruby, and see if syntax highlighting works. Might
* need some tweaks, e.g. a derived mode for shell ruby.
* Also, if the TextAreaReadline messes with attributes in the StyledDocument,
* we're hosed. The NetBeans editor GuardedDocument implementation does not like that.
* @todo Use output2's APIs: AbstractOutputTab - it has a lot of good
* logic for keeping the pane scrolled to track output, locking the caret on the
* last line, etc.
*/
final class IrbTopComponent extends TopComponent {
private boolean finished = true;
private JTextPane text;
private static IrbTopComponent instance;
/** path to the icon used by the component and its open action */
static final String ICON_PATH = "org/netbeans/modules/ruby/rubyproject/jruby.png"; // NOI18N
private static final String PREFERRED_ID = "IrbTopComponent"; // NOI18N
private IrbTopComponent() {
initComponents();
setName(NbBundle.getMessage(IrbTopComponent.class, "CTL_IrbTopComponent"));
setToolTipText(NbBundle.getMessage(IrbTopComponent.class, "HINT_IrbTopComponent"));
setIcon(ImageUtilities.loadImage(ICON_PATH, true));
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:initComponents
private void initComponents() {
setLayout(new java.awt.BorderLayout());
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
/**
* Gets default instance. Do not use directly: reserved for *.settings files only,
* i.e. deserialization routines; otherwise you could get a non-deserialized instance.
* To obtain the singleton instance, use {@link findInstance}.
*/
public static synchronized IrbTopComponent getDefault() {
if (instance == null) {
instance = new IrbTopComponent();
}
return instance;
}
/**
* Obtain the IrbTopComponent instance. Never call {@link #getDefault} directly!
*/
public static synchronized IrbTopComponent findInstance() {
TopComponent win = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (win == null) {
// Internal error message - NOI18N
ErrorManager.getDefault().log(ErrorManager.WARNING,
"Cannot find MyWindow component. It will not be located properly in the window system."); // NOI18N
return getDefault();
}
if (win instanceof IrbTopComponent) {
return (IrbTopComponent)win;
}
// Internal error message - NOI18N
ErrorManager.getDefault().log(ErrorManager.WARNING,
"There seem to be multiple components with the '" + PREFERRED_ID + // NOI18N
"' ID. That is a potential source of errors and unexpected behavior."); // NOI18N
return getDefault();
}
public @Override int getPersistenceType() {
return TopComponent.PERSISTENCE_ALWAYS;
}
public @Override void componentOpened() {
if (finished) {
// Start a new one
finished = false;
removeAll();
createTerminal();
}
}
public @Override void componentClosed() {
// Leave the terminal session running
}
@Override
public void componentActivated() {
// Make the caret visible. See comment under componentDeactivated.
if (text != null) {
Caret caret = text.getCaret();
if (caret != null) {
caret.setVisible(true);
}
}
}
@Override
public void componentDeactivated() {
// I have to turn off the caret when the window loses focus. Text components
// normally do this by themselves, but the TextAreaReadline component seems
// to mess around with the editable property of the text pane, and
// the caret will not turn itself on/off for noneditable text areas.
if (text != null) {
Caret caret = text.getCaret();
if (caret != null) {
caret.setVisible(false);
}
}
}
/** replaces this in object stream */
public @Override Object writeReplace() {
return new ResolvableHelper();
}
protected @Override String preferredID() {
return PREFERRED_ID;
}
final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Object readResolve() {
return IrbTopComponent.getDefault();
}
}
public void createTerminal() {
text = new JTextPane();
text.setMargin(new Insets(8,8,8,8));
text.setCaretColor(new Color(0xa4, 0x00, 0x00));
text.setBackground(new Color(0xf2, 0xf2, 0xf2));
text.setForeground(new Color(0xa4, 0x00, 0x00));
// From core/output2/**/AbstractOutputPane
Integer i = (Integer) UIManager.get("customFontSize"); //NOI18N
int size;
if (i != null) {
size = i.intValue();
} else {
Font f = (Font) UIManager.get("controlFont"); // NOI18N
size = f != null ? f.getSize() : 11;
}
text.setFont(new Font ("Monospaced", Font.PLAIN, size)); //NOI18N
setBorder (BorderFactory.createEmptyBorder());
// Try to initialize colors from NetBeans properties, see core/output2
Color c = UIManager.getColor("nb.output.selectionBackground"); // NOI18N
if (c != null) {
text.setSelectionColor(c);
}
//Object value = Settings.getValue(BaseKit.class, SettingsNames.CARET_COLOR_INSERT_MODE);
//Color caretColor;
//if (value instanceof Color) {
// caretColor = (Color)value;
//} else {
// caretColor = SettingsDefaults.defaultCaretColorInsertMode;
//}
//text.setCaretColor(caretColor);
//text.setBackground(UIManager.getColor("text")); //NOI18N
//Color selectedFg = UIManager.getColor ("nb.output.foreground.selected"); //NOI18N
//if (selectedFg == null) {
// selectedFg = UIManager.getColor("textText") == null ? Color.BLACK : //NOI18N
// UIManager.getColor("textText"); //NOI18N
//}
//
//Color unselectedFg = UIManager.getColor ("nb.output.foreground"); //NOI18N
//if (unselectedFg == null) {
// unselectedFg = selectedFg;
//}
//text.setForeground(unselectedFg);
//text.setSelectedTextColor(selectedFg);
//
//Color selectedErr = UIManager.getColor ("nb.output.err.foreground.selected"); //NOI18N
//if (selectedErr == null) {
// selectedErr = new Color (164, 0, 0);
//}
//Color unselectedErr = UIManager.getColor ("nb.output.err.foreground"); //NOI18N
//if (unselectedErr == null) {
// unselectedErr = selectedErr;
//}
JScrollPane pane = new JScrollPane();
pane.setViewportView(text);
pane.setBorder(BorderFactory.createLineBorder(Color.darkGray));
add(pane);
validate();
final Ruby runtime = getRuntime(text);
RequestProcessor.Task task = RequestProcessor.getDefault().create(new Runnable() {
//RequestProcessor.getDefault().post(new Runnable() {
@Override
public void run() {
startIRB(runtime);
}
});
task.addTaskListener(new TaskListener() {
@Override
public void taskFinished(Task task) {
finished = true;
//tar.writeMessage(" " + NbBundle.getMessage(IrbTopComponent.class, "IrbGoodbye") + " "); // NOI18N
text.setEditable(false);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
IrbTopComponent.this.close();
IrbTopComponent.this.removeAll();
text = null;
}
});
}
});
task.schedule(10);
// [Issue 91208] avoid of putting cursor in IRB console on line where is not a prompt
text.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent ev) {
final int mouseX = ev.getX();
final int mouseY = ev.getY();
// Ensure that this is done after the textpane's own mouse listener
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// Attempt to force the mouse click to appear on the last line of the text input
int pos = text.getDocument().getEndPosition().getOffset()-1;
if (pos == -1) {
return;
}
try {
Rectangle r = text.modelToView(pos);
if (mouseY >= r.y) {
// The click was on the last line; try to set the X to the position where
// the user clicked since perhaps it was an attempt to edit the existing
// input string. Later I could perhaps cast the text document to a StyledDocument,
// then iterate through the document positions and locate the end of the
// input prompt (by comparing to the promptStyle in TextAreaReadline).
r.x = mouseX;
pos = text.viewToModel(r.getLocation());
}
text.getCaret().setDot(pos);
} catch (BadLocationException ble) {
// do nothing - see #154991
}
}
});
}
});
}
// package-private for unit-test only
static Ruby getRuntime(final JTextComponent text) {
final TextAreaReadline tar = new TextAreaReadline(text,
" " + NbBundle.getMessage(IrbTopComponent.class, "IrbWelcome") + " \n\n"); // NOI18N
// Ensure that ClassPath can find libraries etc.
RubyInstallation.getInstance().setJRubyLoadPaths();
final PipedInputStream pipeIn = new PipedInputStream();
final RubyInstanceConfig config = new RubyInstanceConfig() {{
setInput(pipeIn);
setOutput(new PrintStream(tar.getOutputStream()));
setError(new PrintStream(tar.getOutputStream()));
setObjectSpaceEnabled(false);
//setArgv(args);
}};
final Ruby runtime = Ruby.newInstance(config);
runtime.getGlobalVariables().defineReadonly("$$", new ValueAccessor(runtime.newFixnum(System.identityHashCode(runtime))));
runtime.getLoadService().init(new ArrayList());
tar.hookIntoRuntime(runtime);
return runtime;
}
private static void startIRB(final Ruby runtime) {
runtime.evalScriptlet("require 'irb'; require 'irb/completion'; IRB.start"); // NOI18N
}
@Override
public void requestFocus() {
if (text != null) {
text.requestFocus();
}
}
@Override
public boolean requestFocusInWindow() {
if (text != null) {
return text.requestFocusInWindow();
}
return false;
}
}