package com.jediterm.terminal.ui;
import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import com.jediterm.terminal.*;
import com.jediterm.terminal.TextStyle.Option;
import com.jediterm.terminal.display.*;
import com.jediterm.terminal.emulator.ColorPalette;
import com.jediterm.terminal.emulator.charset.CharacterSets;
import com.jediterm.terminal.emulator.mouse.MouseMode;
import com.jediterm.terminal.emulator.mouse.TerminalMouseListener;
import com.jediterm.terminal.ui.settings.SettingsProvider;
import com.jediterm.terminal.util.Pair;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodRequests;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.util.List;
public class TerminalPanel extends JComponent implements TerminalDisplay, ClipboardOwner, StyledTextConsumer, TerminalActionProvider {
private static final Logger LOG = Logger.getLogger(TerminalPanel.class);
private static final long serialVersionUID = -1048763516632093014L;
private static final double FPS = 50;
public static final double SCROLL_SPEED = 0.05;
/*images*/
private BufferedImage myImage;
protected Graphics2D myGfx;
private BufferedImage myImageForSelection;
private Graphics2D myGfxForSelection;
private BufferedImage myImageForCursor;
private Graphics2D myGfxForCursor;
private final Component myTerminalPanel = this;
/*font related*/
private Font myNormalFont;
private Font myItalicFont;
private Font myBoldFont;
private Font myBoldItalicFont;
private int myDescent = 0;
protected Dimension myCharSize = new Dimension();
private boolean myMonospaced;
protected Dimension myTermSize = new Dimension(80, 24);
private TerminalStarter myTerminalStarter = null;
private MouseMode myMouseMode = MouseMode.MOUSE_REPORTING_NONE;
private Point mySelectionStartPoint = null;
private TerminalSelection mySelection = null;
private Clipboard myClipboard;
private TerminalPanelListener myTerminalPanelListener;
private SettingsProvider mySettingsProvider;
final private BackBuffer myBackBuffer;
final private StyleState myStyleState;
/*scroll and cursor*/
final private TerminalCursor myCursor = new TerminalCursor();
private final BoundedRangeModel myBoundedRangeModel = new DefaultBoundedRangeModel(0, 80, 0, 80);
protected int myClientScrollOrigin;
protected int newClientScrollOrigin;
protected KeyListener myKeyListener;
private long myLastCursorChange;
private boolean myCursorIsShown;
private long myLastResize;
private boolean myScrollingEnabled = true;
private String myWindowTitle = "Terminal";
private TerminalActionProvider myNextActionProvider;
private String myInputMethodUncommitedChars;
public TerminalPanel(@NotNull SettingsProvider settingsProvider, @NotNull BackBuffer backBuffer, @NotNull StyleState styleState) {
mySettingsProvider = settingsProvider;
myBackBuffer = backBuffer;
myStyleState = styleState;
myTermSize.width = backBuffer.getWidth();
myTermSize.height = backBuffer.getHeight();
updateScrolling();
enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK);
enableInputMethods(true);
}
public void init() {
myNormalFont = createFont();
myBoldFont = myNormalFont.deriveFont(Font.BOLD);
myItalicFont = myNormalFont.deriveFont(Font.ITALIC);
myBoldItalicFont = myBoldFont.deriveFont(Font.ITALIC);
establishFontMetrics();
setupImages();
setUpClipboard();
setPreferredSize(new Dimension(getPixelWidth(), getPixelHeight()));
setFocusable(true);
enableInputMethods(true);
setFocusTraversalKeysEnabled(false);
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(final MouseEvent e) {
if (isMouseReporting()) {
return;
}
final Point charCoords = panelToCharCoords(e.getPoint());
if (mySelection == null) {
// prevent unlikely case where drag started outside terminal panel
if (mySelectionStartPoint == null) {
mySelectionStartPoint = charCoords;
}
mySelection = new TerminalSelection(new Point(mySelectionStartPoint));
}
repaint();
mySelection.updateEnd(charCoords);
if (mySettingsProvider.copyOnSelect()) {
handleCopy(false);
}
if (e.getPoint().y < 0) {
moveScrollBar((int)((e.getPoint().y) * SCROLL_SPEED));
}
if (e.getPoint().y > getPixelHeight()) {
moveScrollBar((int)((e.getPoint().y - getPixelHeight()) * SCROLL_SPEED));
}
}
});
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
moveScrollBar(notches);
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
if (e.getClickCount() == 1) {
mySelectionStartPoint = panelToCharCoords(e.getPoint());
mySelection = null;
repaint();
}
}
}
@Override
public void mouseReleased(final MouseEvent e) {
requestFocusInWindow();
repaint();
}
@Override
public void mouseClicked(final MouseEvent e) {
requestFocusInWindow();
if (e.getButton() == MouseEvent.BUTTON1) {
int count = e.getClickCount();
if (count == 1) {
// do nothing
}
else if (count == 2 && !isMouseReporting()) {
// select word
final Point charCoords = panelToCharCoords(e.getPoint());
Point start = SelectionUtil.getPreviousSeparator(charCoords, myBackBuffer);
Point stop = SelectionUtil.getNextSeparator(charCoords, myBackBuffer);
mySelection = new TerminalSelection(start);
mySelection.updateEnd(stop);
if (mySettingsProvider.copyOnSelect()) {
handleCopy(false);
}
}
else if (count == 3 && !isMouseReporting()) {
// select line
final Point charCoords = panelToCharCoords(e.getPoint());
int startLine = charCoords.y;
while (startLine > -getScrollBuffer().getLineCount()
&& myBackBuffer.getLine(startLine - 1).isWrapped()) {
startLine--;
}
int endLine = charCoords.y;
while (endLine < myBackBuffer.getHeight()
&& myBackBuffer.getLine(endLine).isWrapped()) {
endLine++;
}
mySelection = new TerminalSelection(new Point(0, startLine));
mySelection.updateEnd(new Point(myTermSize.width, endLine));
if (mySettingsProvider.copyOnSelect()) {
handleCopy(false);
}
}
}
else if (e.getButton() == MouseEvent.BUTTON2 && mySettingsProvider.pasteOnMiddleMouseClick() && !isMouseReporting()) {
handlePaste();
}
else if (e.getButton() == MouseEvent.BUTTON3) {
JPopupMenu popup = createPopupMenu();
popup.show(e.getComponent(), e.getX(), e.getY());
}
repaint();
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(final ComponentEvent e) {
myLastResize = System.currentTimeMillis();
sizeTerminalFromComponent();
}
});
myBoundedRangeModel.addChangeListener(new ChangeListener() {
public void stateChanged(final ChangeEvent e) {
newClientScrollOrigin = myBoundedRangeModel.getValue();
}
});
Timer redrawTimer = new Timer((int)(1000 / FPS), new WeakRedrawTimer(this));
setDoubleBuffered(true);
redrawTimer.start();
repaint();
}
protected boolean isRetina() {
return UIUtil.isRetina();
}
static class WeakRedrawTimer implements ActionListener {
private WeakReference<TerminalPanel> ref;
public WeakRedrawTimer(TerminalPanel terminalPanel) {
this.ref = new WeakReference<TerminalPanel>(terminalPanel);
}
@Override
public void actionPerformed(ActionEvent e) {
TerminalPanel terminalPanel = ref.get();
if (terminalPanel != null) {
try {
terminalPanel.redraw();
}
catch (Exception ex) {
LOG.error("Error while terminal panel redraw", ex);
}
}
else { // terminalPanel was garbage collected
Timer timer = (Timer)e.getSource();
timer.removeActionListener(this);
timer.stop();
}
}
}
@Override
public void terminalMouseModeSet(MouseMode mode) {
myMouseMode = mode;
}
private boolean isMouseReporting() {
return myMouseMode != MouseMode.MOUSE_REPORTING_NONE;
}
private void scrollToBottom() {
myBoundedRangeModel.setValue(myTermSize.height);
}
private void moveScrollBar(int k) {
myBoundedRangeModel.setValue(myBoundedRangeModel.getValue() + k);
}
protected Font createFont() {
return mySettingsProvider.getTerminalFont();
}
protected Point panelToCharCoords(final Point p) {
int x = Math.min(p.x / myCharSize.width, getColumnCount() - 1);
x = Math.max(0, x);
int y = Math.min(p.y / myCharSize.height, getRowCount() - 1) + myClientScrollOrigin;
return new Point(x, y);
}
protected Point charToPanelCoords(final Point p) {
return new Point(p.x * myCharSize.width, (p.y - myClientScrollOrigin) * myCharSize.height);
}
void setUpClipboard() {
myClipboard = Toolkit.getDefaultToolkit().getSystemSelection();
if (myClipboard == null) {
myClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
}
}
protected void copySelection(final Point selectionStart, final Point selectionEnd) {
if (selectionStart == null || selectionEnd == null) {
return;
}
final String selectionText = SelectionUtil
.getSelectionText(selectionStart, selectionEnd, myBackBuffer);
if (selectionText.length() != 0) {
try {
setCopyContents(new StringSelection(selectionText));
}
catch (final IllegalStateException e) {
LOG.error("Could not set clipboard:", e);
}
}
}
protected void setCopyContents(StringSelection selection) {
myClipboard.setContents(selection, this);
}
protected void pasteSelection() {
final String selection = getClipboardString();
if (selection == null) {
return;
}
try {
myTerminalStarter.sendString(selection);
}
catch (RuntimeException e) {
LOG.info(e);
}
}
private String getClipboardString() {
try {
return getClipboardContent();
}
catch (final Exception e) {
LOG.info(e);
}
return null;
}
protected String getClipboardContent() throws IOException, UnsupportedFlavorException {
try {
return (String)myClipboard.getData(DataFlavor.stringFlavor);
}
catch (Exception e) {
LOG.info(e);
return null;
}
}
/* Do not care
*/
public void lostOwnership(final Clipboard clipboard, final Transferable contents) {
}
private void setupImages() {
final BufferedImage oldImage = myImage;
int width = getPixelWidth();
int height = getPixelHeight();
if (width > 0 && height > 0) {
Pair<BufferedImage, Graphics2D> imageAndGfx = createAndInitImage(width, height);
myImage = imageAndGfx.first;
myGfx = imageAndGfx.second;
imageAndGfx = createAndInitImage(width, height);
myImageForSelection = imageAndGfx.first;
myGfxForSelection = imageAndGfx.second;
imageAndGfx = createAndInitImage(width, height);
myImageForCursor = imageAndGfx.first;
myGfxForCursor = imageAndGfx.second;
if (oldImage != null) {
drawImage(myGfx, oldImage);
}
}
}
private void drawImage(Graphics2D gfx, BufferedImage image) {
drawImage(gfx, image, 0, 0, myTerminalPanel);
}
protected void drawImage(Graphics2D gfx, BufferedImage image, int x, int y, ImageObserver observer) {
gfx.drawImage(image, x, y,
image.getWidth(), image.getHeight(), observer);
}
private Pair<BufferedImage, Graphics2D> createAndInitImage(int width, int height) {
BufferedImage image = createBufferedImage(width, height);
Graphics2D gfx = image.createGraphics();
setupAntialiasing(gfx);
gfx.setColor(getBackground());
gfx.fillRect(0, 0, width, height);
return Pair.create(image, gfx);
}
protected BufferedImage createBufferedImage(int width, int height) {
return new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
}
private void sizeTerminalFromComponent() {
if (myTerminalStarter != null) {
final int newWidth = getWidth() / myCharSize.width;
final int newHeight = getHeight() / myCharSize.height;
if (newHeight > 0 && newWidth > 0) {
final Dimension newSize = new Dimension(newWidth, newHeight);
myTerminalStarter.postResize(newSize, RequestOrigin.User);
}
}
}
public void setTerminalStarter(final TerminalStarter terminalStarter) {
myTerminalStarter = terminalStarter;
sizeTerminalFromComponent();
}
public void setKeyListener(final KeyListener keyListener) {
this.myKeyListener = keyListener;
}
public Dimension requestResize(final Dimension newSize,
final RequestOrigin origin,
int cursorY,
JediTerminal.ResizeHandler resizeHandler) {
if (!newSize.equals(myTermSize)) {
myBackBuffer.lock();
try {
myBackBuffer.resize(newSize, origin, cursorY, resizeHandler, mySelection);
myTermSize = (Dimension)newSize.clone();
// resize images..
setupImages();
final Dimension pixelDimension = new Dimension(getPixelWidth(), getPixelHeight());
setPreferredSize(pixelDimension);
if (myTerminalPanelListener != null) {
myTerminalPanelListener.onPanelResize(pixelDimension, origin);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateScrolling();
}
});
}
finally {
myBackBuffer.unlock();
}
}
return new Dimension(getPixelWidth(), getPixelHeight());
}
public void setTerminalPanelListener(final TerminalPanelListener resizeDelegate) {
myTerminalPanelListener = resizeDelegate;
}
private void establishFontMetrics() {
final BufferedImage img = createBufferedImage(1, 1);
final Graphics2D graphics = img.createGraphics();
setupAntialiasing(graphics);
graphics.setFont(myNormalFont);
final float lineSpace = mySettingsProvider.getLineSpace();
final FontMetrics fo = graphics.getFontMetrics();
myDescent = fo.getDescent();
myCharSize.width = fo.charWidth('W');
myCharSize.height = fo.getHeight() + (int)(lineSpace * 2);
myDescent += lineSpace;
myMonospaced = isMonospaced(fo);
if (!myMonospaced) {
LOG.info("WARNING: Font " + myNormalFont.getName() + " is non-monospaced");
}
img.flush();
graphics.dispose();
}
private static boolean isMonospaced(FontMetrics fontMetrics) {
boolean isMonospaced = true;
int charWidth = -1;
for (int codePoint = 0; codePoint < 128; codePoint++) {
if (Character.isValidCodePoint(codePoint)) {
char character = (char)codePoint;
if (isWordCharacter(character)) {
int w = fontMetrics.charWidth(character);
if (charWidth != -1) {
if (w != charWidth) {
isMonospaced = false;
break;
}
}
else {
charWidth = w;
}
}
}
}
return isMonospaced;
}
private static boolean isWordCharacter(char character) {
return Character.isLetterOrDigit(character);
}
protected void setupAntialiasing(Graphics graphics) {
if (graphics instanceof Graphics2D) {
Graphics2D myGfx = (Graphics2D)graphics;
final Object mode = mySettingsProvider.useAntialiasing() ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON
: RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
final RenderingHints hints = new RenderingHints(
RenderingHints.KEY_TEXT_ANTIALIASING, mode);
myGfx.setRenderingHints(hints);
}
}
@Override
public Color getBackground() {
return getPalette().getColor(myStyleState.getBackground());
}
@Override
public Color getForeground() {
return getPalette().getColor(myStyleState.getForeground());
}
@Override
public void paintComponent(final Graphics g) {
Graphics2D gfx = (Graphics2D)g;
if (myImage != null) {
drawImage(gfx, myImage);
drawMargins(gfx, myImage.getWidth(), myImage.getHeight());
drawSelection(myImageForSelection, gfx);
if (mySettingsProvider.useInverseSelectionColor()) {
myCursor.drawCursor(gfx, myImageForSelection, myImage);
}
else {
myCursor.drawCursor(gfx, myImageForCursor, inSelection(myCursor.getCoordX(), myCursor.getCoordY()) ? myImageForSelection : myImage);
}
drawInputMethodUncommitedChars(gfx);
}
}
private void drawInputMethodUncommitedChars(Graphics2D gfx) {
if (myInputMethodUncommitedChars != null && myInputMethodUncommitedChars.length()>0) {
int x = myCursor.getCoordX() * myCharSize.width;
int y = (myCursor.getCoordY()) * myCharSize.height - 2;
int len = (myInputMethodUncommitedChars.length()) * myCharSize.width;
gfx.setColor(getBackground());
gfx.fillRect(x, (myCursor.getCoordY()-1)*myCharSize.height, len, myCharSize.height);
gfx.setColor(getForeground());
gfx.setFont(myNormalFont);
gfx.drawString(myInputMethodUncommitedChars, x, y);
Stroke saved = gfx.getStroke();
BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2, 0, 2}, 0);
gfx.setStroke(dotted);
gfx.drawLine(x, y, x + len, y);
gfx.setStroke(saved);
}
}
private boolean inSelection(int x, int y) {
return mySelection != null && mySelection.contains(new Point(x, y));
}
@Override
public void processKeyEvent(final KeyEvent e) {
handleKeyEvent(e);
e.consume();
}
public void handleKeyEvent(KeyEvent e) {
final int id = e.getID();
if (id == KeyEvent.KEY_PRESSED) {
myKeyListener.keyPressed(e);
}
else if (id == KeyEvent.KEY_RELEASED) {
/* keyReleased(e); */
}
else if (id == KeyEvent.KEY_TYPED) {
myKeyListener.keyTyped(e);
}
}
public int getPixelWidth() {
return myCharSize.width * myTermSize.width;
}
public int getPixelHeight() {
return myCharSize.height * myTermSize.height;
}
public int getColumnCount() {
return myTermSize.width;
}
public int getRowCount() {
return myTermSize.height;
}
public String getWindowTitle() {
return myWindowTitle;
}
public void addTerminalMouseListener(final TerminalMouseListener listener) {
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (mySettingsProvider.enableMouseReporting()) {
Point p = panelToCharCoords(e.getPoint());
listener.mousePressed(p.x, p.y, e);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (mySettingsProvider.enableMouseReporting()) {
Point p = panelToCharCoords(e.getPoint());
listener.mouseReleased(p.x, p.y, e);
}
}
});
}
public void initKeyHandler() {
setKeyListener(new TerminalKeyHandler());
}
public class TerminalCursor {
private boolean myCursorHasChanged;
protected Point myCursorCoordinates = new Point();
private boolean myShouldDrawCursor = true;
private boolean myBlinking = true;
private boolean calculateIsCursorShown(long currentTime) {
if (!isBlinking()) {
return true;
}
if (myCursorHasChanged) {
return true;
}
if (cursorShouldChangeBlinkState(currentTime)) {
return !myCursorIsShown;
}
else {
return myCursorIsShown;
}
}
private boolean cursorShouldChangeBlinkState(long currentTime) {
return currentTime - myLastCursorChange > mySettingsProvider.caretBlinkingMs();
}
public void drawCursor(Graphics2D g, BufferedImage imageForCursor, BufferedImage normalImage) {
if (needsRepaint()) {
final int y = getCoordY();
final int x = getCoordX();
if (y >= 0 && y < myTermSize.height) {
boolean isCursorShown = calculateIsCursorShown(System.currentTimeMillis());
BufferedImage imageToDraw;
if (isCursorShown) {
imageToDraw = imageForCursor;
}
else {
imageToDraw = normalImage;
}
drawImage(g, imageToDraw, x * myCharSize.width, y * myCharSize.height,
(x + 1) * myCharSize.width, (y + 1) * myCharSize.height);
myCursorIsShown = isCursorShown;
myLastCursorChange = System.currentTimeMillis();
myCursorHasChanged = false;
}
}
}
public boolean needsRepaint() {
long currentTime = System.currentTimeMillis();
return isShouldDrawCursor() &&
isFocusOwner() &&
noRecentResize(currentTime) &&
(myCursorHasChanged || cursorShouldChangeBlinkState(currentTime));
}
public void setX(int x) {
myCursorCoordinates.x = x;
myCursorHasChanged = true;
}
public void setY(int y) {
myCursorCoordinates.y = y;
myCursorHasChanged = true;
}
public int getCoordX() {
return myCursorCoordinates.x;
}
public int getCoordY() {
return myCursorCoordinates.y - 1 - myClientScrollOrigin;
}
public void setShouldDrawCursor(boolean shouldDrawCursor) {
myShouldDrawCursor = shouldDrawCursor;
}
public boolean isShouldDrawCursor() {
return myShouldDrawCursor;
}
private boolean noRecentResize(long time) {
return time - myLastResize > mySettingsProvider.caretBlinkingMs();
}
public void setBlinking(boolean blinking) {
myBlinking = blinking;
}
public boolean isBlinking() {
return myBlinking && (mySettingsProvider.caretBlinkingMs() > 0);
}
}
public void drawSelection(BufferedImage imageForSelection, Graphics2D g) {
/* which is the top one */
if (mySelection == null) {
return;
}
Pair<Point, Point> points = mySelection.pointsForRun(myTermSize.width);
Point start = points.first;
Point end = points.second;
if (start.y == end.y) { /* same line */
if (start.x == end.x) {
return;
}
copyImage(g, imageForSelection, start.x * myCharSize.width, (start.y - myClientScrollOrigin) * myCharSize.height,
(end.x - start.x) * myCharSize.width, myCharSize.height);
}
else {
/* to end of first line */
copyImage(g, imageForSelection, start.x * myCharSize.width, (start.y - myClientScrollOrigin) * myCharSize.height,
(myTermSize.width - start.x) * myCharSize.width, myCharSize.height);
if (end.y - start.y > 1) {
/* intermediate lines */
int startInScreen = start.y + 1 - myClientScrollOrigin;
int heightInScreen = end.y - start.y - 1;
if (startInScreen < 0) {
heightInScreen += startInScreen;
startInScreen = 0;
}
heightInScreen = Math.min(myTermSize.height - startInScreen, heightInScreen);
copyImage(g, imageForSelection, 0, startInScreen * myCharSize.height,
myTermSize.width * myCharSize.width, heightInScreen
* myCharSize.height);
}
/* from beginning of last line */
copyImage(g, imageForSelection, 0, (end.y - myClientScrollOrigin) * myCharSize.height, end.x
* myCharSize.width, myCharSize.height);
}
}
private void drawImage(Graphics2D g, BufferedImage image, int x1, int y1, int x2, int y2) {
drawImage(g, image, x1, y1, x2, y2, x1, y1, x2, y2);
}
protected void drawImage(Graphics2D g, BufferedImage image, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2) {
g.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
}
private void copyImage(Graphics2D g, BufferedImage image, int x, int y, int width, int height) {
drawImage(g, image, x, y, x + width, y + height, x, y, x + width, y + height);
}
@Override
public void consume(int x, int y, @NotNull TextStyle style, @NotNull CharBuffer buf, int startRow) {
if (myGfx != null) {
drawCharacters(x, y, style, buf, myGfx);
}
if (myGfxForSelection != null) {
TextStyle selectionStyle = style.clone();
if (mySettingsProvider.useInverseSelectionColor()) {
selectionStyle = getInversedStyle(style);
}
else {
TextStyle mySelectionStyle = mySettingsProvider.getSelectionColor();
selectionStyle.setBackground(mySelectionStyle.getBackground());
selectionStyle.setForeground(mySelectionStyle.getForeground());
}
drawCharacters(x, y, selectionStyle, buf, myGfxForSelection);
}
if (myGfxForCursor != null) {
TextStyle cursorStyle = getInversedStyle(style);
drawCharacters(x, y, cursorStyle, buf, myGfxForCursor);
}
}
private TextStyle getInversedStyle(TextStyle style) {
TextStyle selectionStyle;
selectionStyle = style.clone();
selectionStyle.setOption(Option.INVERSE, !selectionStyle.hasOption(Option.INVERSE));
if (selectionStyle.getForeground() == null) {
selectionStyle.setForeground(myStyleState.getForeground());
}
if (selectionStyle.getBackground() == null) {
selectionStyle.setBackground(myStyleState.getBackground());
}
return selectionStyle;
}
private void drawCharacters(int x, int y, TextStyle style, CharBuffer buf, Graphics2D gfx) {
gfx.setColor(getPalette().getColor(myStyleState.getBackground(style.getBackgroundForRun())));
int textLength = CharacterUtils.getTextLength(buf.getBuf(), buf.getStart(), buf.getLength());
gfx.fillRect(x * myCharSize.width,
(y - myClientScrollOrigin) * myCharSize.height,
textLength * myCharSize.width,
myCharSize.height);
drawChars(x, y, buf, style, gfx);
gfx.setColor(getPalette().getColor(myStyleState.getForeground(style.getForegroundForRun())));
int baseLine = (y + 1 - myClientScrollOrigin) * myCharSize.height - myDescent;
if (style.hasOption(TextStyle.Option.UNDERLINED)) {
gfx.drawLine(x * myCharSize.width, baseLine + 1, (x + textLength) * myCharSize.width, baseLine + 1);
}
}
/**
* Draw every char in separate terminal cell to guaranty equal width for different lines.
* Nevertheless to improve kerning we draw word characters as one block for monospaced fonts.
*/
private void drawChars(int x, int y, CharBuffer buf, TextStyle style, Graphics2D gfx) {
int newBlockLen = 1;
int offset = 0;
int drawCharsOffset = 0;
// workaround to fix Swing bad rendering of bold special chars on Linux
// TODO required for italic?
CharBuffer renderingBuffer;
if(mySettingsProvider.DECCompatibilityMode() && style.hasOption(TextStyle.Option.BOLD)) {
renderingBuffer = CharacterUtils.heavyDecCompatibleBuffer(buf);
} else {
renderingBuffer = buf;
}
while (offset + newBlockLen <= buf.getLength()) {
Font font = getFontToDisplay(buf.charAt(offset + newBlockLen - 1), style);
while (myMonospaced && (offset + newBlockLen < buf.getLength()) && isWordCharacter(buf.charAt(offset + newBlockLen - 1))
&& (font == getFontToDisplay(buf.charAt(offset + newBlockLen - 1), style))) {
newBlockLen++;
}
gfx.setFont(font);
int descent = gfx.getFontMetrics(font).getDescent();
int baseLine = (y + 1 - myClientScrollOrigin) * myCharSize.height - descent;
int xCoord = (x + drawCharsOffset) * myCharSize.width;
int textLength = CharacterUtils.getTextLength(buf.getBuf(), buf.getStart() + offset, newBlockLen);
gfx.setClip(xCoord,
(y - myClientScrollOrigin) * myCharSize.height,
textLength * myCharSize.width,
myCharSize.height);
gfx.setColor(getPalette().getColor(myStyleState.getForeground(style.getForegroundForRun())));
gfx.drawChars(renderingBuffer.getBuf(), buf.getStart() + offset, newBlockLen, xCoord, baseLine);
drawCharsOffset += textLength;
offset += newBlockLen;
newBlockLen = 1;
}
gfx.setClip(null);
}
protected Font getFontToDisplay(char c, TextStyle style) {
boolean bold = style.hasOption(TextStyle.Option.BOLD);
boolean italic = style.hasOption(TextStyle.Option.ITALIC);
// workaround to fix Swing bad rendering of bold special chars on Linux
// TODO required for italic?
if (bold && mySettingsProvider.DECCompatibilityMode() && CharacterSets.isDecSpecialChar(c)) {
return myNormalFont;
}
return bold ? (italic ? myBoldItalicFont : myBoldFont)
: (italic ? myItalicFont : myNormalFont);
}
private ColorPalette getPalette() {
return mySettingsProvider.getTerminalColorPalette();
}
private void clientScrollOriginChanged(int oldOrigin) {
int dy = myClientScrollOrigin - oldOrigin;
int dyPix = dy * myCharSize.height;
copyAndClearAreaOnScroll(dyPix, myGfx, myImage);
copyAndClearAreaOnScroll(dyPix, myGfxForSelection, myImageForSelection);
copyAndClearAreaOnScroll(dyPix, myGfxForCursor, myImageForCursor);
if (dy < 0) {
// Scrolling up; Copied down
// New area at the top to be filled in - can only be from scroll buffer
//
myBackBuffer.getScrollBuffer().processLines(myClientScrollOrigin, -dy, this);
}
else {
// Scrolling down; Copied up
// New area at the bottom to be filled - can be from both
int oldEnd = oldOrigin + myTermSize.height;
// Either its the whole amount above the back buffer + some more
// Or its the whole amount we moved
// Or we are already out of the scroll buffer
int portionInScroll = oldEnd < 0 ? Math.min(-oldEnd, dy) : 0;
int portionInBackBuffer = dy - portionInScroll;
if (portionInScroll > 0) {
myBackBuffer.getScrollBuffer().processLines(oldEnd, portionInScroll, this);
}
if (portionInBackBuffer > 0) {
myBackBuffer.processBufferRows(oldEnd + portionInScroll, portionInBackBuffer, this);
}
}
}
private void copyAndClearAreaOnScroll(int dy, Graphics2D gfx, BufferedImage image) {
if (getPixelHeight() > Math.abs(dy)) {
copyArea(gfx, image, 0, Math.max(0, dy),
getPixelWidth(), getPixelHeight() - Math.abs(dy),
0, -dy);
}
//clear rect before drawing scroll buffer on it
gfx.setColor(getBackground());
if (dy < 0) {
gfx.fillRect(0, 0, getPixelWidth(), Math.min(getPixelHeight(), Math.abs(dy)));
}
else {
gfx.fillRect(0, Math.max(getPixelHeight() - dy, 0), getPixelWidth(), Math.min(getPixelHeight(), dy));
}
}
private void copyArea(Graphics2D gfx, BufferedImage image, int x, int y, int width, int height, int dx, int dy) {
if (isRetina()) {
Pair<BufferedImage, Graphics2D> pair = createAndInitImage(x + width, y + height);
drawImage(pair.second, image,
x, y, x + width, y + height
);
drawImage(gfx, pair.first,
x + dx, y + dy, x + dx + width, y + dy + height, //destination
x, y, x + width, y + height //source
);
}
else {
gfx.copyArea(x, y, width, height, dx, dy);
}
}
int myNoDamage = 0;
int myFramesSkipped = 0;
public void redraw() {
if (tryRedrawDamagedPartFromBuffer() || myCursor.needsRepaint()) {
repaint();
}
}
/**
* This method tries to get a lock for back buffer. If it fails it increments skippedFrames counter and tries next time.
* After 5 attempts it locks buffer anyway.
*
* @return true if was successfully redrawn and there is anything to repaint
*/
private boolean tryRedrawDamagedPartFromBuffer() {
final int newOrigin = newClientScrollOrigin;
if (!myBackBuffer.tryLock()) {
if (myFramesSkipped >= 5) {
myBackBuffer.lock();
}
else {
myFramesSkipped++;
return false;
}
}
try {
myFramesSkipped = 0;
boolean serverScroll = pendingScrolls.enact(myGfx, myImage, getPixelWidth(), myCharSize.height);
boolean clientScroll = myClientScrollOrigin != newOrigin;
if (clientScroll) {
final int oldOrigin = myClientScrollOrigin;
myClientScrollOrigin = newOrigin;
clientScrollOriginChanged(oldOrigin);
}
boolean hasDamage = myBackBuffer.hasDamage();
if (hasDamage) {
myNoDamage = 0;
myBackBuffer.processDamagedCells(this);
myBackBuffer.resetDamage();
}
else {
myNoDamage++;
}
return serverScroll || clientScroll || hasDamage;
}
finally {
myBackBuffer.unlock();
}
}
private void drawMargins(Graphics2D gfx, int width, int height) {
gfx.setColor(getBackground());
gfx.fillRect(0, height, getWidth(), getHeight() - height);
gfx.fillRect(width, 0, getWidth() - width, getHeight());
}
public void scrollArea(final int scrollRegionTop, final int scrollRegionSize, int dy) {
if (dy < 0) {
//Moving lines off the top of the screen
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateScrolling();
}
});
}
mySelection = null;
pendingScrolls.add(scrollRegionTop, scrollRegionSize, dy);
}
private void updateScrolling() {
if (myScrollingEnabled) {
myBoundedRangeModel
.setRangeProperties(0, myTermSize.height, -myBackBuffer.getScrollBuffer().getLineCount(), myTermSize.height, false);
}
else {
myBoundedRangeModel.setRangeProperties(0, myTermSize.height, 0, myTermSize.height, false);
}
}
private class PendingScrolls {
int[] ys = new int[10];
int[] hs = new int[10];
int[] dys = new int[10];
int scrollCount = -1;
void ensureArrays(int index) {
int curLen = ys.length;
if (index >= curLen) {
ys = Util.copyOf(ys, curLen * 2);
hs = Util.copyOf(hs, curLen * 2);
dys = Util.copyOf(dys, curLen * 2);
}
}
void add(int y, int h, int dy) {
if (dy == 0) return;
if (scrollCount >= 0 &&
y == ys[scrollCount] &&
h == hs[scrollCount]) {
dys[scrollCount] += dy;
}
else {
scrollCount++;
ensureArrays(scrollCount);
ys[scrollCount] = y;
hs[scrollCount] = h;
dys[scrollCount] = dy;
}
}
boolean enact(Graphics2D gfx, BufferedImage image, int width, int charHeight) {
if (scrollCount < 0) return false;
for (int i = 0; i <= scrollCount; i++) {
if (dys[i] == 0 || Math.abs(dys[i]) >= hs[i]) { // nothing to do
continue;
}
if (dys[i] > 0) {
copyArea(gfx, image, 0, (ys[i] - 1) * charHeight, width, (hs[i] - dys[i]) * charHeight, 0, dys[i] * charHeight);
}
else {
copyArea(gfx, image, 0, (ys[i] - dys[i] - 1) * charHeight, width, (hs[i] + dys[i]) * charHeight, 0, dys[i] * charHeight);
}
}
scrollCount = -1;
return true;
}
}
final PendingScrolls pendingScrolls = new PendingScrolls();
public void setCursor(final int x, final int y) {
myCursor.setX(x);
myCursor.setY(y);
}
public void beep() {
if (mySettingsProvider.audibleBell()) {
Toolkit.getDefaultToolkit().beep();
}
}
public BoundedRangeModel getBoundedRangeModel() {
return myBoundedRangeModel;
}
public BackBuffer getBackBuffer() {
return myBackBuffer;
}
public TerminalSelection getSelection() {
return mySelection;
}
public LinesBuffer getScrollBuffer() {
return myBackBuffer.getScrollBuffer();
}
public void lock() {
myBackBuffer.lock();
}
public void unlock() {
myBackBuffer.unlock();
}
@Override
public void setCursorVisible(boolean shouldDrawCursor) {
myCursor.setShouldDrawCursor(shouldDrawCursor);
}
protected JPopupMenu createPopupMenu() {
JPopupMenu popup = new JPopupMenu();
TerminalAction.addToMenu(popup, this);
return popup;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
myScrollingEnabled = scrollingEnabled;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateScrolling();
}
});
}
@Override
public void setBlinkingCursor(boolean enabled) {
myCursor.setBlinking(enabled);
}
public TerminalCursor getTerminalCursor() {
return myCursor;
}
public TerminalOutputStream getTerminalOutputStream() {
return myTerminalStarter;
}
@Override
public void setWindowTitle(String name) {
myWindowTitle = name;
if (myTerminalPanelListener != null) {
myTerminalPanelListener.onTitleChanged(myWindowTitle);
}
}
@Override
public List<TerminalAction> getActions() {
return Lists.newArrayList(
new TerminalAction("Copy", mySettingsProvider.getCopyKeyStrokes(), new Predicate<KeyEvent>() {
@Override
public boolean apply(KeyEvent input) {
return handleCopy(true);
}
}).withMnemonicKey(KeyEvent.VK_C).withEnabledSupplier(new Supplier<Boolean>() {
@Override
public Boolean get() {
return mySelection != null;
}
}),
new TerminalAction("Paste", mySettingsProvider.getPasteKeyStrokes(), new Predicate<KeyEvent>() {
@Override
public boolean apply(KeyEvent input) {
handlePaste();
return true;
}
}).withMnemonicKey(KeyEvent.VK_P).withEnabledSupplier(new Supplier<Boolean>() {
@Override
public Boolean get() {
return getClipboardString() != null;
}
}));
}
@Override
public TerminalActionProvider getNextProvider() {
return myNextActionProvider;
}
@Override
public void setNextProvider(TerminalActionProvider provider) {
myNextActionProvider = provider;
}
private void processTerminalKeyPressed(KeyEvent e) {
try {
final int keycode = e.getKeyCode();
final char keychar = e.getKeyChar();
// numLock does not change the code sent by keypad VK_DELETE
// although it send the char '.'
if (keycode == KeyEvent.VK_DELETE && keychar == '.') {
myTerminalStarter.sendBytes(new byte[]{'.'});
return;
}
// CTRL + Space is not handled in KeyEvent; handle it manually
else if (keychar == ' ' && (e.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
myTerminalStarter.sendBytes(new byte[]{Ascii.NUL});
return;
}
final byte[] code = myTerminalStarter.getCode(keycode);
if (code != null) {
myTerminalStarter.sendBytes(code);
if (mySettingsProvider.scrollToBottomOnTyping() && isCodeThatScrolls(keycode)) {
scrollToBottom();
}
}
else if ((keychar & 0xff00) == 0) {
final byte[] obuffer = new byte[1];
obuffer[0] = (byte)keychar;
myTerminalStarter.sendBytes(obuffer);
if (mySettingsProvider.scrollToBottomOnTyping()) {
scrollToBottom();
}
}
}
catch (final Exception ex) {
LOG.error("Error sending key to emulator", ex);
}
}
private static boolean isCodeThatScrolls(int keycode) {
return keycode == KeyEvent.VK_UP
|| keycode == KeyEvent.VK_DOWN
|| keycode == KeyEvent.VK_LEFT
|| keycode == KeyEvent.VK_RIGHT
|| keycode == KeyEvent.VK_BACK_SPACE
|| keycode == KeyEvent.VK_DELETE;
}
private void processTerminalKeyTyped(KeyEvent e) {
final char keychar = e.getKeyChar();
if ((keychar & 0xff00) != 0) {
final char[] foo = new char[1];
foo[0] = keychar;
try {
myTerminalStarter.sendString(new String(foo));
if (mySettingsProvider.scrollToBottomOnTyping()) {
scrollToBottom();
}
}
catch (final RuntimeException ex) {
LOG.error("Error sending key to emulator", ex);
}
}
}
public class TerminalKeyHandler implements KeyListener {
public TerminalKeyHandler() {
}
public void keyPressed(final KeyEvent e) {
if (!TerminalAction.processEvent(TerminalPanel.this, e)) {
processTerminalKeyPressed(e);
}
}
public void keyTyped(final KeyEvent e) {
processTerminalKeyTyped(e);
}
//Ignore releases
public void keyReleased(KeyEvent e) {
}
}
private void handlePaste() {
pasteSelection();
}
// "unselect" is needed to handle Ctrl+C copy shortcut collision with ^C signal shortcut
private boolean handleCopy(boolean unselect) {
if (mySelection != null) {
Pair<Point, Point> points = mySelection.pointsForRun(myTermSize.width);
copySelection(points.first, points.second);
if (unselect) {
mySelection = null;
repaint();
}
return true;
}
return false;
}
/**
* InputMethod implementation
* For details read http://docs.oracle.com/javase/7/docs/technotes/guides/imf/api-tutorial.html
*/
@Override
protected void processInputMethodEvent(InputMethodEvent e) {
int commitCount = e.getCommittedCharacterCount();
if (commitCount > 0) {
myInputMethodUncommitedChars = null;
AttributedCharacterIterator text = e.getText();
if (text != null) {
//noinspection ForLoopThatDoesntUseLoopVariable
for (char c = text.first(); commitCount > 0; c = text.next(), commitCount--) {
if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction
int id = (c & 0xff00) == 0 ? KeyEvent.KEY_PRESSED : KeyEvent.KEY_TYPED;
handleKeyEvent(new KeyEvent(this, id, e.getWhen(), 0, 0, c));
}
}
}
}
else {
myInputMethodUncommitedChars = uncommitedChars(e.getText());
}
}
private static String uncommitedChars(AttributedCharacterIterator text) {
StringBuilder sb = new StringBuilder();
for (char c = text.first(); c != CharacterIterator.DONE; c = text.next()) {
if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction
sb.append(c);
}
}
return sb.toString();
}
@Override
public InputMethodRequests getInputMethodRequests() {
return new MyInputMethodRequests();
}
private class MyInputMethodRequests implements InputMethodRequests {
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
Rectangle r = new Rectangle(myCursor.getCoordX() * myCharSize.width, (myCursor.getCoordY() + 1) * myCharSize.height,
0, 0);
Point p = TerminalPanel.this.getLocationOnScreen();
r.translate(p.x, p.y);
return r;
}
@Nullable
@Override
public TextHitInfo getLocationOffset(int x, int y) {
return null;
}
@Override
public int getInsertPositionOffset() {
return 0;
}
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public int getCommittedTextLength() {
return 0;
}
@Nullable
@Override
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Nullable
@Override
public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
}
}