| /******************************************************************************* |
| * 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.elt.emulator.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 ILineRenderer 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, ILineRenderer 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 ILineRenderer 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); |
| } |
| } |