alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *			Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License Version
 * 1.0 (the "License"). You may not use this file except in compliance
 * with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is Terminal Emulator.
 * The Initial Developer of the Original Code is Sun Microsystems, Inc..
 * Portions created by Sun Microsystems, Inc. are Copyright (C) 2001-2004.
 * All Rights Reserved.
 *
 * Contributor(s): Ivan Soleimanipour.
 */

package org.netbeans.lib.terminalemulator;

import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.accessibility.*;

import java.awt.font.*;
import java.awt.geom.Point2D;

import java.util.HashSet;
import java.util.Date;
import java.util.LinkedList;
import java.util.ListIterator;

/**
Term is a pure Java multi-purpose terminal emulator.

It has the following generic features:

  • All "dumb" operations. Basically putting characters on a screen and processing keyboard input.
  • ANSI mode "smart" operations. Cursor control etc.
  • Character attributes like color, reverse-video etc.
  • Selection service in character, word and line modes matching xterm with configurable word boundary detection.
  • History buffer.
  • Facilities to iterate through logical lines, to implement search for example.
  • Support for nested pickable regions in order to support hyperlinked views or more complex active text utilities.
  • Support for double-width Oriental characters.

Coordinate systems

The following coordinate systems are used with Term. They are all cartesian and have their origin at the top left. All but the first are 0-origin. But they differ in all other respects:
ANSI Screen coordinates
Address only the visible portion of the screen. They are 1-origin and extend thru the width and height of the visible portion of the screen per getColumns() and getRows().

This is how an application (like 'vi' etc) views the screen. This coordinate system primarily comes into play in the cursor addressing directive, op_cm() and otherwise is not really used in the implementation.

Cell coordinates
Each character usually takes one cell, and all placement on the screen is in terms of a grid of cells getColumns() wide This cellular nature is why fixed font is "required". In some locales some characters may be double-width. Japanese characters are like this, so they take up two cells. There are no double-height characters (that I know of).

Cursor motion is in cell coordinates, so to move past a Japanese character you need the cursor to move right twice. A cursor can also be placed on the second cell of a double-width character.

Note that this is strictly an internal coordinate system. For example Term.getCursorCol() and getCursorCoord() return buffer coordinates.

The main purpose of this coordinate system is to capture logical columns. In the vertical direction sometimes it extends only the height of the screen and sometimes the height of the buffer.

Buffer coordinates ...
... address the whole history character buffer. These are 0-origin and extend thru the width of the screen per getColumns(), or more if horizontal scrolling is enabled, and the whole history, that is, getHistorySize()+getRows().

The BCoord class captures the value of such coordinates. It is more akin to the 'int offset' used in the Java text package as opposed to javax.swing.text.Position.

If there are no double-width characters the buffer coords pretty much overlap with cell coords. If double-width characters are added then the buffer column and cell column will have a larger skew the more right you go.

Absolute coordinates ...
... are like Buffer coordinates in the horizontal direction. In the vertical direction their origin is the first line that was sent to the terminal. This line might have scrolled out of history and might no longer be in the buffer. In effect each line ever printed by Term gets a unique Absolute row.

What good is this? The ActiveRegion mechanism maintains coordinates for its' boundaries. As text scrolls out of history buffer row coordinates have to shift and all ActiveRegions' coords need to be relocated. This can get expensive because as soon as the history buffer becomes full each newline will require a relocation. This is the approach that javax.swing.text.Position implements and it's justified there because no Swing component has a "history buffer". However, if you use absolute coordinates you'll never have to relocate anything! Simple and effective.

Well almost. What happens when you reach Integer.MAX_VALUE? You wrap and that can confuse everything. What are the chances of this happening? Suppose term can process 4000 lines per second. A runaway process will produce Integer.MAX_VALUE lines in about 4 days. That's too close for comfort, so Term does detect the wrap and only then goes and relocates stuff. This, however, causes a secondary problem with testability since no-one wants to wait 4 days for a single wrap. So what I've done is periodically set Term.modulo to something smaller and tested stuff.

I'm indebted to Alan Kostinsky for this bit of lateral thinking.

Modes of use

There are three ways Term can be used. These modes aren't explicit they are just a convenient way of discussing functionality.
Screen mode
This represents the traditional terminal without a history buffer. Applications running under the terminal assume they are dealing with a fixed size screen and interact with it in the screen coordinate system through escape sequence (ANSI or otherwise). The most common application which uses terminals in this ways is the screen editor, like vi or emacs.

Term will convert keystrokes to an output stream and will process characters in an input stream and render them unto the screen. What and how these streams are connected to is up to the client of Term, since it is usually highly platform dependent. For example on unixes the streams may be connected to partially-JNI-based "pty" streams.

This mode works correctly even if there is history and you see a scrollbar, just as it does under XTerm and it's derivatives.

Buffer/Interactive mode
This is the primary facility that XTerm and other derivatives provide. The screen has a history buffer in the vertical dimension.

Because of limited history active regions can scroll out of history and while the coordinate invalidation problem is not addressed by absolute coordiantes sometimes we don't want stuff to wink out.
Which is why we have ...

Page mode
It is possible to "anchor" a location in the buffer and prevent it from going out of history. This can be helpful in having the client of Term make sure that crucial output doesn't get lost due to short-sighted history settings on the part of the user.

To use Term in this mode you can use setText() or appendText() instead of connecting to Terms streams. This mode is called page mode because the most common use of it would be as something akin to a hypertext browser. To that end Term supports nestable ActiveRegions and mapping of coordinates to regions. ActiveTerm puts all of this together in a comprehensive subclass.

What Term is not

  • While there is an internal Buffer class, and while it behaves like a document in that it can be implicitly "edited" and character attributes explicitly changed, Term is not a document editing widget.

  • Term is also not a command line processor in the sense that a MS Windows console is. Its shuttling of keyboard events to an output stream and rendering of characters from the input stream unto the screen are completely independent activities.

    This is due to Terms unix heritage where shells (ksh, bash etc) do their own cmdline and history editing, but if you're so inclined the LineDiscipline may be used for experimentation with indigenous cmdline processing.

*/ public class Term extends JComponent implements Accessible { private State st = new State(); private Sel sel = new Sel(this, st); private Ops ops = new OpsImpl(); private int top_margin = 0; // 0 means default (see topMargin()) private int bot_margin = 0; // Stuff to control how often RegionManager.cull() gets called private int cull_count = 0; private static final int cull_frequency = 50; // 'firsta' is the absolute line number of the line at 'lines[0]'. protected int firsta = 0; // chars gone by in lines that winked out of history // 'firsta' ~= 'linesInPrehistory' private int charsInPrehistory = 0; private static final int modulo = Integer.MAX_VALUE/2; private Screen screen; private JScrollBar vscroll_bar; private ScrollWrapper hscroll_wrapper; private JScrollBar hscroll_bar; private boolean has_focus; // statistics private int n_putchar; private int n_putchars; private int n_linefeeds; private int n_repaint; private int n_paint; MyFontMetrics metrics = null; Buffer buf = new Buffer(80); private RegionManager region_manager = new RegionManager(); // 'left_down_point' remembers where the left button came down as a // workaround for the flakey mouseDragged event. The flakiness has to with // the fact that the mousePressed coord is not delivered in the first drag // event, so if you proess and drag very quickly, the first drag coord // will be quite far from the initial press location. private Point left_down_point; // getSystemSelection() wasn't available on Java prior to 1.4 private Clipboard systemClipboard = getToolkit().getSystemClipboard(); private Clipboard systemSelection = getToolkit().getSystemSelection(); /** * ScrollWrapper is a HACK that allows us to make pairs of scrollbars * look nice. *

* A JScrollPane, or more specifically ScrollPaneLayout, arranges * a pair of vertical and horizontal scrollbars as follows: * | | * | | * |v| * ------------- * >| * ------------- * ... so that here is a nice square corner. * * But ScrollPaneLayout insists that it's viewport is a JViewport and that * it's container is a JScrollPane. It is probably possible to make the * screen be a JViewPort and use a JScrollPane to contain the screen, but * it's very tricky. (For that matter it should be possible to avoid doing * our own scrolling altogether and use JScrollPane functionality, but * it's also tricky). * * Since we're using a BorderLayout putting the horizontal SB in the SOUTH * portion yields something like this: * | | * | | * |v| * ---------------- * >| * ---------------- * Soooo, to make things look right, we use ScrollWrapper to control the * sizing of the horizontal scrollbar. It basically uses a GridBagLayout * and GridBagConstraints.insets to create the square corner. */ private class ScrollWrapper extends JComponent implements Accessible { public JScrollBar scroll_bar; public ScrollWrapper(JScrollBar scroll_bar) { GridBagLayout gbl = new GridBagLayout(); setLayout(gbl); GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 1.0f; gbc.weighty = 1.0f; int slop = vscroll_bar.getMaximumSize().width; gbc.insets = new Insets(0, 0, 0, slop); add(scroll_bar, gbc); this.scroll_bar = scroll_bar; } protected void paintComponent(Graphics g) { // If we don't do this, the square corner will end getting filled // with random grot. Dimension sz = getSize(); g.clearRect(0, 0, sz.width, sz.height); } //...................................................................... // Accessibility stuff is all here //...................................................................... public AccessibleContext getAccessibleContext() { if (accessible_context == null) { accessible_context = new AccessibleScrollWrapper(); } return accessible_context; } private AccessibleContext accessible_context; protected class AccessibleScrollWrapper extends AccessibleJComponent { public AccessibleRole getAccessibleRole() { return AccessibleRole.PANEL; } } } private class BaseTermStream extends TermStream { public void flush() { repaint(true); } public void putChar(char c) { /* * echoes a character unto the screen */ ckEventDispatchThread(); // OLD NPE-x synchronized(Term.this) { n_putchar++; putc_work(c); } possibly_repaint(true); // pavel.buzek@czech.sun.com put this as a fix to speed up // StreamTerm on windows. This will make raw mode not work, // Instead now LineDiscipline properly buffers incoming characters. // so should be considered temporary. // repaint(c == '\n'); } public void putChars(char buf[], int offset, int count) { ckEventDispatchThread(); // OLD NPE-x synchronized (Term.this) { n_putchars++; for (int bx = 0; bx < count; bx++) { putc_work(buf[offset+bx]); } } possibly_repaint(true); } public void sendChar(char c) { fireChar(c); } public void sendChars(char buf[], int offset, int count) { fireChars(buf, offset, count); } } // head is closer to Term // pushes extend tail private TermStream base_stream = new BaseTermStream(); private TermStream dce_end = base_stream; private TermStream dte_end = base_stream; /** * */ public void pushStream(TermStream stream) { // Term will send keystrokes by calling dte_end.sendChar // Characters sent via Term.putChar will be sent down dce_end. // // The base stream is strange in that on the first push it will get // split into two parts, one sticking at the end of the dce chain, // the other at the end of the dte chain. Hence the special case // treatment if (dce_end == base_stream) { // towards dce dte_end = stream; stream.setToDCE(base_stream); // towards dte stream.setToDTE(base_stream); dce_end = stream; } else { // towards dce dte_end.setToDCE(stream); stream.setToDCE(base_stream); // towards dte stream.setToDTE(dce_end); dce_end = stream; } stream.setTerm(this); } /* * Debugging utilities */ public static final int DEBUG_OPS = 1<<0; public static final int DEBUG_KEYS = 1<<1; public static final int DEBUG_INPUT = 1<<2; public static final int DEBUG_OUTPUT = 1<<3; public static final int DEBUG_WRAP = 1<<4; public static final int DEBUG_MARGINS = 1<<5; private int debug_gutter_width = 0; public void setDebugFlags(int flags) { debug = flags; } private int debug = /* DEBUG_OPS|DEBUG_KEYS|DEBUG_INPUT|DEBUG_OUTPUT | */ 0; private boolean debugOps() { return (debug & DEBUG_OPS) == DEBUG_OPS; } private boolean debugKeys() { return (debug & DEBUG_KEYS) == DEBUG_KEYS; } private boolean debugWrap() { return (debug & DEBUG_WRAP) == DEBUG_WRAP; } private boolean debugMargins() { return true; /* TMP return (debug & DEBUG_MARGINS) == DEBUG_MARGINS; */ } /** * Return true if DEBUG_INPUT flag has been set. */ protected boolean debugInput() { return (debug & DEBUG_INPUT) == DEBUG_INPUT; } /** * Return true if DEBUG_OUTPUT flag has been set. */ protected boolean debugOutput() { return (debug & DEBUG_OUTPUT) == DEBUG_OUTPUT; } /* * Top and bottom margins are stored as 1-origin values, with 0 * denoting default. * * topMargin() & botMargin() return 0-origin values which is what we * use for screen coordinates. * * The margin lines are inclusive, that is, lines on the margin lines * participate in scrolling. */ private int topMargin() { return (top_margin == 0)? 0: top_margin-1; } private int botMargin() { return (bot_margin == 0)? st.rows-1: bot_margin-1; } /** * beginx is row lines above the bottom. * It's used for all cursor motion calculations and cursor relative * line operations. * It's used instead of firstx because firstx changes as we scroll. * This allows us to restrict screen editing to the last chunk of * the buffer. */ private int beginx() { return buf.nlines - st.rows; } private Line cursor_line() { return buf.lineAt(st.cursor.row); } /** * Set/unset WordDelineator. * Passing a null sets it to the default WordDelineator. */ public void setWordDelineator(WordDelineator word_delineator) { if (word_delineator == null) this.word_delineator = default_word_delineator; else this.word_delineator = word_delineator; } /** * Get the WordDelineator used by this. */ public WordDelineator getWordDelineator() { return this.word_delineator; } private WordDelineator default_word_delineator = new WordDelineator(); WordDelineator word_delineator = default_word_delineator; /** * Set/unset input listener. * Entered text gets sent via this listener * * @deprecated Replaced by {@link #addInputListener(TermInputListener)}. */ public void setInputListener(TermInputListener l) { addInputListener(l); } /** * Add an input listener to this. *

