blob: bfafd3cdc507021ffb0a62a6a0dcd0f4e54f5525 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2011 Wind River Systems, Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package com.google.eclipse.tm.internal.terminal.textcanvas;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.Composite;
/**
* A cell oriented Canvas. Maintains a list of "cells". It can either be vertically or horizontally scrolled. The
* CellRenderer is responsible for painting the cell.
*/
public class TextCanvas extends GridCanvas {
private final ITextCanvasModel cellCanvasModel;
private final ILinelRenderer cellRenderer;
private boolean scrollLockOn;
private Point draggingStart;
private Point draggingEnd;
private boolean hasSelection;
private ResizeListener resizeListener;
// The minSize is meant to determine the minimum size of the backing store (grid) into which remote data is rendered.
// If the viewport is smaller than that minimum size, the backing store size remains at the minSize,and a scrollbar is
// shown instead. In reality, this has the following issues or effects today:
//
// (a) Bug 281328: For very early data coming in before the widget is realized, the minSize determines into what
// initial grid that is rendered. See also {@link #addResizeHandler(ResizeListener)}.
//
// (b) Bug 294468: Since we have redraw and size computation problems with horizontal scrollers, for now the
// minColumns must be small enough to avoid a horizontal scroller appearing in most cases.
//
// (c) Bug 294327: Since we have problems with the vertical scroller showing the correct location, minLines must be
// small enough to avoid a vertical scroller or new data may be rendered off-screen.
//
// As a compromise, we have been working with a 20x4 since the terminal inception, though many users would want a
// 80x24 minSize and backing store.
// Pros and cons of the small minsize:
// + consistent "remote size==viewport size", vi works as expected
// - dumb terminals which expect 80x24 render garbled on small viewport
//
// If bug 294468 were resolved, an 80 wide minSize would be preferrable since it allows switching the terminal
// viewport small/large as needed, without destroying the backing store. For a complete solution, bug 196462 tracks
// the request for a user-defined fixed-widow-size-mode.
private int minColumns = 80;
private int minLines = 4;
private boolean cursorEnabled;
private boolean resizing;
/**
* Create a new CellCanvas with the given SWT style bits. (SWT.H_SCROLL and SWT.V_SCROLL are automatically added).
*/
public TextCanvas(Composite parent, ITextCanvasModel model, int style, ILinelRenderer cellRenderer) {
super(parent, style | SWT.H_SCROLL | SWT.V_SCROLL);
this.cellRenderer = cellRenderer;
setCellWidth(cellRenderer.getCellWidth());
setCellHeight(cellRenderer.getCellHeight());
cellCanvasModel = model;
cellCanvasModel.addCellCanvasModelListener(new ITextCanvasModelListener() {
@Override public void rangeChanged(int col, int line, int width, int height) {
repaintRange(col, line, width, height);
}
@Override public void dimensionsChanged(int cols, int rows) {
calculateGrid();
}
@Override public void terminalDataChanged() {
if (!isDisposed() && !resizing) {
// scroll to end (unless scroll lock is active)
calculateGrid();
scrollToEnd();
}
}
});
// let the cursor blink if the text canvas gets the focus...
addFocusListener(new FocusListener() {
@Override public void focusGained(FocusEvent e) {
cellCanvasModel.setCursorEnabled(cursorEnabled);
}
@Override public void focusLost(FocusEvent e) {
cellCanvasModel.setCursorEnabled(false);
}
});
addMouseListener(new MouseAdapter() {
@Override public void mouseDown(MouseEvent e) {
if (e.button == 1) { // left button
draggingStart = screenPointToCell(e.x, e.y);
hasSelection = false;
if ((e.stateMask & SWT.SHIFT) != 0) {
Point anchor = cellCanvasModel.getSelectionAnchor();
if (anchor != null) {
draggingStart = anchor;
}
} else {
cellCanvasModel.setSelectionAnchor(draggingStart);
}
draggingEnd = null;
}
}
@Override public void mouseUp(MouseEvent e) {
if (e.button == 1) { // left button
updateHasSelection(e);
if (hasSelection) {
setSelection(screenPointToCell(e.x, e.y));
} else {
cellCanvasModel.setSelection(-1, -1, -1, -1);
}
draggingStart = null;
}
}
});
addMouseMoveListener(new MouseMoveListener() {
@Override public void mouseMove(MouseEvent e) {
if (draggingStart != null) {
updateHasSelection(e);
setSelection(screenPointToCell(e.x, e.y));
}
}
});
serVerticalBarVisible(true);
setHorizontalBarVisible(false);
}
// The user has to drag the mouse to at least one character to make a selection. Once this is done, even a one char
// selection is OK.
private void updateHasSelection(MouseEvent e) {
if (draggingStart != null) {
Point p = screenPointToCell(e.x, e.y);
if (draggingStart.x != p.x || draggingStart.y != p.y) {
hasSelection = true;
}
}
}
void setSelection(Point p) {
if (draggingStart != null && !p.equals(draggingEnd)) {
draggingEnd = p;
if (compare(p, draggingStart) < 0) {
// bug 219589 - make sure selection start coordinates are non-negative
int startColumn = Math.max(0, p.x);
int startRow = Math.max(p.y, 0);
cellCanvasModel.setSelection(startRow, draggingStart.y, startColumn, draggingStart.x);
} else {
cellCanvasModel.setSelection(draggingStart.y, p.y, draggingStart.x, p.x);
}
}
}
int compare(Point p1, Point p2) {
if (p1.equals(p2)) {
return 0;
}
if (p1.y == p2.y) {
return p1.x > p2.x ? 1 : -1;
}
return p1.y > p2.y ? 1 : -1;
}
public ILinelRenderer getCellRenderer() {
return cellRenderer;
}
public int getMinColumns() {
return minColumns;
}
public void setMinColumns(int minColumns) {
this.minColumns = minColumns;
}
public int getMinLines() {
return minLines;
}
public void setMinLines(int minLines) {
this.minLines = minLines;
}
protected void onResize(boolean init) {
if (resizeListener != null) {
Rectangle bonds = getClientArea();
int cellHeight = getCellHeight();
int cellWidth = getCellWidth();
int lines = bonds.height / cellHeight;
int columns = bonds.width / cellWidth;
// When the view is minimized, its size is set to 0 we don't sent this to the terminal!
if ((lines > 0 && columns > 0) || init) {
if (columns < minColumns) {
if (!isHorizontalBarVisble()) {
setHorizontalBarVisible(true);
bonds = getClientArea();
lines = bonds.height / cellHeight;
}
columns = minColumns;
} else if (columns >= minColumns && isHorizontalBarVisble()) {
setHorizontalBarVisible(false);
bonds = getClientArea();
lines = bonds.height / cellHeight;
columns = bonds.width / cellWidth;
}
if (lines < minLines) {
lines = minLines;
}
resizeListener.sizeChanged(lines, columns);
}
}
super.onResize();
calculateGrid();
}
@Override protected void onResize() {
resizing = true;
try {
onResize(false);
} finally {
resizing = false;
}
}
private void calculateGrid() {
Rectangle virtualBounds = getVirtualBounds();
setRedraw(false);
try {
setVirtualExtend(getCols() * getCellWidth(), getRows() * getCellHeight());
getParent().layout();
if (resizing) {
// scroll to end if view port was near last line
Rectangle viewRect = getViewRectangle();
if (virtualBounds.height - (viewRect.y + viewRect.height) < getCellHeight() * 2) {
scrollToEnd();
}
}
} finally {
setRedraw(true);
}
}
void scrollToEnd() {
if (!scrollLockOn) {
int y = -(getRows() * getCellHeight() - getClientArea().height);
if (y > 0) {
y = 0;
}
Rectangle v = getViewRectangle();
if (v.y != -y) {
setVirtualOrigin(v.x, y);
}
// make sure the scroll area is correct.
scrollY(getVerticalBar());
scrollX(getHorizontalBar());
}
}
public boolean isScrollLockOn() {
return scrollLockOn;
}
public void setScrollLockOn(boolean on) {
scrollLockOn = on;
}
protected void repaintRange(int col, int line, int width, int height) {
Point origin = cellToOriginOnScreen(col, line);
Rectangle r = new Rectangle(origin.x, origin.y, width * getCellWidth(), height * getCellHeight());
repaint(r);
}
@Override protected void drawLine(GC gc, int line, int x, int y, int colFirst, int colLast) {
cellRenderer.drawLine(cellCanvasModel, gc, line, x, y, colFirst, colLast);
}
@Override protected Color getTerminalBackgroundColor() {
return cellRenderer.getDefaultBackgroundColor();
}
@Override protected void visibleCellRectangleChanged(int x, int y, int width, int height) {
cellCanvasModel.setVisibleRectangle(y, x, height, width);
update();
}
@Override protected int getCols() {
return cellCanvasModel.getTerminalText().getWidth();
}
@Override protected int getRows() {
return cellCanvasModel.getTerminalText().getHeight();
}
public String getSelectionText() {
// TODO -- create a hasSelectionMethod!
return cellCanvasModel.getSelectedText();
}
public void copy() {
Clipboard clipboard = new Clipboard(getDisplay());
clipboard.setContents(new Object[] { getSelectionText() }, new Transfer[] { TextTransfer.getInstance() });
clipboard.dispose();
}
public void selectAll() {
cellCanvasModel.setSelection(
0, cellCanvasModel.getTerminalText().getHeight(), 0, cellCanvasModel.getTerminalText().getWidth());
cellCanvasModel.setSelectionAnchor(new Point(0, 0));
}
public boolean isEmpty() {
return false;
}
/**
* Gets notified when the visible size of the terminal changes. This should update the model!
*/
public static interface ResizeListener {
void sizeChanged(int lines, int columns);
}
public void addResizeHandler(ResizeListener listener) {
if (resizeListener != null) {
throw new IllegalArgumentException("There can be at most one listener at the moment!");
}
resizeListener = listener;
// Bug 281328: The very first few characters might be missing in the terminal control if opened and connected
// programmatically.
//
// In case the terminal had not been visible yet or is too small (less than one line visible), the terminal should
// have a minimum size to avoid RuntimeExceptions.
Rectangle bonds = getClientArea();
if (bonds.height < getCellHeight() || bonds.width < getCellWidth()) {
// Widget not realized yet, or minimized to < 1 item. Just tell the listener our min size.
resizeListener.sizeChanged(getMinLines(), getMinColumns());
} else {
// Widget realized: compute actual size and force telling the listener
onResize(true);
}
}
public void onFontChange() {
cellRenderer.onFontChange();
setCellWidth(cellRenderer.getCellWidth());
setCellHeight(cellRenderer.getCellHeight());
calculateGrid();
}
public void setInvertedColors(boolean invert) {
cellRenderer.setInvertedColors(invert);
redraw();
}
/**
* Indicates whether the cursor is enabled (blinking.) By default the cursor is not enabled.
*
* @return {@code true} if the cursor is enabled, {@code false} otherwise.
*/
public boolean isCursorEnabled() {
return cursorEnabled;
}
/**
* Enables or disables the cursor (enabling means that the cursor blinks.)
*
* @param enabled indicates whether the cursor should be enabled.
*/
public void setCursorEnabled(boolean enabled) {
if (enabled != cursorEnabled) {
cursorEnabled = enabled;
cellCanvasModel.setCursorEnabled(cursorEnabled);
}
}
public void setColors(RGB background, RGB foreground) {
cellRenderer.setColors(background, foreground);
redraw();
}
@Override public void setFont(Font font) {
super.setFont(font);
cellRenderer.setFont(font);
redraw();
}
@Override public Point screenPointToCell(int x, int y) {
return super.screenPointToCell(x, y);
}
public void setBlinkingCursor(boolean useBlinkingCursor) {
cellCanvasModel.setBlinkingCursor(useBlinkingCursor);
}
}