* Text entered via the keyboard gets sent via this listener. */ public void addInputListener(TermInputListener l) { input_listeners.add(l); } public void removeInputListener(TermInputListener l) { input_listeners.remove(l); } private void fireChar(char c) { ListIterator iter = input_listeners.listIterator(); while (iter.hasNext()) { TermInputListener l = (TermInputListener) iter.next(); l.sendChar(c); } } private void fireChars(char buf[], int offset, int count) { ListIterator iter = input_listeners.listIterator(); while (iter.hasNext()) { TermInputListener l = (TermInputListener) iter.next(); l.sendChars(buf, offset, count); } } private LinkedList input_listeners = new LinkedList(); /** * Set/unset misc listener. * The following events gets sent via this listener: * window size changes * * @deprecated Replaced by{@link #addListener(TermListener)}. */ public void setListener(TermListener l) { addListener(l); } /** * Add a TermListener to this. */ public void addListener(TermListener l) { listeners.add(l); } /** * Remove the given TermListener from this. */ public void removeListener(TermListener l) { listeners.remove(l); } private void fireSizeChanged(Dimension cells, Dimension pixels) { ListIterator iter = listeners.listIterator(); while (iter.hasNext()) { TermListener l = (TermListener) iter.next(); l.sizeChanged(cells, pixels); } } private LinkedList listeners = new LinkedList(); /** * Set/unset focus policy. *
* When set, the Term screen will grab focus when clicked on, otherwise * it will grab focus when the mouse moves into it. */ public void setClickToType(boolean click_to_type) { this.click_to_type = click_to_type; } public boolean isClickToType() { return click_to_type; } private boolean click_to_type = true; /** * Control whether keystrokes are ignored. */ public void setReadOnly(boolean read_only) { this.read_only = read_only; } /** * Return whether keystrokes are ignored. */ public boolean isReadOnly() { return read_only; } private boolean read_only = false; /** * Clear the visible portion of screen */ public void clear() { for (int row = 0; row < st.rows; row++) { Line l = buf.lineAt(beginx() + row); l.reset(); } regionManager().reset(); } /** * Clear all of the history without repainting the screen. *

* This is useful if you want to avoid flicker. */ public void clearHistoryNoRefresh() { sel.cancel(true); int old_cols = buf.visibleCols(); buf = new Buffer(old_cols); firsta = 0; charsInPrehistory = 0; st.firstx = 0; st.firsty = 0; st.cursor.row = 0; st.cursor.col = 0; st.attr = 0; st.saveCursor(); // This clobbers the saved cursor value st.restoreCursor(); // release reference to saved cursor object adjust_lines(st.rows); st.firstx = 0; st.firsty = 0; regionManager().reset(); screen.possiblyUpdateCaretText(); } /** * Clear all of the history, including any visible portion of it. *

* Use {@link #clearHistoryNoRefresh()} if you find that clearHistory * causes flickering. */ public void clearHistory() { clearHistoryNoRefresh(); repaint(true); } /** * Return the RegionManager associated with this Term */ public RegionManager regionManager() { return region_manager; } public String textWithin(Coord begin, Coord end) { if (begin == null || end == null) return null; final StringBuffer buf = new StringBuffer(); visitLines(begin, end, false, new LineVisitor() { public boolean visit(Line l, int row, int bcol, int ecol) { buf.append(l.charArray(), bcol, ecol-bcol+1); return true; } } ); return buf.toString(); } public String getRowText(int row) { Line line = buf.lineAt( row ); if ( line == null ) return null; return line.stringBuffer().toString(); } /** * Get KeyStroke set. *

* Be default Term consumes all keystrokes. * Any KeyStroke added to this set will be passed through and not consumed. *

* Be careful with control characters you need to create the keystroke * as follows (note the - 64): *

     * KeyStroke.getKeyStroke(new Character((char)('T'-64)), Event.CTRL_MASK)
     * 
*/ public HashSet getKeyStrokeSet() { return keystroke_set; } /* * Set the KeyStroke set. *

* While Term has a KeyStroke set set up by default, often many Terms * share the same keystroke. This method allows this sharing. */ public void setKeyStrokeSet(HashSet keystroke_set) { this.keystroke_set = keystroke_set; /* DEBUG System.out.println("-----------------------------------------");//NOI18N java.util.Iterator i = keystroke_set.iterator(); while (i.hasNext()) { KeyStroke ks = (KeyStroke) i.next(); System.out.println("--- " + ks); // NOI18N } */ } private HashSet keystroke_set = new HashSet(); // attempted partial fix for IZ 17337 // 'keystroke_set' is a collection of KeyStrokes in the form: // ks3 = getKeyStroke(VK_C, CTRL_MASK) // we use maybeConsume() in keyPressed and keyTyped events. During // keyTyped the event->KS gives us // ks2 = getKeyStroke((char) ('c'-64), CTRL_MASK) // ks2 and ks3 while logically equivalent don't hash to the same so // maybeConsume() says yes to ks2 and the Ctrl-C gets passed on. // // So to detect whether something in 'keystroke_set' needs to be dropped // we need to check at keyPress time but take action at keyTyped time. // 'passOn' helps us do that. private boolean passOn = true; /** * Return true (and consume it) if 'e' is allowed to be consumed by us. * * If our owner is interested in some keys they will put someting into * keystroke_set. */ private boolean maybeConsume(KeyEvent e) { if (e.isConsumed()) return false; KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e); /* DEBUG System.out.println("Term.maybeConsume(" + e + ")"); // NOI18N System.out.println("\tKS = " + ks); // NOI18N System.out.println("\tcontained = " + keystroke_set.contains(ks)); // NOI18N */ if (keystroke_set == null || ! keystroke_set.contains(ks)) { e.consume(); return true; } return false; } /** * Visit the physical lines from begin, through 'end'. *

* If 'newlines' is set, the passed 'ecol' is set to the actual * number of columns in the view to signify that the newline is included. * This way of doing it helps with rendering of a whole-line selection. * Also Line knows about this and will tack on a "\n" when Line.text() * is asked for. */ void visitLines(Coord begin, Coord end, boolean newlines, LineVisitor visitor) { buf.visitLines(begin.toBCoord(firsta), end.toBCoord(firsta), newlines, visitor); } /** * Visit logical lines from begin through end. *

* If begin is null, then the start of the buffer is assumed. * If end is null, then the end of the buffer is assumed. */ public void visitLogicalLines(Coord begin, Coord end, final LogicalLineVisitor llv) { // Create a trampoline visitor LineVisitor tramp = new LineVisitor() { String text = ""; // NOI18N int lineno = 0; Coord begin = null; Coord end = null; public boolean visit(Line l, int brow, int bcol, int ecol) { if (l.isWrapped()) { if (begin == null) begin = new Coord(new BCoord(brow, bcol), firsta); text += l.text(bcol, ecol); } else { if (begin == null) begin = new Coord(new BCoord(brow, bcol), firsta); end = new Coord(new BCoord(brow, ecol), firsta); text += l.text(bcol, ecol); if (!llv.visit(lineno, begin, end, text)) return false; lineno++; text = ""; // NOI18N begin = null; end = null; } return true; } } ; if (begin == null) begin = new Coord(); if (end == null) { int lastrow = buf.nlines-1; Line l = buf.lineAt(lastrow); end = new Coord(new BCoord(lastrow, l.length()-1), firsta); } if (begin.compareTo(end) > 0) return; buf.visitLines(begin.toBCoord(firsta), end.toBCoord(firsta), false, tramp); } /** * Visit logical lines in reverse from end through begin. *

* If begin is null, then the start of the buffer is assumed. * If end is null, then the end of the buffer is assumed. */ public void reverseVisitLogicalLines(Coord begin, Coord end, final LogicalLineVisitor llv) { // Create a trampoline visitor LineVisitor tramp = new LineVisitor() { String text = ""; // NOI18N int lineno = 0; Coord begin = null; Coord end = null; public boolean visit(Line l, int brow, int bcol, int ecol) { boolean line_is_continuation = false; if (brow > 0 && buf.lineAt(brow-1).isWrapped()) line_is_continuation = true; if (line_is_continuation) { if (end == null) end = new Coord(new BCoord(brow, ecol), firsta); text = l.text(bcol, ecol) + text; } else { if (end == null) end = new Coord(new BCoord(brow, ecol), firsta); begin = new Coord(new BCoord(brow, bcol), firsta); text = l.text(bcol, ecol) + text; if (!llv.visit(lineno, begin, end, text)) return false; lineno++; text = ""; // NOI18N begin = null; end = null; } return true; } } ; if (begin == null) begin = new Coord(); if (end == null) { int lastrow = buf.nlines-1; Line l = buf.lineAt(lastrow); end = new Coord(new BCoord(lastrow, l.length()-1), firsta); } if (begin.compareTo(end) > 0) return; buf.reverseVisitLines(begin.toBCoord(firsta), end.toBCoord(firsta), false, tramp); } /** * Convert offsets in logical lines into physical coordinates. *

* Usually called from within a LogicalLineVisitor.visit(). * 'begin' should be the 'begin' Coord passed to visit. 'offset' is an * offset into the logical line, the 'text' argument passed to visit(). *

* Note that 'offset' is relative to begin.col! *

* Following is an example of a line "aaaaaxxxxxxxxxXXXxxxxxxxx" which * got wrapped twice. Suppose you're searching for "XXX" and you * began your search from the first 'x' on row 2. *

	    row |  columns |
	    ----+----------+
	    0	|0123456789|
	    1	|          |
	    2	|aaaaaxxxxx| wrap
	    3	|xxxxXXXxxx| wrap
	    4	|xxxxx     |
	    5	|	   |

     * 
* The visitor will be called with 'begin' pointing at the first 'x', * 'end' pointing at the last 'x' and 'text' containing the above line. * Let's say you use String.indexOf("XXX") and get an offset of 9. * Passing the 'begin' through and the 9 as an offset and 3 as the * length will yield an Extent (3,4) - (3,7) which youcan forward to * setSelectionExtent(); *

* The implementation of this function is not snappy (it calls * Term.advance() in a loop) but the assumption is that his function * will not be called in a tight loop. */ public Extent extentInLogicalLine(Coord begin, int offset, int length) { // SHOULD factor the A/B Coord conversion out Coord from = (Coord) begin.clone(); while (offset-- > 0) { from = new Coord(buf.advance(from.toBCoord(firsta)), firsta); } Coord to = (Coord) from.clone(); while (--length > 0) { to = new Coord(buf.advance(to.toBCoord(firsta)), firsta); } return new Extent(from, to); } private boolean cursor_was_visible() { return st.cursor.row-1 >= st.firstx && st.cursor.row-1 < st.firstx + st.rows; } /** * Ensure that the given coordinate is visible. *

* If the given coordinate is visible within the screen nothing happens. * Otherwise the screen is scrolled so that the given target ends up * approximately mid-sceeen. */ public void possiblyNormalize(Coord target) { if (target == null) return; BCoord btarget = target.toBCoord(firsta); if (btarget.row >= st.firstx && btarget.row < st.firstx + st.rows) return; st.firstx = btarget.row - (st.rows/2); if (st.firstx < 0) st.firstx = 0; else if (st.firstx + st.rows > buf.nlines) st.firstx = buf.nlines - st.rows; repaint(true); } /** * Ensure that maximum of the given region is visible. *

* If the given region is visible within the screen nothing happens. * If the region is bigger than the screen height, first line of the region * will be displayed in first line of screen and end of region * won't be displayed. * If the region is bigger than the half of screen height, last line * of the region will be displayed in the last line of the screen. * Otherwise the region will start approximately in mid-sceeen */ public void possiblyNormalize(ActiveRegion region) { if (region == null) return; BCoord bbegin = region.begin.toBCoord(firsta); BCoord bend = region.end.toBCoord(firsta); if ( bbegin.row >= st.firstx && bend.row < st.firstx + st.rows ) return; if ( bend.row - bbegin.row + 1 >= st.rows ) // region has more rows then screen st.firstx = bbegin.row; else { st.firstx = bbegin.row - (st.rows/2); if (st.firstx < 0) st.firstx = 0; else if (st.firstx + st.rows > buf.nlines) st.firstx = buf.nlines - st.rows; else if (st.firstx + st.rows <= bend.row) // display also end of region st.firstx = bend.row - st.rows + 1; } repaint(true); } /** * If the given coordinate is visible within the screen returns true, * otherwise returns false. */ public boolean isCoordVisible(Coord target) { BCoord btarget = target.toBCoord(firsta); if (btarget.row >= st.firstx && btarget.row < st.firstx + st.rows) return true; else return false; } private void possiblyScrollOnInput() { if (!scroll_on_input) return; // vertical (row) dimension if (st.cursor.row >= st.firstx && st.cursor.row < st.firstx + st.rows) { ; } else { st.firstx = buf.nlines - st.rows; repaint(true); } } /* * Horizontal scrolling to track the cursor. * * The view/buffer etc calculations, performed by possiblyHScroll(), are not * terribly complex, but they do depend on up-to-date cursor position. * If we were to do all this work on every character received we would * get some slowdown, but worse if say a large file with long lines is * being 'cat'ed Term will end up scrolling left and right. It would be * rather annoying. * But attempting to do the scrolling on typed input isn't going to do * because (because of non-local echoing) the cursor isn't yet updated when * a key has been pressed. * Additionally, a key may result in more than character being echoed. * for example, an ENTER might result in a "\n\r", a TAB in 8 ' 's, * and a '\b' in a "\b \b" sequence so a single flag won't do. * Instead we use a count, 'hscroll_count' which says: attempt * possiblyHScroll() on the next received characters. It may happen * there there is a lot of keyboard input and very little output. In * such cases 'hscroll_count' will start accumulating a defecit. To * counter this we reset the count to 8 on "newline" type stuff. */ private int hscroll_count = 0; private void hscrollReset(char c) { ckEventDispatchThread(); // OLD NPE-x synchronized(hscroll_lock) { if (c == '\n' || c == '\r') hscroll_count = 8; else hscroll_count += 8; } } private void possiblyHScroll() { // decide if we actually need to do it if (!horizontally_scrollable) return; ckEventDispatchThread(); // OLD NPE-x synchronized(hscroll_lock) { if (hscroll_count > 0) { hscroll_count--; } else { return; } } /* DEBUG System.out.println("Checking hscroll cursor.col " + st.cursor.col + // NOI18N " firsty " + st.firsty // NOI18N " visibleCols " + buf.visibleCols()); // NOI18N */ // horizontal (col) dimension if (st.cursor.col >= st.firsty + buf.visibleCols()) { /* DEBUG System.out.println("Need to scroll right"); // NOI18N */ st.firsty = st.cursor.col - buf.visibleCols() + 1; // Expand our idea of column count so that if the cursor is // at the end the scrollbar allows us to scroll to it. // This is a bit unusual in that if we type stuff right up to the // last visible column and return the hscrollbar will display this // one extra column even though nothing was ever typed in it. // This is particularly annoying in 'vi' with right-butting lines. // In the future SHOULD think up of something "smart". buf.noteColumn(st.cursor.col+1); repaint(true); } else if (st.cursor.col - buf.visibleCols() < st.firsty) { /* DEBUG System.out.println("Need to scroll left"); // NOI18N */ st.firsty = st.cursor.col - buf.visibleCols() + 1; if (st.firsty < 0) st.firsty = 0; else repaint(true); } } /** * Set the display attribute for characters printed from now on. *

* Unrecognized values silently ignored. */ public void setAttribute(int value) { st.attr = Attr.setAttribute(st.attr, value); } /** * Set or unset the display attribute for characters from 'begin' to 'end' * inclusive to 'value' depending on the value of 'on'. *

* Will cause a repaint. */ public void setCharacterAttribute(Coord begin, Coord end, final int value, final boolean on ) { visitLines(begin, end, false, new LineVisitor() { public boolean visit(Line l, int brow, int bcol, int ecol) { l.setCharacterAttribute(bcol, ecol, value, on); return true; } } ); repaint(false); } /* * Set the glyph and line background colors for the line the cursor is on. *

* Will not repaint. */ public void setGlyph(int glyph_id, int background_id) { Line l = cursor_line(); l.glyph_glyph = glyph_id; l.glyph_rendition = background_id; } /* * Set the glyph and line background colors for the give line. *

* Will repaint. */ public void setRowGlyph(int row, int glyph_id, int background_id) { Coord c = new Coord(); c.row = row; c.col = 0; BCoord b = c.toBCoord(firsta); Line l = buf.lineAt(b.row); if (l == null) return; l.glyph_glyph = glyph_id; l.glyph_rendition = background_id; possibly_repaint(false); } /** * Adjust lines when the widget is resized (and also at construction time) *

* When growing makes sure that everything in the screen is backed up * by a buffer Line. When shrinking removes lines from the top or the * bottom as appropriate. *

* In other words implements the xterm resizeGravity=SouthWest semantics. * which roughly says: When resizing keep the line that was at the bottom, * _at_ the bottom. */ private void adjust_lines(int delta_row) { if (delta_row > 0) { // attempt to scroll st.firstx -= delta_row; // SHOULD eliminate the loop and move the work to Buffer while (st.firstx < 0) { buf.appendLine(); st.firstx++; } } else if (delta_row < 0) { // we shrunk int orows = st.rows - delta_row; // reconstruct orows // First attempt to remove lines from the bottom of the buffer. // This comes into play mostly when you have just started Term // and have but few lines near the top and nothing in history // so you really can't scroll. // How many lines can we trim at the bottom before we eat // into the cursor? // Really weird I seem to get the same results regardless of // whether I use orows or buf.nlines. SHOULD investigate more. int allowed = buf.nlines - st.cursor.row - 1; if (allowed < 0) allowed = 0; int delete_from_bottom; if (allowed > -delta_row) { // no need to scroll, we accomodate the whole resize by // removing lines from the bottom delete_from_bottom = -delta_row; } else { // delete as much as we're allowed ... delete_from_bottom = allowed; // ... and scroll for the rest st.firstx += (-delta_row - allowed); } // SHOULD eliminate the loop and move the work to Buffer while (delete_from_bottom-- > 0) { buf.removeLineAt(buf.nlines-1); } } // printStats("From delta_row of " + delta_row); // NOI18N adjust_scrollbar(); } /** * returns curent history size of buffer. */ public int getHistoryBuffSize () { return buf.nlines - st.rows; } private void limit_lines() { /* * Make sure we don't exceed the buffer size limit historySize. * This implements the vanishing of lines from the beginning of history. */ if (anchored) return; int history = buf.nlines - st.rows; if (history < 0) history = 0; int toremove = history - history_size; if (toremove > 0) { int charsRemoved = buf.removeLinesAt(0, toremove); charsInPrehistory += charsRemoved; // relocate all row indices st.adjust(-toremove); firsta += toremove; // cull any regions that are no longer in history if (++cull_count % cull_frequency == 0) { /* DEBUG System.out.println("Culling regions ..."); // NOI18N */ region_manager.cull(firsta); } // our absolute coordinates are about to wrap if (firsta + buf.nlines >= modulo) { int old_firsta = firsta; firsta = 0; sel.relocate(old_firsta, firsta); region_manager.relocate(old_firsta, firsta); } } // deal with selection moving out of the buffer sel.adjust(firsta, 0, firsta + buf.nlines); adjust_scrollbar(); } /** * Scroller is used to implement selection auto-scrolling. *

* When a selection drag moves out of the window a scroller object/thread * is started to periodically scroll and extend the selection. */ private class Scroller extends Thread { public final static int UP = 1<<1; public final static int DOWN = 1<<2; public final static int LEFT = 1<<3; public final static int RIGHT = 1<<4; private int direction; public Scroller(int direction) { this.direction = direction; } private boolean extend() { ckEventDispatchThread(); // OLD NPE-x synchronized (Term.this) { // Selection might wink out while we're auto scrolling. // Since we use 'sel.sel_extent' further below we // synchronize. if (sel.sel_extent == null) return false; BCoord x = sel.sel_extent.toBCoord(firsta); BCoord v = toViewCoord(x); int r = v.row; int c = v.col; if ((direction & DOWN) == DOWN) { lineDown(1); r = st.rows-1; c = buf.totalCols(); } else if ((direction & UP) == UP) { lineUp(1); r = 0; c = 0; } else { BCoord vc2 = toViewCoord(drag_point); r = vc2.row; c = vc2.col; } if ((direction & LEFT) == LEFT) { st.firsty --; if (st.firsty < 0) st.firsty = 0; c = 0; } else if ((direction & RIGHT) == RIGHT) { st.firsty ++; int limit = buf.totalCols() - buf.visibleCols(); if (limit < 0) limit = 0; if (st.firsty > limit) st.firsty = limit; c = st.firsty + buf.visibleCols(); } BCoord vc = new BCoord(r, c); BCoord bc = toBufCoords(vc); sel.track(new Coord(bc, firsta)); repaint(true); return true; } } public void run() { while (true) { /* DEBUG System.out.print("Scrolling "); // NOI18N if ((direction & UP) == UP) System.out.print("UP "); // NOI18N if ((direction & DOWN) == DOWN) System.out.print("DOWN "); // NOI18N if ((direction & LEFT) == LEFT) System.out.print("LEFT "); // NOI18N if ((direction & RIGHT) == RIGHT) System.out.print("RIGHT "); // NOI18N System.out.println(); */ extend(); try { // See issue 36404 sleep(10); } catch (InterruptedException x) { break; } } /* DEBUG System.out.println("Done with Scrolling"); // NOI18N */ } } private Scroller scroller; private Point drag_point; private int scrolling_direction = 0; private void scroll_to(int direction, MouseEvent e) { if (direction == scrolling_direction) { if (direction == 0) { // We're moving inside the view BCoord bc = toBufCoords(toViewCoord(e.getPoint())); sel.track(new Coord(bc, firsta)); repaint(false); } return; } // we changed direction // get rid of the old scroller if (scroller != null) { scroller.interrupt(); scroller = null; } if (direction == 0) { BCoord bc = toBufCoords(toViewCoord(e.getPoint())); sel.track(new Coord(bc, firsta)); repaint(false); } else { scroller = new Scroller(direction); scroller.start(); } scrolling_direction = direction; } /** * Constructor */ public Term() { st.rows = 25; st.firstx = 0; st.firsty = 0; standard_color[0] = Color.black; standard_color[1] = Color.red; standard_color[2] = Color.green; standard_color[3] = Color.yellow; standard_color[4] = Color.blue; standard_color[5] = Color.magenta; standard_color[6] = Color.cyan; standard_color[7] = Color.white; custom_color[0] = Color.black; custom_color[1] = Color.black; custom_color[2] = Color.black; custom_color[3] = Color.black; custom_color[4] = Color.black; custom_color[5] = Color.black; custom_color[6] = Color.black; custom_color[7] = Color.black; Font f = UIManager.getFont ("TextArea.font"); //NOI18N if (f == null) { // on, e.g., GTK L&F f = UIManager.getFont ("controlFont"); //NOI18N } if (f != null) { setFont(new Font("Monospaced", Font.PLAIN, f.getSize() + 1)); // NOI18N } else { setFont (new Font("Monospaced", Font.PLAIN, 12)); // NOI18N } BorderLayout layout = new BorderLayout(); setLayout(layout); screen = new Screen(this, (buf.visibleCols() * metrics.width + glyph_gutter_width + debug_gutter_width), st.rows * metrics.height); add(BorderLayout.CENTER, screen); screen.setBackground(null); // inherit from this screen.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); adjust_lines(st.rows); st.cursor.row = 0; vscroll_bar = new JScrollBar(JScrollBar.VERTICAL); add(BorderLayout.EAST, vscroll_bar); vscroll_bar.setValues(st.firstx, st.rows-1, 0, st.rows); vscroll_bar.setUnitIncrement(1); vscroll_bar.setEnabled(true); vscroll_bar.setFocusable(false); vscroll_bar.addAdjustmentListener(new AdjustmentListener() { public void adjustmentValueChanged(AdjustmentEvent e) { JScrollBar sb = (JScrollBar) e.getAdjustable(); switch(e.getAdjustmentType()) { case AdjustmentEvent.TRACK: int pos = e.getValue(); // adjustmentValueChanged() will get called when we // call setValues() on the scrollbar. // This test sort-of filters that out if (pos == st.firstx) break; // deal with the user moving the thumb st.firstx = pos; /* DEBUG if (st.firstx + st.rows > buf.nlines) { Thread.dumpStack(); printStats("bad scroll value"); // NOI18N } */ repaint(false); break; default: /* DEBUG System.out.println("adjustmentValueChanged: " + e); // NOI18N */ break; } } } ); hscroll_bar = new JScrollBar(JScrollBar.HORIZONTAL); hscroll_bar.setValues(st.firsty, buf.totalCols()-1, 0, buf.totalCols()); hscroll_bar.setUnitIncrement(1); hscroll_bar.setEnabled(true); hscroll_bar.setFocusable(false); hscroll_wrapper = new ScrollWrapper(hscroll_bar); add(BorderLayout.SOUTH, hscroll_wrapper); hscroll_bar.addAdjustmentListener(new AdjustmentListener() { public void adjustmentValueChanged(AdjustmentEvent e) { JScrollBar sb = (JScrollBar) e.getAdjustable(); switch(e.getAdjustmentType()) { case AdjustmentEvent.TRACK: int pos = e.getValue(); // adjustmentValueChanged() will get called when we // call setValues() on the scrollbar. // This test sort-of filters that out if (pos == st.firsty) break; // deal with the user moving the thumb st.firsty = pos; repaint(false); break; default: /* DEBUG System.out.println("adjustmentValueChanged: " + e); // NOI18N */ break; } } } ); screen.addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { Dimension size = screen.getSize(); sizeChanged(size.width, size.height); // workaround for java bug 4711314 where a repaint() // appears before componentResized() causing issue 25313 repaint(true); } } ); screen.addKeyListener(new KeyListener() { // We consume all events so no additional side-effects take place. // Examples: // - We don't want TAB to move focus away. (see more below) // - We don't want ^p to bring up a printer diaolog // - etc // HACK. Java maps [Return] to 10 as opposed to 13. This is due // to it's Windows-chauvinism, a 'reality' pointed out to me by // one of the AWT people. // The keycode doesn't make it to keyTyped, so for now we detect // a Return by capturing press/releases of VK_ENTER and // use a flag. boolean saw_return; public void keyTyped(KeyEvent e) { char c = e.getKeyChar(); if (debugKeys()) System.out.println("term: keyTyped: " + e); // NOI18N if (read_only) return; if (c == 10 && saw_return) { saw_return = false; c = (char) 13; } if (passOn && maybeConsume(e)) { on_char(c); possiblyScrollOnInput(); } hscrollReset(c); } public void keyPressed(KeyEvent e) { /* DEBUG System.out.println("keyPressed " + e); // NOI18N */ switch (e.getKeyCode()) { case KeyEvent.VK_COPY: copyToClipboard(); break; case KeyEvent.VK_PASTE: pasteFromClipboard(); break; case KeyEvent.VK_ENTER: saw_return = true; break; case KeyEvent.VK_PAGE_UP: if ( e.getModifiers() == Event.CTRL_MASK ) { pageLeft(1); } else { pageUp(1); } break; case KeyEvent.VK_PAGE_DOWN: if ( e.getModifiers() == Event.CTRL_MASK ) { pageRight(1); } else { pageDown(1); } break; case KeyEvent.VK_UP: if (e.getModifiers() == Event.CTRL_MASK) { lineUp(1); } break; case KeyEvent.VK_DOWN: if (e.getModifiers() == Event.CTRL_MASK) { lineDown(1); } break; } passOn = maybeConsume(e); } public void keyReleased(KeyEvent e) { /* DEBUG System.out.println("keyReleased"); // NOI18N */ if (e.getKeyCode() == KeyEvent.VK_ENTER) { /* DEBUG System.out.println("keyReleased VK_ENTER"); // NOI18N */ saw_return = false; } maybeConsume(e); } }); screen.addMouseMotionListener(new MouseMotionListener() { public void mouseDragged(MouseEvent e) { /* DEBUG System.out.println("mouseDragged"); // NOI18N */ // akemr - fix of #25648 if ( SwingUtilities.isRightMouseButton( e ) ) return; if (left_down_point != null) { BCoord bc = toBufCoords(toViewCoord(left_down_point)); sel.track(new Coord(bc, firsta)); left_down_point = null; } drag_point = e.getPoint(); /* DEBUG System.out.println("mouseDrag: " + drag_point); // NOI18N */ int scroll_direction = 0; if (drag_point.y < 0) { scroll_direction |= Scroller.UP; } else if (drag_point.y > screen.getSize().height) { scroll_direction |= Scroller.DOWN; } if (drag_point.x < 0) { scroll_direction |= Scroller.LEFT; } else if (drag_point.x > screen.getSize().width) { scroll_direction |= Scroller.RIGHT; } scroll_to(scroll_direction, e); } public void mouseMoved(MouseEvent e) { /* DEBUG Point p = (Point) e.getPoint().clone(); BCoord bc = toBufCoords(toViewCoord(p)); Coord c = new Coord(bc, firsta); Extent x = sel.getExtent(); if (x == null) { System.out.println("sel intersect: no extent"); // NOI18N } else { System.out.println("sel intersect: " + // NOI18N (x.intersects(c.row, c.col)? "intersects" // NOI18N "doesn't intersect")); // NOI18N } */ } }); addMouseWheelHandler( screen, vscroll_bar ); screen.addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent e) { BCoord bcoord = toBufCoords(toViewCoord(e.getPoint())); if (SwingUtilities.isLeftMouseButton(e)) { /* DEBUG System.out.println("LEFT click"); // NOI18N */ if (click_to_type) requestFocus(); } else if ( SwingUtilities.isMiddleMouseButton(e)) { /* DEBUG System.out.println("MIDDLE click"); // NOI18N System.out.println("Selection: '" + sel.sel_get() + "'"); // NOI18N */ pasteFromSelection(); } } public void mousePressed(MouseEvent e) { /* DEBUG System.out.println("mousePressed "+e.getModifiers()); // NOI18N */ if (SwingUtilities.isLeftMouseButton(e)) { if (e.isShiftDown()) { // JLF/dtterm selection extension // Actually it's _addition_ so SHOULD enhance Sel // to do that instead. BCoord bc = toBufCoords(toViewCoord(e.getPoint())); if (sel.extend(new Coord(bc, firsta))) { fireSelectionExtentChanged(); repaint(false); } return; } if (sel.cancel(false)) repaint(false); if (e.getClickCount() == 1) { left_down_point = (Point) e.getPoint().clone(); } else if (e.getClickCount() == 2) { BCoord bcoord = toBufCoords(toViewCoord(e.getPoint())); BExtent word = buf.find_word(word_delineator, bcoord); sel.select_word(word.toExtent(firsta)); repaint(false); } else if (e.getClickCount() == 3) { BCoord bcoord = toBufCoords(toViewCoord(e.getPoint())); sel.select_line(new Coord(bcoord, firsta)); repaint(false); } fireSelectionExtentChanged(); } } public void mouseReleased(MouseEvent e) { /* DEBUG System.out.println("mouseReleased"); // NOI18N */ if (SwingUtilities.isLeftMouseButton(e)) { if (e.isShiftDown()) { // we're extending return; } if (scroller != null) { scroller.interrupt(); scroller = null; } // Don't put in the selection if we didn't move // When left button goes down 'left_down_point' is set. // As soon as we move the mouse it gets reset. if (left_down_point == null) sel.done( /* OLD false */); left_down_point = null; } } /* * Implement follow-mouse focus */ public void mouseEntered(MouseEvent e) { /* DEBUG System.out.println("mouseEntered"); // NOI18N */ if (!click_to_type) { requestFocus(); } } public void mouseExited(MouseEvent e) { /* DEBUG System.out.println("mouseExited"); // NOI18N */ } }); screen.addFocusListener(new FocusListener() { public void focusGained(FocusEvent e) { /* DEBUG System.out.println("Focus gained >>>>>>>>>>>>>>>>>>>>>>>"); // NOI18N */ has_focus = true; repaint(false); } public void focusLost(FocusEvent e) { /* DEBUG Component o = e.getOppositeComponent(); System.out.println("Focus lost <<<<<<<<<<<<<<<<<<<<<<<" + o); */ has_focus = false; repaint(false); } } ); } /* * Print interesting statistics and facts about this Term */ public void printStats(String message) { if (message != null) System.out.println(message); if (message != null) System.out.print("\t"); // NOI18N buf.printStats(); if (message != null) System.out.print("\t"); // NOI18N System.out.println("rows " + st.rows + // NOI18N " v cols " + buf.visibleCols() + // NOI18N " t cols " + buf.totalCols() + // NOI18N " history " + history_size + // NOI18N " firstx " + st.firstx + // NOI18N " firsty " + st.firsty + // NOI18N " firsta " + firsta + // NOI18N " gutter " + glyph_gutter_width); // NOI18N if (message != null) System.out.print("\t"); // NOI18N System.out.println("Cursor " + st.cursor + // NOI18N " topMargin " + topMargin() + // NOI18N " botMargin " + botMargin()); // NOI18N if (message != null) System.out.print("\t"); // NOI18N System.out.println("putChar " + n_putchar + // NOI18N " putChars " + n_putchars + // NOI18N " linefeeds " + n_linefeeds + // NOI18N " repaint " + n_repaint + // NOI18N " paint " + n_paint); // NOI18N } public void paste() { pasteFromClipboard(); } private void pasteHelp(Clipboard cb) { if (read_only) return; Transferable contents = cb.getContents(screen); if (contents == null) return; if (!contents.isDataFlavorSupported(DataFlavor.stringFlavor)) return; try { String string; string = (String) contents.getTransferData(DataFlavor.stringFlavor); /* DEBUG System.out.println("System selection contains '" + string + "'"); // NOI18N */ char ca[] = string.toCharArray(); sendChars(ca, 0, ca.length); } catch (Exception e) { ; } } /** * Transfer contents of selection to Terms input stream. *

* The pasted content is sent through sendChars. *
* The paste will silently fail if: *

    *
  • The selection has null contents. *
  • The selections data flavor is not string. *
*/ public void pasteFromSelection() { /* DEBUG System.out.println("Term: pasteFromSelection()"); // NOI18N */ if (systemSelection == null) return; pasteHelp(systemSelection); } /** * Transfer contents of clipboard to Terms input stream. *

* The pasted content is sent through sendChars. *
* The paste will silently fail if: *

    *
  • The selection has null contents. *
  • The selections data flavor is not string. *
*/ public void pasteFromClipboard() { pasteHelp(systemClipboard); } /** * Transfer selected text into clipboard. */ public void copy() { copyToClipboard(); } /** * Transfer selected text into clipboard. */ public void copyToClipboard() { String text = sel.getSelection(); if (text != null) { StringSelection ss = new StringSelection(text); systemClipboard.setContents(ss, sel); } } /** * Transfer selected text into selection. */ public void copyToSelection() { /* DEBUG System.out.println("Term: copyToSelection()"); // NOI18N */ if (systemSelection == null) return; String text = sel.getSelection(); StringSelection ss = new StringSelection(text); // null 'text' is OK systemSelection.setContents(ss, sel); } private static Extent old_extent = null; void fireSelectionExtentChanged() { Extent new_extent = getSelectionExtent(); firePropertyChange("selectionExtent", old_extent, new_extent); old_extent = new_extent; } /** * Set the number of character rows in the screen. *

* See setRowsColumns() for some additional important information. */ public void setRows(int rows) { if (old_rows == -1) old_rows = st.rows; st.rows = rows; updateScreenSize(); } /** * Get the number of character rows in the screen */ public int getRows() { return st.rows; } /** * Set the number of character columns in the screen. *

* See setRowsColumns() for some additional important information. */ public void setColumns(int cols) { buf.setVisibleCols(cols); updateScreenSize(); } /** * Trampoline from Line.ensureCapacity() to Buffer.noteColumn() */ void noteColumn(Line l, int capacity) { int vcapacity = l.bufToCell(metrics, capacity); buf.noteColumn(vcapacity); } /** * Trampoline from Line to MyFontMetrics.checkForMultiCell() */ void checkForMultiCell(char c) { metrics.checkForMultiCell(c); } /** * Get the number of character columns in the screen */ public int getColumns() { return buf.visibleCols(); } /** * Simultaneously set the number of character rows and columns. *

* A Term is a composite widget made of a contained screen (getScreen()) * and a scrollbar. It is not a JScrollPane. You're actually setting * the screen size here. *

* Setting the column size also sets the width of the buffer. This doesn't * alter the length (column at which lines were wrapped) of past lines, * but only new additional lines. For example, if you set columns to * 20, print a bunch of lines that wrap, then resize to 80 columns, all * the lines that were wrapped to 20 will stay wrapped that way. This is * consistent with xterm behaviour. *

* If this Term is embedded in a component with a layout manager that is * set up to accept child resizes gracefully this widget will be resized * as expected. *

* Alternatively if this Term is embedded in a Window (like JFrame) * the window will need to be re-pack()ed as it does not accomodate it's * childrens size changes. This has to be done by the application using * Term. The best way to do this is to add a TermListener() and call pack() * on the appropriate window when a resize notification is fired. */ public void setRowsColumns(int rows, int columns) { // Combine code in setRows() and setColumns() so we factor // the calls to updateScreenSize(). if (old_rows == -1) old_rows = st.rows; st.rows = rows; buf.setVisibleCols(columns); updateScreenSize(); } /** * Governs whether Term will round down resize requests to character * cell size. *

* Sizes are usually set by containers' layout managers. If rounding is * enabled Term will attempt to adjust the size to an even multiple * of the character cell size. *

* The layout manager might not neccesarily honor the rounded size. * The situation can be somewhat improved by making sure that the ultimate * container is re-packed as described in {@link #setRowsColumns(int, int)}. */ public void setSizeRounded(boolean size_rounded) { this.size_rounded = size_rounded; updateScreenSize(); } /** * Returns true if Term will round down resize requests to character * cell size. *

* See {@link #setSizeRounded(boolean)} for more info. */ public boolean isSizeRounded() { return size_rounded; } private boolean size_rounded = true; /* * Used to extract the dimensions of the terminal. * SHOULD replace with getRowsCols and getScreenSize */ public void fillSizeInfo(Dimension cells, Dimension pixels) { cells.height = st.rows; cells.width = buf.visibleCols(); Dimension cpixels = screen.getSize(); pixels.width = cpixels.width - glyph_gutter_width - debug_gutter_width; pixels.height = cpixels.height; } /** * Once the terminal is connected to something, use this function to * send all Term Listener notifications. */ protected void updateTtySize() { if (screen != null) { Dimension cells = new Dimension(buf.visibleCols(), st.rows); Dimension pixels = screen.getSize(); fireSizeChanged(cells, pixels); } } /* * various coordinate conversion functions */ BCoord toViewCoord(BCoord b) { /* * Convert from buffer coords to view coords */ Line l = buf.lineAt(b.row); if (l != null) { //XXX Hotfix for issue 40189 - Buffer.lineAt() will //catch an AIOBEE and return null. Probably related //to Ivan's notes about clipping in Term.paint() int vc = buf.lineAt(b.row).bufToCell(metrics, b.col); BCoord v = new BCoord(b.row - st.firstx, vc - st.firsty); return v; } else { return null; } } Point toPixel(BCoord v) { /* * Convert from view coords to pixel coords */ Point p = new Point(v.col * metrics.width + glyph_gutter_width + debug_gutter_width, v.row * metrics.height); return p; } /** * Convert row/column coords to pixel coords within the widgets * coordinate system. * It returns the pixel of the upper left corner of the target cell. */ public Point toPixel(Coord target) { BCoord btarget = target.toBCoord(firsta); return toPixel( btarget ); } /** * Convert pixel coords to view row/column coords (0/0-origin) */ BCoord toViewCoord(Point p) { BCoord v = new BCoord(p.y / metrics.height, (p.x - glyph_gutter_width - debug_gutter_width) / metrics.width); v.clip(st.rows, buf.visibleCols()); /* DEBUG System.out.println("toViewCoord() -> " + v); // NOI18N */ return v; } BCoord toBufCoords(BCoord v) { /* * Convert view row/column coords to buffer row/column coords. * If the buffer is smaller than the view, map to the last line. */ int brow = st.firstx + v.row; if (brow >= buf.nlines) brow = buf.nlines-1; int bc = buf.lineAt(brow).cellToBuf(metrics, st.firsty + v.col); BCoord b = new BCoord(brow, bc); /* DEBUG System.out.println("toBufCoords(" + v + ") -> " + b); // NOI18N */ return b; } /** * Convert pixel coords to view (visible area) row/column coords (both * o-origin). *

* In the returned Point, x represents the column, y the row. */ public Point mapToViewRowCol(Point p) { BCoord c = toViewCoord(p); return new Point(c.col, c.row); } /** * Convert pixel coords to (history) buffer row/column coords (both * 0-origin). *

* In the returned Point, x represents the column, y the row. */ public Point mapToBufRowCol(Point p) { BCoord c = toBufCoords(toViewCoord(p)); return new Point(c.col, c.row); } private Color rendition_to_color(int rendition) { switch (rendition) { case 40: return standard_color[0]; case 41: return standard_color[1]; case 42: return standard_color[2]; case 43: return standard_color[3]; case 44: return standard_color[4]; case 45: return standard_color[5]; case 46: return standard_color[6]; case 47: return standard_color[7]; case 58: return custom_color[0]; case 59: return custom_color[1]; case 60: return custom_color[2]; case 61: return custom_color[3]; case 62: return custom_color[4]; case 63: return custom_color[5]; case 64: return custom_color[6]; case 65: return custom_color[7]; default: return null; } } private Color actual_foreground; private Color actual_background; private boolean check_selection; private int totcols; private void do_run(Graphics g, int yoff, int xoff, int baseline, int brow, char buf[], Line l, int attr, int rbegin, int rend) { /* DEBUG System.out.println("do_run(" + rbegin + ", " + rend + ")"); // NOI18N */ int x; int rlength; int xlength; if (metrics.isMultiCell()) { int vbegin = l.bufToCell(metrics, rbegin); int vend = l.bufToCell(metrics, rend+1)-1; x = xoff + (vbegin - st.firsty) * metrics.width; int vlength = vend - vbegin + 1; if (vlength <= 0) { /* DEBUG System.out.println("do_run(" + rbegin + ", " + rend + ")"); // NOI18N */ return; } rlength = rend - rbegin + 1; xlength = vlength * metrics.width; } else { x = xoff + (rbegin - st.firsty) * metrics.width; rlength = rend - rbegin + 1; if (rlength <= 0) { /* DEBUG System.out.println("do_run(" + rbegin + ", " + rend + ")"); // NOI18N */ return; } xlength = rlength * metrics.width; } boolean reverse = ((attr & Attr.REVERSE) == Attr.REVERSE); boolean active = ((attr & Attr.ACTIVE) == Attr.ACTIVE); // choose background color Color bg = null; if (active) { bg = active_color; } else { bg = backgroundColor(reverse, attr); } if (bg != null) { // Draw any background g.setColor(bg); g.fillRect(x, yoff, xlength, metrics.height - metrics.leading); } // Set foreground color Color fg = foregroundColor(reverse, attr); g.setColor(fg); // draw any underscores if ( (attr & Attr.UNDERSCORE) == Attr.UNDERSCORE) { int h = metrics.height - metrics.leading - 1; g.drawLine(x, yoff+h, x + xlength, yoff+h); } // Draw the foreground character glyphs myDrawChars(g, buf, l, rbegin, rlength, x, baseline); // Draw fake bold characters by redrawing one pixel to the right if ( (attr & Attr.BRIGHT) == Attr.BRIGHT) { myDrawChars(g, buf, l, rbegin, rlength, x+1, baseline); } } private final Point newp = new Point(); /* * Tweak glyph X positions so they fall on cell/grid/column boundries. */ private void massage_glyphs(GlyphVector gv, int start, int n, Line l) { Point2D pos0 = gv.getGlyphPosition(0); // There's one big assumption here that in a monospaced font all the // Y placements are identical. So we use the placement for the first // glyph only. newp.y = (int) pos0.getY(); int col = (int) pos0.getX(); for (int gx = 0; gx < n; gx++) { newp.x = col; gv.setGlyphPosition(gx, newp); col += l.width(metrics, start + gx) * metrics.width; } } /** * Draw characters in cells. * * Fixed width or monospaced fonts implies that the glyphs of all characters * have the same width. Some non-latin characters (japanese) might have * glyph widths that are an _integer multiple_ of the latin glyphs. Thus * cellular (grid based) text widget like this termulator can still place * all characters nicely. There is a 'C' function wcwidth() which * ... determines the number of _column_ positions ... and CDE's DtTrem * ultimately depends on it to place things. (See also Tuthill & Smallberg, * "Creating worldwide software" PrenticeHall 2nd ed. p98) * * Unfortunately the fonts used by Java, even the "monospaced" fonts, do * not abide by the above convention. I measured a 10pt ja locale latin * character at 7 pixels wide and a japanese character at 12 pixels wide, * instead of 14. A similar problem existed with respect to the "unprintbale" * placeholder square. Until Java 1.4 it used to be 9 or 10 pixels wide! * The square is fixed, but I"m not sure the above will be anytime soon. * * What this means is that Graphics.drawString() when given a mix and match * of latin and japanese characters will not place them right. Selection * doesn't work etc. * * Nor does Java provide anything resembling wcwidth() so we're rolling * our own here. That's done in Line.width(). * * So one approach would be to place each character individually, but it's * rather slow. Fortunately Java provides a GlyphVector class that allows * us to tweak the positions of the glyphs. The timing I"ve gotten are * 50 for one drawChars() per charactr. (SLOWER below) * 15 using the GlyphVector technique * 8 using plain drawChars * Unfortunately GlyphVector's interface leaves a bit to be desired. * - It does not take a (char [], offset, length) triple and depends * on the length of the char array passed in. Since our Line char arrays * have some slop in them we can't pass them directly. Hence the * "new char[]" and the "System.arraycopy". * - The interface for getting and setting positions is also a bit * awkward as you may notice from massage_glyphs(). * * We SHOULD fall back on plain drawChars() if the host charset is an * 8 bit encoding like ASCII or ISO 8859. This encoding is available * via System.getProperty("file.encoding") but there are so many aliases * for each that I"m wary of hardcoding tests. See * http://www.iana.org/assignments/character-sets * Java 1.4 has class Charset that helps with the aliases but we can't * yet lock into 1.4. */ private void myDrawChars(Graphics g, char buf[], Line l, int start, int howmany, int xoff, int baseline) { if (metrics.isMultiCell()) { // slow way // This looks expensive but it is in fact a whole lot faster // than issuing a g.drawChars() _per_ character Graphics2D g2 = (Graphics2D) g; FontRenderContext frc = g2.getFontRenderContext(); // Gaaah, why doesn't createGlyphVector() take a (char[],offset,len) // triple? char[] tmp = new char[howmany]; System.arraycopy(buf, start, tmp, 0, howmany); GlyphVector gv = getFont().createGlyphVector(frc, tmp); massage_glyphs(gv, start, howmany, l); g2.drawGlyphVector(gv, xoff, baseline); } else { // fast way g.drawChars(buf, start, howmany, xoff, baseline); } } /* * Render one line * Draw the line on this brow (buffer row 0-origin) */ private void paint_line_new(Graphics g, Line l, int brow, int xoff, int yoff, int baseline, Extent selx) { int length = l.length(); if (length == 0) return; int lastcol; int firstcol; if (metrics.isMultiCell()) { // Figure what buffer column is the first visible one (moral // equivalent of st.firsty) // SHOULD replace with something that does cellToBuf/bufToCell // all at once. There are a couple of other occurances of this // pattern. firstcol = l.cellToBuf(metrics, st.firsty); int inverse_firstcol = l.bufToCell(metrics, firstcol); int delta = st.firsty - inverse_firstcol; if (delta > 0) { /* This is what to do if we want to draw the right half of the * glyph. However the left half of it will end up in the glyph * gutter and to compensate for thet we'll need to tweak the * clip region. For now taking the easy way out> int pdelta = delta * metrics.width; // pixel delta xoff -= pdelta; */ firstcol++; int pdelta = delta * metrics.width; // pixel delta xoff += pdelta; } lastcol = l.cellToBuf(metrics, st.firsty + buf.visibleCols() - 1); /* DEBUG System.out.print ("firstcol = " + firstcol + " for firsty " + st.firsty); // NOI18N System.out.print (" delta = " + delta); // NOI18N System.out.println (" lastcol = " + lastcol + // NOI18N " for visibleCols " + buf.visibleCols()); // NOI18N */ } else { lastcol = st.firsty + buf.visibleCols() - 1; firstcol = st.firsty; } lastcol = Math.min(lastcol, length-1); if (firstcol > lastcol) return; int howmany = lastcol - firstcol + 1; // 'length' is not used from here on down char buf[] = l.charArray(); if (! l.hasAttributes()) { if (debugWrap()) { if (l.isWrapped() && l.isAboutToWrap()) g.setColor(Color.red); // not a good state to be in else if (l.isAboutToWrap()) g.setColor(Color.orange); else if (l.isWrapped()) g.setColor(Color.magenta); } myDrawChars(g, buf, l, firstcol, howmany, xoff, baseline); return; } int attrs[] = l.attrArray(); // find the extent of the selection on this line int sbegin = -1; int send = -1; if (check_selection && selx != null) { int arow = firsta + brow; Coord b = selx.begin; Coord e = selx.end; if (b.row <= arow && e.row >= arow) { if (b.row == e.row) { sbegin = b.col; send = e.col; } else if (arow == b.row) { sbegin = b.col; send = totcols; } else if (arow == e.row) { sbegin = 0; send = e.col; } else { sbegin = 0; send = totcols; } } } // iterate through runs int rbegin = firstcol; int rend = rbegin; while (true) { // find a "run" // A run, as in run-length-encoding, is a set of characters with // the same attributes. int attr = attrs[rbegin]; rend = rbegin+1; while (rend <= lastcol) { if (attrs[rend] != attr) break; rend++; } rend--; // render the run // need to do this with awareness of the selection // parts that fall under the selection are rendered with an // alternative attribute set. int alt_attr = (attr & ~ (Attr.BGCOLOR|Attr.FGCOLOR|Attr.REVERSE|Attr.ACTIVE)); if (sbegin == -1 || send < rbegin || sbegin > rend) { // run is not in selection do_run(g, yoff, xoff, baseline, brow, buf, l, attr, rbegin, rend); } else if (sbegin <= rbegin && send >= rend) { // run entirely in selection /* DEBUG System.out.println("run entirely in selection"); // NOI18N */ do_run(g, yoff, xoff, baseline, brow, buf, l, alt_attr, rbegin, rend); } else if (sbegin > rbegin && send < rend) { // selection fully within run // split into three parts /* DEBUG System.out.println("run selection fully within run"); // NOI18N */ do_run(g, yoff, xoff, baseline, brow, buf, l, attr, rbegin, sbegin-1); do_run(g, yoff, xoff, baseline, brow, buf, l, alt_attr, sbegin, send); do_run(g, yoff, xoff, baseline, brow, buf, l, attr, send+1, rend); } else if (sbegin <= rbegin) { // selection covers left portion of run /* DEBUG System.out.println("selection covers left portion of run"); // NOI18N */ // split into two parts do_run(g, yoff, xoff, baseline, brow, buf, l, alt_attr, rbegin, send); do_run(g, yoff, xoff, baseline, brow, buf, l, attr, send+1, rend); } else if (send >= rend) { // selection covers right portion of run // split into two parts /* DEBUG System.out.println("selection covers right portion of run"); // NOI18N */ do_run(g, yoff, xoff, baseline, brow, buf, l, attr, rbegin, sbegin-1); do_run(g, yoff, xoff, baseline, brow, buf, l, alt_attr, sbegin, rend); } else { /* DEBUG System.out.println("Odd run/selection overlap"); // NOI18N */ } if (rend+1 >= lastcol) break; // shift rbegin = rend + 1; } } /* OLD NPE-x synchronized */ void do_paint(Graphics g) { ckEventDispatchThread(); /* * Render the buffer unto the screen * SHOULD try theses: * - use drawChars? * - make passes first the glyphs and BG, then chars, then cursor * - use drawString(AttributedCharacterIterator iterator, ...) * - precompute any metrics related stuff */ if (st.firstx == -1) { /* DEBUG System.out.println("Term.paint() no lines"); // NOI18N */ return; } /* DEBUG long paint_start_time = System.currentTimeMillis(); */ // If Screen is opaque it seems that there is a bug in Swing where // the Graphics that we get here ends up with fonts other than what // we assigned to Term. So we make doubly sure here. g.setFont(getFont()); n_paint++; if (reverse_video) { actual_foreground = getBackground(); actual_background = getForeground(); } else { actual_foreground = getForeground(); actual_background = getBackground(); } // clear the screen g.setColor(actual_background); g.fillRect(0, 0, screen.getSize().width, screen.getSize().height); // draw any BG stripes // do this before the selection int xoff = debug_gutter_width + glyph_gutter_width; int lx = st.firstx; for (int vrow = 0; vrow < st.rows; vrow++) { Line l = buf.lineAt(lx); if (l == null) break; // don't make a big fuss the loop below will int yoff = metrics.height*vrow; Color background = rendition_to_color(l.glyph_rendition); if (background != null) { int rect_height = metrics.height - metrics.leading; g.setColor(background); g.fillRect(xoff, yoff, screen.getWidth(), rect_height); } lx++; } if (!selection_xor) { // The screen is clear, draw any selections // // If most of the lines are w/o attributes then the text just gets // draw over this. Lines that are attributed end up doing some // redundant work repainting. sel.paint(g); } g.setColor(actual_foreground); Extent selx = sel.getExtent(); check_selection = (selx != null && !selection_xor); totcols = buf.totalCols(); /* DEBUG System.out.println("=========================================="); // NOI18N */ lx = st.firstx; for (int vrow = 0; vrow < st.rows; vrow++) { Line l = buf.lineAt(lx); if (l == null) { /* DEBUG System.out.println("vrow " + vrow + " lx " + lx); // NOI18N */ printStats(null); break; } xoff = 0; int yoff = metrics.height*vrow; int baseline = yoff + metrics.ascent; if (debug_gutter_width > 0) { String buf = "" + (firsta + st.firstx + vrow); // NOI18N g.drawString(buf, xoff, baseline); } xoff += debug_gutter_width; // draw any glyphs that we might have if (glyph_gutter_width > 0) { Image image = glyph_images[l.glyph_glyph]; if (image != null) { // xy passed to drawImage() is the top-left of the image int gyoff = yoff; g.drawImage(image, xoff, gyoff, Color.white, null); } } xoff += glyph_gutter_width; paint_line_new(g, l, vrow + st.firstx, xoff, yoff, baseline, selx); // restore 'g' to something reasonable g.setColor(actual_foreground); lx++; } paint_cursor(g); if (selection_xor) sel.paint(g); if (debugMargins()) paint_margins(g); /* DEBUG long paint_stop_time = System.currentTimeMillis(); long paint_time = paint_stop_time - paint_start_time; System.out.println("paint_time = " + paint_time); // NOI18N */ } private void paint_margins(Graphics g) { } private void paint_cursor(Graphics g) { if (!cursor_visible) return; // figure what row the cursor is on if (st.cursor.row == -1) { // System.out.println("Term.paint_cursor: " + // NOI18N // "cursor doesn't point to any line"); // NOI18N return; } int cursor_row = st.cursor.row - st.firstx; if (cursor_row >= st.rows) { return; // cursor not visible } int cursor_col = st.cursor.col - st.firsty; if (cursor_col >= buf.visibleCols()) { return; // cursor not visible } else if (cursor_col < 0) { return; // cursor not visible } g.setXORMode(actual_background); int rect_x = cursor_col * metrics.width + glyph_gutter_width + debug_gutter_width; int rect_y = cursor_row * metrics.height; // we _don't_ make cursor as wide as underlying character int rect_width = metrics.width; int rect_height = metrics.height - metrics.leading; if (has_focus) g.fillRect(rect_x, rect_y, rect_width, rect_height); else g.drawRect(rect_x, rect_y, rect_width, rect_height); } private final boolean do_margins = true; private boolean possiblyScrollDown() { /* * If cursor has moved below the scrollable region scroll down. * Buffer manipulation is partly done here or at the callsite if * 'true' is returned. */ if (!do_margins) { if (st.cursor.row >= st.firstx + st.rows) { // scroll down st.firstx++; return true; } return false; } else { // new margin based scrolling if (st.cursor.row >= st.firstx + botMargin() + 1) { // scroll down if (topMargin() == 0) { if (scroll_on_output || cursor_was_visible() && track_cursor) st.firstx++; return true; } else { st.cursor.row = st.firstx + botMargin(); Line l = buf.moveLineFromTo(st.firstx + topMargin(), st.cursor.row); l.reset(); return false; } } return false; } } /** * Send a character to the terminal screen. */ public void putChar(char c) { dce_end.putChar(c); } /** * Send several characters to the terminal screen. *
* While 'buf' will have a size it may not be fully filled, hence * the explicit 'nchar'. */ public void putChars(char buf[], int offset, int count) { dce_end.putChars(buf, offset, count); } /** * Force a repaint. *

* Normally a putChar() or putChars() will call repaint, unless ... * setRepaintEnabled() has been called with false. This function * allows for some flexibility wrt to buffering and flushing: *

     *		term.setRefreshEnabled(false);
     *		for (cx = 0; cx < buf.length; cx++) {
     *			term.putChar(c);
     *			if (c % 10 == 0)
     *				term.flush();
     *		}
     *		term.setRefreshEnabled(true);
     * 
*/ public void flush() { dce_end.flush(); } /** * Send a message back to DCE. *

* Perhaps SHOULD lock out sendChar() so user input doesn't interfere. */ private void reply(String str) { /* DEBUG System.out.println("replying " + str); // NOI18N */ for (int sx = 0; sx < str.length(); sx++) sendChar(str.charAt(sx)); } /* * Term used to implement Ops but then that forces Ops to be public * which we don't want. So we do it this way. */ private class OpsImpl implements Ops { public void op_pause() { // This yields slighlty more reasonable results. Thread.currentThread().yield(); /* DtTerm sends ~240 NUL's between reverse video switching to simulate a flash. The resolution of Thread.sleep() isn't really one millisecond so the flash ends up being too long. try { Thread.currentThread().sleep(0, 500); } catch (InterruptedException x) { ; } */ } public void op_char(char c) { if (debugOps()) System.out.println("op_char('" + c + "') = " + (int) c); // NOI18N // generic character printing Line l = cursor_line(); int insertion_col = l.cellToBuf(metrics, st.cursor.col); if (debugOps()) { System.out.println("op_char(): st.cursor.col " + st.cursor.col + // NOI18N " insertion_col " + insertion_col); // NOI18N } if (!st.overstrike) { // This just shifts stuff the actual character gets put in below. l.insertCharAt(Term.this, ' ', insertion_col, st.attr); } int cwidth = metrics.wcwidth(c); if (l.isAboutToWrap() || (cwidth > 1 && st.cursor.col + cwidth > buf.visibleCols() && !horizontally_scrollable)) { // 'wrap' the line if (debugOps()) System.out.println("\twrapping it"); // NOI18N l.setWrapped(true); l.setAboutToWrap(false); op_line_feed(); op_carriage_return(); l = cursor_line(); insertion_col = 0; // Fall thru } l.setCharAt(Term.this, c, insertion_col, st.attr); // overstrike st.cursor.col += cwidth; if (st.cursor.col >= buf.visibleCols() && !horizontally_scrollable) { if (debugOps()) System.out.println("\tabout to wrap"); // NOI18N l.setAboutToWrap(true); st.cursor.col -= cwidth; } } public void op_attr(int attr) { if (debugOps()) System.out.println("op_attr(" + attr + ")"); // NOI18N setAttribute(attr); } public void op_bel() { // ring the bell // SHOULD implement } public void op_back_space() { // back-space if (debugOps()) System.out.println("op_back_space"); // NOI18N if (st.cursor.col > 0) { if (! cursor_line().isAboutToWrap()) { st.cursor.col--; } cursor_line().setAboutToWrap(false); // If we' backed up to column 0, maybe we need to consider // whether the previous line was wrapped. Older xterms aren't // this clever, newer ones (Solaris 8+?) are. if (st.cursor.col == 0) { if (st.cursor.row > 0) { // maybe we had wrapped on the previous line? if (debugOps()) System.out.println("\tchecking if prev is wrapped"); // NOI18N Line prev = buf.lineAt(st.cursor.row-1); if (prev.isWrapped()) { if (debugOps()) System.out.println("\tit is"); // NOI18N st.cursor.row--; // The below is done in a roundabout way because BS doesn't // really reduce length. So, suppose we went to the end with // latin chars that makes the line 80 long. Then we backspace // to column 78 and enter one 2-cell japanese character. Now // the line is conceptually 79 long, but it still remembers // the 80. So we don't use 'prev.length()' directly. // st.cursor.col = prev.bufToCell(metrics, prev.length()-1); int last_col = prev.cellToBuf(metrics, buf.visibleCols()-1); st.cursor.col = prev.bufToCell(metrics, last_col); prev.setWrapped(false); // The following isn't entirely correct when we backspaced // over a multi-celled character. SHOULD either note // what we BS'ed over or note the slop at the end of the line. prev.setAboutToWrap(true); } } } } } public void op_line_feed() { // NL line feed ctrl-J // move cursor down one line and if goes past the screen // add a new line. if (debugOps()) System.out.println("op_line_feed"); // NOI18N Line last_line = cursor_line(); /* DEBUG if (last_line == null) { Thread.dumpStack(); printStats("last_line == null in op_line_feed()");// NOI18N } */ st.cursor.row++; if (possiblyScrollDown()) { buf.addLineAt(st.cursor.row); limit_lines(); if (debugOps()) System.out.println("op_line_feed ADJUSTED"); // NOI18N } // have new line inherit cursorAtEnd boolean atw = last_line.isAboutToWrap(); cursor_line().setAboutToWrap(atw); last_line.setAboutToWrap(false); n_linefeeds++; // See repaint() for an explanation of this. // repaint(false); } public void op_tab() { // TAB/HT // SHOULD do something better with tabs near the end of the line // On the other hand, that's how ANSI terminals are supposed // to behave if (debugOps()) System.out.println("op_tab"); // NOI18N if (st.cursor.col == buf.visibleCols()-1 && !horizontally_scrollable) return; Line l = cursor_line(); int insert_col = l.cellToBuf(metrics, st.cursor.col); l.setCharAt(Term.this, ' ', insert_col, st.attr); st.cursor.col++; insert_col++; // no need to re-apply cellToBuf to cursor since we're only adding 1-wide ' ' while ((st.cursor.col < buf.visibleCols()-1 || horizontally_scrollable) && (st.cursor.col % tab_size) != 0) { cursor_line().setCharAt(Term.this, ' ', insert_col, st.attr); st.cursor.col++; insert_col++; } } public void op_carriage_return() { if (debugOps()) System.out.println("op_carriage_return"); // NOI18N st.cursor.col = 0; cursor_line().setAboutToWrap(false); } public void op_al(int count) { // add new blank line if (debugOps()) System.out.println("op_al(" + count + ")"); // NOI18N Line l; while (count-- > 0) { boolean old_atw = cursor_line().setAboutToWrap(false); // reverse of op_dl() // Rotate a line from bottom to top if (!do_margins) { l = buf.moveLineFromTo(buf.nlines-1, st.cursor.row); } else { l = buf.moveLineFromTo(st.firstx + botMargin(), st.cursor.row); } l.reset(); cursor_line().setAboutToWrap(old_atw); } switch(sel.intersection(st.cursor.row - 1)) { case Sel.INT_NONE: case Sel.INT_ABOVE: case Sel.INT_ON: // nothing to do break; case Sel.INT_STRADDLES: sel.cancel(true); // DtTerm behaviour break; case Sel.INT_BELOW: sel.adjust(firsta, +1, firsta + buf.nlines); break; } } public void op_bc(int count) { // back cursor/column if (debugOps()) System.out.println("op_bc(" + count + ")"); // NOI18N while (count-- > 0) { if (st.cursor.col <= 0) return; st.cursor.col--; } cursor_line().setAboutToWrap(false); } public void op_cm(int row, int col) { // cursor motion row and col come in as 1-origin) if (debugOps()) System.out.println("op_cm(row " + row + ", col " + col + ")"); // NOI18N // "xemacs -nw" seems to overflow and underflow often. // 0 is allowed if (row == 0) row = 1; if (col == 0) col = 1; // deal with overflow if (row > st.rows) row = st.rows; if (col > buf.visibleCols()) col = buf.visibleCols(); cursor_line().setAboutToWrap(false); st.cursor.row = beginx() + row - 1; st.cursor.col = col - 1; // Maybe SHOULD setAboutToWrap(true) if on last column? } public void op_cl() { // clear screen and home cursor if (debugOps()) System.out.println("op_cl"); // NOI18N cursor_line().setAboutToWrap(false); clear(); st.cursor.row = beginx(); st.cursor.col = 0; } public void op_ce() { // clear to end of line if (debugOps()) System.out.println("op_ce"); // NOI18N Line l = cursor_line(); l.clearToEndFrom(Term.this, l.cellToBuf(metrics, st.cursor.col)); switch(sel.intersection(st.cursor.row)) { case Sel.INT_NONE: case Sel.INT_ABOVE: case Sel.INT_BELOW: // nothing to do break; case Sel.INT_ON: case Sel.INT_STRADDLES: sel.cancel(true); // DtTerm behaviour break; } } public void op_cd() { // clear to end of screen if (debugOps()) System.out.println("op_cd -- clear to end of screen"); // NOI18N for (int lx = st.cursor.row; lx < beginx() + st.rows; lx++) { Line l = buf.lineAt(lx); l.reset(); } switch(sel.intersection(st.cursor.row)) { case Sel.INT_NONE: case Sel.INT_ABOVE: // nothing to do break; case Sel.INT_BELOW: case Sel.INT_ON: case Sel.INT_STRADDLES: sel.cancel(true); // DtTerm behaviour break; } } public void op_dc(int count) { // delete character if (debugOps()) System.out.println("op_dc(" + count + ")"); // NOI18N if (count == 0) count = 1; Line l = cursor_line(); while (count-- > 0) l.deleteCharAt(l.cellToBuf(metrics, st.cursor.col)); } public void op_dl(int count) { // delete line // and scroll everything under it up if (debugOps()) System.out.println("op_dl(" + count + ")"); // NOI18N Line l; while (count-- > 0) { boolean old_atw = cursor_line().setAboutToWrap(false); // reverse of op_al() // Rotate a line from top to bottom if (!do_margins) { l = buf.moveLineFromTo(st.cursor.row, (beginx()+st.rows-1)-1); } else { l = buf.moveLineFromTo(st.cursor.row, (beginx()+botMargin())-1); } l.reset(); cursor_line().setAboutToWrap(old_atw); } switch(sel.intersection(st.cursor.row)) { case Sel.INT_NONE: case Sel.INT_ABOVE: // nothing to do break; case Sel.INT_ON: case Sel.INT_STRADDLES: sel.cancel(true); // DtTerm behaviour break; case Sel.INT_BELOW: sel.adjust(firsta, -1, firsta + buf.nlines); break; } } public void op_do(int count) { // down count lines // SHOULD add a mode: {scroll, warp, stay} for cases where // cursor is on the bottom line. if (debugOps()) System.out.println("op_do(" + count + ") -- down"); // NOI18N boolean old_atw = cursor_line().setAboutToWrap(false); while (count-- > 0) { st.cursor.row++; if (st.cursor.row >= buf.nlines) { // equivalent of op_newline: if (possiblyScrollDown()) { buf.addLineAt(st.cursor.row); limit_lines(); if (debugOps()) System.out.println("op_do ADJUSTED"); // NOI18N } } } cursor_line().setAboutToWrap(old_atw); } public void op_ho() { // cursor home (upper left of the screen) if (debugOps()) System.out.println("op_ho -- home"); // NOI18N cursor_line().setAboutToWrap(false); st.cursor.row = beginx(); st.cursor.col = 0; } public void op_ic(int count) { // insert character if (debugOps()) System.out.println("op_ic(" + count + ")"); // NOI18N Line l = cursor_line(); int insertion_col = l.cellToBuf(metrics, st.cursor.col); while (count-- > 0) { l.insertCharAt(Term.this, ' ', insertion_col, st.attr); } // SHOULD worry about line wrapping } public void op_nd(int count) { // cursor right (non-destructive space) if (debugOps()) System.out.println("op_nd(" + count + ")"); // NOI18N int vc = st.cursor.col; while (count-- > 0) { vc++; if (vc >= buf.visibleCols()) { if (debugOps()) System.out.println("\tbailing out at count " + count); // NOI18N vc--; break; } } st.cursor.col = vc; } public void op_up(int count) { // cursor up if (debugOps()) System.out.println("op_up(" + count + ")"); // NOI18N boolean old_atw = cursor_line().setAboutToWrap(false); Line l; while (count-- > 0) { st.cursor.row--; if (st.cursor.row < st.firstx) { st.cursor.row = st.firstx; // scroll down, Rotate a line from bottom to top if (!do_margins) { l = buf.moveLineFromTo(buf.nlines-1, st.cursor.row); } else { l = buf.moveLineFromTo(st.firstx + botMargin(), st.cursor.row); } l.reset(); // SHOULD note and do something about the selection? } } cursor_line().setAboutToWrap(old_atw); } public void op_sc() { // save cursor position if (debugOps()) System.out.println("op_sc()"); // NOI18N st.saveCursor(); // SHOULD defeat repaint? } public void op_rc() { // restore saved cursor position if (debugOps()) System.out.println("op_rc()"); // NOI18N st.restoreCursor(); } public void op_glyph(int glyph, int rendition) { if (debugOps()) { System.out.println("op_glyph(glyph " + glyph + // NOI18N ", rendition " + rendition + ")"); // NOI18N } setGlyph(glyph, rendition); } public void op_reverse(boolean reverse_video) { setReverseVideo(reverse_video); } public void op_cursor_visible(boolean visible) { setCursorVisible(visible); } public void op_margin(int from, int to) { if (debugOps()) { System.out.println("op_margin(" + from + ", " + // NOI18N to + ")"); // NOI18N } if (from < 0) top_margin = 0; else if (from > st.rows) top_margin = st.rows; else top_margin = from; if (to < 0) bot_margin = 0; else if (to > st.rows) bot_margin = st.rows; else bot_margin = to; if (top_margin > bot_margin) { int tmp = top_margin; top_margin = bot_margin; bot_margin = tmp; } } long last_time = System.currentTimeMillis(); public void op_time(boolean repaint) { long time = System.currentTimeMillis(); long elapsed = time - last_time; Date d = new Date(time); String date_str = d.toString(); String elapsed_str = "" + elapsed/1000 + "." + elapsed%1000;// NOI18N String output1 = date_str + " Elapsed (sec): " + elapsed_str;// NOI18N String output2 = "putChar " + n_putchar + // NOI18N " putChars " + n_putchars + // NOI18N " linefeeds " + n_linefeeds + // NOI18N " repaint " + n_repaint + // NOI18N " paint " + n_paint; // NOI18N setAttribute(41); // Red Bg // can't use appendText from within ops. for (int sx = 0; sx < output1.length(); sx++) op_char(output1.charAt(sx)); op_line_feed(); op_carriage_return(); for (int sx = 0; sx < output2.length(); sx++) op_char(output2.charAt(sx)); setAttribute(0); last_time = time; n_putchar = 0; n_putchars = 0; n_linefeeds = 0; n_paint = 0; n_repaint = 0; repaint(true); // TMP setRefreshEnabled(repaint); } public int op_get_width() { return horizontally_scrollable? buf.totalCols(): buf.visibleCols(); } public int op_get_column() { return st.cursor.col; } public void op_soft_reset() { st.overstrike = true; top_margin = 0; // 0 means default (see topMargin()) bot_margin = 0; st.attr = 0; repaint(false); } public void op_full_reset() { op_soft_reset(); op_cl(); // clear screen, home cursor reverse_video = false; repaint(false); } public void op_set_mode(int mode) { switch (mode) { case 4: // insert mode st.overstrike = false; break; case 2: // keyboard lock case 12: // local echo off case 20: // newline // Currently unsupported break; } } public void op_reset_mode(int mode) { switch (mode) { case 4: // replace mode st.overstrike = true; break; case 2: // keyboard unlock case 12: // local echo on case 20: // newline // Currently unsupported break; } } public void op_status_report(int code) { switch (code) { case 5: reply((char)27 + "[0n"); // NOI18N break; case 6: reply((char)27 + "[" + // NOI18N (st.cursor.row - st.firstx) + ";" + // NOI18N st.cursor.col + "R"); // NOI18N break; } } } private void putc_work(char c) { interp.processChar(c); possiblyHScroll(); screen.possiblyUpdateCaretText(); } private void on_char(char c) { sendChar(c); } private void sendChars(char c[], int offset, int count) { dte_end.sendChars(c, offset, count); } private void sendChar(char c) { dte_end.sendChar(c); } /** * Adjust vertical scrollbar range */ private void adjust_scrollbar() { // JScrollBar is weird. // The visible range is 1 (for value) + extent. // So extent has to be set to visible-range - 1: adjust_scrollbar_impl(); /* OLD NPE-x // It's important that we do this from within the AWT event thread. if (SwingUtilities.isEventDispatchThread()) { adjust_scrollbar_impl(); } else { SwingUtilities.invokeLater(new Runnable() { public void run() { adjust_scrollbar_impl(); } }); } */ } private void adjust_scrollbar_impl() { if (vscroll_bar != null) { int value = st.firstx; int extent = st.rows-1; int min = 0; int max; if (buf.nlines <= st.rows) max = st.rows - 1; else max = buf.nlines - 1; vscroll_bar.setValues(value, extent, min, max); } if (hscroll_bar != null && horizontally_scrollable) { int value = st.firsty; int extent = buf.visibleCols()-1; int min = 0; int max; if (buf.totalCols() <= buf.visibleCols()) max = buf.visibleCols() - 1; else max = buf.totalCols() - 1; /* DEBUG System.out.println("HSCROLL " + min + " <= " + value + // NOI18N "[" + extent + "] " + max); // NOI18N */ hscroll_bar.setValues(value, extent, min, max); /* DEBUG System.out.println("HSCROLL " + hscroll_bar.getMinimum() + // NOI18N " <= " + hscroll_bar.getValue() + // NOI18N "[" + hscroll_bar.getModel().getExtent() + "] " + hscroll_bar.getMaximum()); // NOI18N */ } } /** * Figure the pixel size of the screen based on various properties. */ private Dimension calculateSize() { int dx = buf.visibleCols() * metrics.width + glyph_gutter_width + debug_gutter_width; int dy = st.rows * metrics.height; Dimension d = new Dimension(dx, dy); return d; } /** * To be called as the result of programmatic changes in properties * that affect the size of the screen: font, rows & columns, glyph * gutter width etc. * Applies the newly calculated size via sizeChanged(). * * We used to call screen.setSize() which would eventually call * sizeChanged() through the notification mechanism. That worked well * for row column size changes etc, but not so well for font changes. * What would happen is that he cause of size changes would be lost * by the time we got to sizeChanged() and for example the screen * wouldn't resize as a result of font pt-size changes. */ private void updateScreenSize() { /* DEBUG System.out.println("updateScreenSize("+buf.cols+", "+st.rows+")"); // NOI18N */ if (screen != null) { Dimension d = calculateSize(); sizeChanged(d.width, d.height); } } // HACK: // Helper variable to remember the original value of rows across // various different control flows. private int old_rows = -1; /** * Called whenver the screens size is to be changed, either by * us via updateScreenSize(), or thru user action and the Screen * componentResized() notification. * * Adjust the state and buffer, commit to the size by setting * preferredSize and notify any interested parties. */ void sizeChanged(int newWidth, int newHeight) { /* DEBUG System.out.println("sizeChanged(newheight " + newHeight + // NOI18N ", newWidth " + newWidth + ")"); */ // Do columns first ... they're easy int newcols = (newWidth - glyph_gutter_width - debug_gutter_width) / metrics.width; buf.setVisibleCols(newcols); if (old_rows == -1) { // st.rows hasn't changed yet, so remember it before changing it. old_rows = st.rows; } st.rows = newHeight/metrics.height; // akemr - hack to fix #17807 if ( st.rows < 1 ) st.rows = 1; /* DEBUG System.out.println(">>>>>>> rows from "+old_rows+" to "+st.rows); // NOI18N */ int row_delta = st.rows - old_rows; // negative => we shrunk old_rows = -1; adjust_lines(row_delta); limit_lines(); // Commit to the size // // Setting setPreferredSize() is where the commitment is. If we // don't do it our layout manager containers won't honor the resizing // and snap us back. Dimension new_size = isSizeRounded()? calculateSize(): new Dimension(newWidth, newHeight); /* DEBUG System.out.println("but I want "+new_size.height+" "+new_size.width); // NOI18N */ if (false) { // Setting size is a bad idea. the potential for getting into // a looping tug-of-war with our containers' layout manager // is too high and unpredictable. One nasty example we ran // into was JTabbedPane. screen.setSize(new_size); } else { screen.setPreferredSize(new_size); // Do we really need these? invalidate(); if (getParent() != null) getParent().validate(); } // Notify any interested parties. // Normally we'd inline the code in updateTtySize() here but factoring // it has it's uses as explained in updateTtySize(). updateTtySize(); } protected void possibly_repaint(boolean adjust_scrollbar) { if (!refresh_enabled) return; repaint(adjust_scrollbar); } /** * Model and or view settings have changed, redraw everything. */ protected void repaint(boolean adjust_scrollbar) { /* * A long discussion on performance and smooth vs jump vs jerky * scrolling ... (note: a lot of this is based on experiments with * Term as a unix terminal emulator application as opposed to * within the context of NetBeans). * * Term spends it's time between collecting and deciphering input * and repainting the screen. Input processing always goes on, but * screen repainitng can be done more or less often to trade off * smoothness of scrolling vs speed. * * At one end is so-called smooth scrolling. This is where the * screen is redrawn on every linefeed. That's a lot of painting. * To get into that mode use the paintImmediately() below and * uncomment the call to us in op_line_feed(). Also * paintImmediately() doesn't really work unless the Screen is * opaque. I think that is because the paint request comes * to us and we don't forward it to screen; but it could be a * Swing bug too. Term is very slow in this. For example I"ve * time xterm and DtTerm dealing with "cat /etc/termcap" in 2-3 * seconds while Term takes 20-25 seconds. Part of this is * attributed to the fact that Term doesn't take advantage of * bitBlitting when it's adding one line at a time and still * redraws everything. However I'll make a case below that this * isn't that important. * * Then there is so-called jump scrolling. In this regime terminal * emulators redraw the screen "as time permits". This is in effect * what the swing repaint manager helps with. Multiple repaint() * requests translate to one actual paint(). With todays computers * it's very hard to tell visually that you're jump scrolling * things go by so fast (yes, even under Swing), so this is the * preferred setup. * Here term does a bit better. To deal with a cat'ed 100,000 * line file DtTerm takes 8 seconds, while Term takes 22 seconds. * (That's 3 times slower vs 8 times). From some measurements * I've made the number of linefeeds per actual paints has * ranged from > 100 to upper 30's. These numbers are sufficiently * high that the whole screen has to be repained everytime. * I.e. blitting to scroll and drawing only what's new isn't * going to help here. To get reasonable jump-scrolling, you need * to make sure that the Screen is opaque because if you don't * you will get ... * * Jerky scrolling. If Term is not opaque, the number of actual * paints per repaint() requests diminishes drastically. 'cat' of * etc/termcap (once the code has been warmed up) sometimes causes * a single refresh at the end in contrast to ~100 when Screen * is opaque. Naturally Term in this mode can eat up input at * a rate comparable to dtterm etc, but the jerkiness is very * ugly. * Opacity isn't the only criterion. Term, when embeded inside a * tabbed pane (like it is in NetBeans) will also act as if it's * opaque and you get more frequent refreshes, as in the * jump-scrolling regime. But that was way too slow for the * taste of NB users which is why OutputTab window calls us on a * timer. That brings it's own jerkiness of a different sort. * * There is a third factor that contributes to slowness. If you * just 'cat' a file you get the numbers I presneted above. But * if you run an app that actually puts out the 100,000 lines * some sort of timing interaction forces Term into near smooth * scrolling and as a result things slow down a lot! For example, * $ generate_100K_lines > /tmp/bag 00:08 sec * $ cat /tmp/bag 00:20 sec * $ generate_100K_lines 03:42 sec (opaque) * $ generate_100K_lines 01:58 sec (!opaque) * This happens even if the generating program is a lightweight * native application. In fact I believe it is this effect that * forced NB's OutputTab to adopt the timer. I believe there are two * factors that contrinute to this. * a) Running applications are line buffered so putChars(), with * it's attendant repaint(), gets called once per line pushing * us into the smooth scrolling regime. (But why then doesn't * DtTerm suffer from this?) * b) timeslicing gives enough time to the repaint manager such * that it converts evey repaint() to a paint. * I know (b) is a factor since if I "simulate" (a) by issueing * repaints() from op_line_feed() while keeping this function from * using paintImmediately() I don't get that many paints. * The combined case has 44 paints per repaint as does simulated (a). * So ain increased number of paints per repaint doesn't * explain this. * * In the end, currently since jump scrolling is still not very * fast and since NB has the timer anyway, Screen is not opaque. * * A useful quantitative measure is the number of linefeeds vs * the number of repaint requests vs the number of actual paints. * All these are collected and can be dumped via op_time() or * printStats(). */ n_repaint++; if (adjust_scrollbar) adjust_scrollbar(); // The following causes Screen.paint() to get called by the Swing // repaint manager which in turn calls back to term.paint(Graphics). screen.repaint(20); // The following should cause an immediate paint. It doesn't // always though! // I've found that for it to be effective Screen needs to be opaque. /* NOTE: paintImmediately() is probably not the best thing to use. // RepaintManager.currentManager(screen).paintDirtyRegions(); screen.paintImmediately(0, 0, screen.getWidth(), screen.getHeight()); */ } /* * Term-specific properties */ /** * Control whether the default foreground and background colors will * be reversed. *

* Note: This is independent of characters' reverse attribute. */ public void setReverseVideo(boolean reverse_video) { this.reverse_video = reverse_video; repaint(false); } /** * Return the value set by setReverseVideo(). */ public boolean isReverseVideo() { return reverse_video; } private boolean reverse_video = false; /** * Set the color of the hilite (selection) - for non XOR mode */ public void setHighlightColor(Color color) { sel.setColor(color); repaint(false); } /** * Get the color of the hilite (selection) - for non XOR mode */ public Color getHighlightColor() { return sel.getColor(); } /** * Set the color of the hilite (selection) - for XOR mode */ public void setHighlightXORColor(Color color) { sel.setXORColor(color); repaint(false); } /** * Get the color of the hilite (selection) - for XOR mode */ public Color getHighlightXORColor() { return sel.getXORColor(); } /** * Set the feedback color of active regions. */ public void setActiveColor(Color color) { // SHOULD check for null color active_color = color; repaint(false); } /** * Get the feedback color of active regions. */ public Color getActiveColor() { // SHOULD clone? but Color is not clonable and has no simple // Color(COlor) constructor. What does JComponent do? return active_color; } private Color active_color = Color.lightGray; /** * Control whether an anchor is set. *

* Setting an anchor will automatically cause the buffer to grow, in * excess of what was set by setHistorySize(), to ensure that whatever * is displayed and in the current history will still be accessible. *

* Also, if you're working with Extents, Coords and ActiveRegions, or * visiting logical lines, you might want to anchor the text so that * your coordinates don't get invalidated by lines going out of the buffer. *

* Repeated enabling of the anchor will discard all text that * doesn't fit in history and start a new anchor. *

* When anchoring is disabled any text in excess of setHistorySize() * is trimmed and the given history size comes into effect again. */ public void setAnchored(boolean anchored) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { if (anchored) { this.anchored = false; limit_lines(); this.anchored = true; } else { this.anchored = false; limit_lines(); repaint(false); // limit_lines() already adjusted scrollbar } } } /** * Return true if the text is currently anchored. */ public boolean isAnchored() { return anchored; } private boolean anchored = false; /** * Returns the actual drawing area so events can be interposed upon, * like context menus. * @deprecated */ public JComponent getCanvas() { return screen; } /** * Returns the actual drawing area so events can be interposed upon, * like context menus. */ public JComponent getScreen() { return screen; } /** * Return the terminal operations implementation. * WARNING! This is temporary */ public Ops ops() { return ops; } /** * Set the Interpreter type by name. * @see Term#setInterp */ public void setEmulation(String emulation) { Interp new_interp = InterpKit.forName(emulation, ops); if (new_interp == null) return; interp = new_interp; } /** * Returns the termcap string that best describes what this Term * emulates. */ public String getEmulation() { return getInterp().name(); } /** * Set the emulation interpreter. *

* It is not advisable to change the emulation after Term has been * connected to a process, since it's often impossible to advise * the process of the new terminal type. */ public void setInterp(Interp interp) { this.interp = interp; } /** * Return the Interpreter assigned to this. */ public Interp getInterp() { return interp; } private Interp interp = new InterpDumb(ops); // used to InterpANSI /** * Set how many lines of history will be available. *

* If an anchor is in effect the history size will only have an * effect when the anchor is reset. */ public void setHistorySize(int new_size) { history_size = new_size; limit_lines(); repaint(true); } /** * Return the number of lines in history */ public int getHistorySize() { return history_size; } private int history_size = 20; /** * Set the width of the glyph gutter in pixels */ public void setGlyphGutterWidth(int pixels) { glyph_gutter_width = pixels; // protect against client mistakes? if (glyph_gutter_width > 30) glyph_gutter_width = 30; updateScreenSize(); } private int glyph_gutter_width; /** * Associate an Image with a glyph id, or clear it if image is null. * * Numbering the glyphs is confusing. They start with 48. That is, * if you register glyph #0 using hbvi/vim :K command the escape * sequence emitted is 48. 48 is ascii '0'. */ public void setGlyphImage(int glyph_number, Image image) { if (glyph_number > 256) return; // SHOULD throw an exception? glyph_images[glyph_number] = image; } private Image glyph_images[] = new Image[256];; /** * Get the usable area for drawing glyphs * * This value changes when the gutter width or the font changes */ public Dimension getGlyphCellSize() { return new Dimension(glyph_gutter_width, metrics.height); } /** * Register up to 8 new custom colors. * * Unlike glyph id's you can start the numbers from 0. * hbvi/vim's :K command will add a 58 to the number, but that * is the code we interpret as custom color #0. */ public void setCustomColor(int number, Color c) { custom_color[number] = c; } private final Color custom_color[] = new Color[8]; private final Color standard_color[] = new Color[8]; /** * Get cursor row in buffer coordinates (0-origin). */ public int getCursorRow() { return st.cursor.row; } /** * Get cursor column in buffer coordinates (0-origin) */ public int getCursorCol() { return cursor_line().cellToBuf(metrics, st.cursor.col); } /** * Get (absolute) cursor coordinates. *

* The returned Coord is newly allocated and need not be cloned. */ public Coord getCursorCoord() { Line l = buf.lineAt(st.cursor.row); return new Coord(new BCoord(st.cursor.row, l.cellToBuf(metrics, st.cursor.col)), firsta); } /* * * Move the cursor to the given (absolute) coordinates * * @deprecated, replaced by{@link #setCursorCoord(Coord)} */ public void goTo(Coord coord) { setCursorCoord(coord); } /** * Move the cursor to the given (absolute) coordinates * SHOULD be setCursorCoord! */ public void setCursorCoord(Coord coord) { Coord c = (Coord) coord.clone(); c.clip(st.rows, buf.visibleCols(), firsta); st.cursor = c.toBCoord(firsta); st.cursor.col = cursor_line().bufToCell(metrics, st.cursor.col); repaint(true); } /** * Control whether the cursor is visible or not. *

* We don't want a visible cursor when we're using Term in * non-interactve mode. */ public void setCursorVisible(boolean cursor_visible) { this.cursor_visible = cursor_visible; } /** * Find out if cursor is visible. */ public boolean isCursorVisible() { return cursor_visible; } private boolean cursor_visible = true; /** * Back up the coordinate by one character and return new Coord. *

* Travels back over line boundaries *
* Returns null if 'c' is the first character in the buffer. */ public Coord backup(Coord c) { BCoord bRow = buf.backup(c.toBCoord(firsta)); if (bRow == null) return null; return new Coord(bRow, firsta); } /** * Advance the coordinate by one charater and return new coord. *

* Travels forward over line boundaries. *
* Returns null if 'c' is the last character in the buffer. */ public Coord advance(Coord c) { return new Coord(buf.advance(c.toBCoord(firsta)), firsta); } /** * Get contents of current selection. *

* Returns 'null' if there is no current selection. */ public String getSelectedText() { return sel.getSelection(); } /** * Get the extent of the current selection. *

* If there is no selection returns 'null'. */ public Extent getSelectionExtent() { return sel.getExtent(); } /** * Set the extent of the selection. */ public void setSelectionExtent(Extent extent) { extent.begin.clip(buf.nlines, buf.totalCols(), firsta); extent.end.clip(buf.nlines, buf.totalCols(), firsta); sel.setExtent(extent); repaint(false); } /** * Clear the selection. */ public void clearSelection() { sel.cancel( true ); repaint(false); } /** * Set whether slections automatically get copied to the systemSelection * when the selection is completed (the button is released). *

* This is how xterm and other X-windows selections work. *

* This property can probably be deprecated. It was neccessary in the * pre-1.4.2 days when we didn't have a systemSelection and we wanted * to have the option of not cloberring the systemClipboard on text * selection. * * @deprecated selections now always get copied to systemSelection if * it exists. */ public void setAutoCopy(boolean auto_copy) { // no-op } /** * Return the value set by setAutoCopy() * * @deprecated Now always returns 'true'. */ public boolean isAutoCopy() { return true; } /* * Control whether refreshes are enabled. *

* Turn refresh off if you're about to add a lot of text to the * terminal. Another way is to use appendText("stuff", false) */ public void setRefreshEnabled(boolean refresh_enabled) { this.refresh_enabled = refresh_enabled; if (refresh_enabled) repaint(true); } public boolean isRefreshEnabled() { return refresh_enabled; } private boolean refresh_enabled = true; /** * Sets whether the selection highlighting is XOR style or normal * Swing style. */ public void setSelectionXOR(boolean selection_xor) { this.selection_xor = selection_xor; repaint(false); } /* * If returns 'true' then selections are drawn using the xor mode, * Otherwise they are drawn in regular swing fashion. */ public boolean isSelectionXOR() { return selection_xor; } // make accessible to Sel boolean selection_xor = false; /** * Set the TAB size. *

* The cursor is moved to the next column such that *

     * column (0-origin) modulo tab_size == 0
     * 
* The cursor will not go past the last column. *

* Note that the conventional assumption of what a tab is, is not * entirely accurate. ANSI does not define TABs as above but rather * as a directive to move to the next "tabstop" which has to have been * set previously. In fact on unixes it is the terminal line discipline * that expands tabs to spaces in the conventional way. That in, * turn explains why TAB information doesn't make it into selections and * why copying and pasting Makefile instructions is liable to lead * to hard-to-diagnose make rpoblems, which, in turn drove the ANT people * to reinvent the world. */ public void setTabSize(int tab_size) { this.tab_size = tab_size; } /** * Get the TAB size. */ public int getTabSize() { return tab_size; } private int tab_size = 8; /** * Control whether Term scrolls to the bottom on keyboard input. *

* This is analogous to the xterm -sk/+sk option. */ public void setScrollOnInput(boolean scroll_on_input) { this.scroll_on_input = scroll_on_input; } /** * Return whether Term scrolls to the bottom on keyboard input. */ public boolean isScrollOnInput() { return scroll_on_input; } private boolean scroll_on_input = true; /** * Control whether Term scrolls on any output. *

* When set to false, if the user moves the scrollbar to see some * text higher up in history, the view will not change even if more * output is produced. But if the cursor is visible, scrolling will * happen. This is so that in an interactive session any prompt will * be visible etc. *

* However, the tracking of the cursor is controlled by the 'trackCursor' * property which by default is set to 'true'. *

* This property is analogous to the xterm -si/+si option. */ public void setScrollOnOutput(boolean scroll_on_output) { this.scroll_on_output = scroll_on_output; } /** * Return whether Term scrolls on any output. */ public boolean isScrollOnOutput() { return scroll_on_output; } private boolean scroll_on_output = true; /** * Control whether Term will scroll to track the cursor as text is added. *

* If set to true, as output is being generated Term will try to keep * the cursor in view. *

* This property is only relevant when scrollOnOutput is set to false. * If scrollOnOutput is true, this property is also implicitly true. */ public void setTrackCursor(boolean track_cursor) { this.track_cursor = track_cursor; } /** * Return whether Term will scroll to track the cursor as text is added. */ public boolean isTrackCursor() { return track_cursor; } private boolean track_cursor = true; /** * Controls horizontal scrolling and line wrapping. *

* When enabled a horizontal scrollbar becomes visible and line-wrapping * is disabled. */ public void setHorizontallyScrollable(boolean horizontally_scrollable) { this.horizontally_scrollable = horizontally_scrollable; // hscroll_bar.setVisible(horizontally_scrollable); hscroll_wrapper.setVisible(horizontally_scrollable); } /* * Returns whether horizontal scrolling is enabled. * @see Term.setHorizontallyScrollable */ public boolean isHorizontallyScrollable() { return this.horizontally_scrollable; } private boolean horizontally_scrollable = true; /** * Clear everything and assign new text. *

* If the size of the text exceeds history early parts of it will get * lost, unless an anchor was set using setAnchor(). */ public void setText(String text) { // SHOULD make a bit more efficient clearHistoryNoRefresh(); appendText(text, true); } /** * Add new text at the current cursor position. *

* Doesn't repaint the view unless 'repaint' is set to 'true'. *
* Doesn't do anything if 'text' is 'null'. */ public void appendText(String text, boolean repaint) { if (text == null) return; ckEventDispatchThread(); // OLD NPE-x synchronized(this) { for (int cx = 0; cx < text.length(); cx++) { putc_work(text.charAt(cx)); if (text.charAt(cx) == '\n') putc_work('\r'); } } if (repaint) repaint(true); } /** * Scroll the view 'n' pages up. *

* A page is the height of the view. */ public void pageUp(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firstx -= n * st.rows; if (st.firstx < 0) st.firstx = 0; } repaint(true); } /** * Scroll the view 'n' pages down. *

* A page is the height of the view. */ public void pageDown(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firstx += n * st.rows; if (st.firstx + st.rows > buf.nlines) st.firstx = buf.nlines - st.rows; } repaint(true); } /** * Scroll the view 'n' lines up. */ public void lineUp(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firstx -= n; if (st.firstx < 0) st.firstx = 0; } repaint(true); } /** * Scroll the view 'n' lines down. */ public void lineDown(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firstx += n; if (st.firstx + st.rows > buf.nlines) st.firstx = buf.nlines - st.rows; } repaint(true); } /** * Scroll the view 'n' pages to the left. */ public void pageLeft(int n) { columnLeft(n * buf.visibleCols()); } /** * Scroll the view 'n' pages to the right. */ public void pageRight(int n) { columnRight(n * buf.visibleCols()); } /** * Scroll the view 'n' columns to the right. */ public void columnRight(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firsty += n; if (st.firsty + buf.visibleCols() > buf.totalCols()) st.firsty = buf.totalCols() - buf.visibleCols(); } repaint(true); } /** * Scroll the view 'n' columns to the left. */ public void columnLeft(int n) { ckEventDispatchThread(); // OLD NPE-x synchronized(this) { st.firsty -= n; if (st.firsty < 0) st.firsty = 0; } repaint(true); } /** * Return the cell width of the given character. */ public int charWidth(char c) { return metrics.wcwidth(c); } /* * The following are overrides of JComponent/Component */ /** * Override of JComponent. *

* We absolutely require fixed width fonts, so if the font is changed * we create a monospaced version of it with the same style and size. */ public void setFont(Font new_font) { Font font = new Font("Monospaced", // NOI18N new_font.getStyle(), new_font.getSize()); super.setFont(font); // This should invalidate us, which // ultimately will cause a repaint /* DEBUG System.out.println("Font info:"); // NOI18N System.out.println("\tlogical name: " + font.getName()); // NOI18N System.out.println("\tfamily name: " + font.getFamily()); // NOI18N System.out.println("\tface name: " + font.getFontName()); // NOI18N */ // cache the metrics metrics = new MyFontMetrics(this, font); updateScreenSize(); } /** * Override of JComponent. *

* Pass on the request to the screen where all the actual focus * management happens. */ public void requestFocus() { screen.requestFocus(); } public boolean requestFocusInWindow() { return screen.requestFocusInWindow(); } /** * Override of JComponent. *

* Pass on enabledness to sub-components (scrollbars and screen) */ public void setEnabled(boolean enabled) { // This was done as a result of issue 24824 super.setEnabled(enabled); hscroll_bar.setEnabled(enabled); vscroll_bar.setEnabled(enabled); screen.setEnabled(enabled); } //.......................................................................... // Accessibility stuff is all here // Not just the required interfaces but also all the helpers. //.......................................................................... /** * Since Term is a composite widget the main accessible JComponent is * not Term but an internal JComponent. We'll speak of Term accessibility * when we in fact are referring to the that inner component. *

* Accessibility for Term is tricky because it doesn't fit into the * roles delineated by Swing. The closest role is that of TEXT and that * is too bound to how JTextComponent works. To wit ...

2D vs 1D coordinates
Term has a 2D coordinate system while AccessibleText works with 1D locations. So Term actually has code which translates between the two. This code is not exactly efficient but only kicks in when assistive technology latches on.
Line breaks ('\n's) count as characters! However we only count logical line breaks ('\n's appearing in the input stream) as opposed to wrapped lines!

The current implementation doesn't cache any of the mappings because that would require a word per line extra storage for the cumulative char count. The times actually we're pretty fast with a 4000 line histroy.

WORDs and SENTENCEs
For AccessibleText.get*Index() functions WORD uses the regular Term WordDelineator. SENTENCE translates to just a line.
Character attributes
Term uses the ANSI convention of character attributes so when AccessibleText.getCharacterAttribute() is used a rough translation is made as follows:
  • ANSI underscore -> StyleConstants.Underline
  • ANSI bright/bold -> StyleConstants.Bold
  • Non-black foreground color -> StyleConstants.Foreground
  • Explicitly set background color -> StyleConstants.Background
Font related information is always constant so it is not provided.
History
Term has history and lines wink out. If buffer coordinates were used to interact with accessibility, caretPosition and charCount would be dancing around. Fortunately Term has absolute coordinates. So positions returned via AccessibleText might eventually refer to text that has gone by.
Caret and Mark vs Cursor and Selection
While Term keeps the selection and cursor coordinates independent, JTextComponent merges them and AccessibleText inherits this view. With Term caretPosition is the position of the cursor and selection ends will not neccessarily match with the caret position.

Currently only notifications of ACCESSIBLE_CARET_PROPERTY and ACCESSIBLE_TEXT_PROPERTY are fired and that always in pairs. They are fired on the receipt of any character to be processed.

IMPORTANT: It is assumed that under assistive technology Term will be used primarily as a continuous text output device or a readonly document. Therefore ANSI cursor motion and text editing commands or anything that mutates the text will completely invalidate all of AccessibleTexts properties. (Perhaps an exception SHOULD be made for backspace) */ public AccessibleContext getAccessibleContext() { if (accessible_context == null) { accessible_context = new AccessibleTerm(); } return accessible_context; } private AccessibleContext accessible_context; /* * Term is really a container. Screen is where things get drawn and * where focus is set to, so all real accessibility work is done there. * We just declare us to have a generic role. */ protected class AccessibleTerm extends AccessibleJComponent { public AccessibleRole getAccessibleRole() { return AccessibleRole.PANEL; } public void setAccessibleName(String name) { screen.getAccessibleContext().setAccessibleName(name); } } /** * [DO NOT USE] Convert a 2D Coord to a 1D linear position. *

* This function really should be private but I need it to be public for * unit-testing purposes. */ public int CoordToPosition(Coord c) { BCoord b = c.toBCoord(firsta); int nchars = charsInPrehistory; for (int r = 0; r < b.row; r++) { Line l = buf.lineAt(r); nchars += l.length(); if (!l.isWrapped()) nchars += 1; } nchars += c.col; return nchars; } /** * [DO NOT USE] Convert a 1D linear position to a 2D Coord. *

* This function really should be private but I need it to be public for * unit-testing purposes. */ public Coord PositionToCoord(int position) { int nchars = charsInPrehistory; for (int r = 0; r < buf.nlines; r++) { Line l = buf.lineAt(r); nchars += l.length(); if (!l.isWrapped()) nchars += 1; if (nchars > position) { BCoord b = new BCoord(); b.row = r; b.col = buf.lineAt(r).length() + 1 - (nchars - position); return new Coord(b, firsta); } } return null; } /** * Return the number of characters stored. *

* Include logical newlines for now to match the above conversions. * Hmm, do we include chars in prehistory? */ int getCharCount() { int nchars = charsInPrehistory; for (int r = 0; r < buf.nlines; r++) { Line l = buf.lineAt(r); nchars += l.length(); if (!l.isWrapped()) nchars += 1; } return nchars; } /** * Return the bounding rectangle of the character at the given coordinate */ Rectangle getCharacterBounds(Coord c) { if (c == null) return null; BCoord b = c.toBCoord(firsta); char ch = '\0'; try { Line l = buf.lineAt(b.row); ch = l.charArray()[b.col]; } catch(Exception x) { ; } Point p1 = toPixel(b); Rectangle rect = new Rectangle(); rect.x = p1.x; rect.y = p1.y; rect.width = metrics.width * charWidth(ch); rect.height = metrics.height; return rect; } Color backgroundColor(boolean reverse, int attr) { Color bg = null; if (reverse) { int fcx = Attr.foregroundColor(attr); if (fcx != 0 && fcx <= 8) { bg = standard_color[fcx-1]; } else if (fcx > 8) { bg = custom_color[fcx-9]; } else { bg = actual_foreground; } } else { int bcx = Attr.backgroundColor(attr); if (bcx != 0 && bcx <= 8) { bg = standard_color[bcx-1]; } else if (bcx > 8) { bg = custom_color[bcx-9]; } } return bg; } Color foregroundColor(boolean reverse, int attr) { Color fg = null; if (reverse) { int bcx = Attr.backgroundColor(attr); if (bcx != 0 && bcx <= 8) { fg = standard_color[bcx-1]; } else if (bcx > 8) { fg = custom_color[bcx-9]; } else { fg = actual_background; } } else { int fcx = Attr.foregroundColor(attr); if (fcx != 0 && fcx <= 8) { fg = standard_color[fcx-1]; } else if (fcx > 8) { fg = custom_color[fcx-9]; } else { fg = actual_foreground; } } return fg; } private static void ckEventDispatchThread() { /* if (!SwingUtilities.isEventDispatchThread()) { System.out.println("term: NOT IN EventDispatchThread"); Thread.dumpStack(); } */ } /* attaches MouseWheelHandler to scroll the component */ private static void addMouseWheelHandler(JComponent comp, JScrollBar bar) { comp.addMouseWheelListener(new MouseWheelHandler (bar)); // XXX who removes this lsnr? } private static class MouseWheelHandler implements MouseWheelListener { private JScrollBar scrollbar; public MouseWheelHandler (JScrollBar scrollbar) { this.scrollbar = scrollbar; } public void mouseWheelMoved(MouseWheelEvent e) { int totalScrollAmount = e.getUnitsToScroll() * scrollbar.getUnitIncrement(); scrollbar.setValue( scrollbar.getValue() + totalScrollAmount ); } } }

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.