blob: 498ce6aba57b16c206b294c2b212e455feaacb27 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.lib.terminalemulator;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.JComponent.AccessibleJComponent;
import javax.swing.*;
import javax.swing.text.Keymap;
/**
* Term is a pure Java multi-purpose terminal emulator.
* <p>
* It has the following generic features:
* <ul>
* <li>All "dumb" operations. Basically putting characters on a screen and
* processing keyboard input.</li>
* <li>ANSI mode "smart" operations. Cursor control etc.</li>
* <li>Character attributes like color, reverse-video etc.</li>
* <li>Selection service in character, word and line modes matching xterm
* with configurable word boundary detection.</li>
* <li>History buffer.</li>
* <li>Facilities to iterate through logical lines, to implement search for
* example.</li>
* <li>Support for nested pickable regions in order to support hyperlinked
* views or more complex active text utilities.</li>
* <li>Support for double-width Oriental characters.</li>
* </ul>
* <p>
* <h2>Coordinate systems</h2>
* 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:
* <dl>
*
* <dt>ANSI Screen coordinates
* <dd>
* 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().
* <p>
* 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.
* <p>
*
* <dt>Cell coordinates
* <dd>
* 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).
* <p>
* 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.
* <p>
* Note that this is strictly an internal coordinate system. For example
* Term.getCursorCol() and getCursorCoord() return buffer coordinates.
* <p>
* 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.
* <p>
*
* <dt>Buffer coordinates ...
* <dd>
* ... 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().
* <p>
* 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.
* <p>
* 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.
* <p>
* <dt>Absolute coordinates ...
* <dd>
* ... 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* I'm indebted to Alan Kostinsky for this bit of lateral thinking.
* </dl>
*
*
* <p>
* <h2>Modes of use</h2>
* There are three ways Term can be used.
* These modes aren't explicit they are just a convenient way of discussing
* functionality.
* <dl>
* <dt>Screen mode
* <dd>
* 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.
* <p>
* 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.
* <p>
* This mode works correctly even if there is history and you see a
* scrollbar, just as it does under XTerm and it's derivatives.
*
* <p>
* <dt>Buffer/Interactive mode
* <dd>
* This is the primary facility that XTerm and other derivatives provide. The
* screen has a history buffer in the vertical dimension.
* <p>
* 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.
* <br>
* Which is why we have ...
*
* <p>
* <dt>Page mode
* <dd>
* 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.
* <p>
* 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.
* </dl>
*
* <p>
* <h2>What Term is not</h2>
* <ul>
* <li>
* 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.
* <p>
* <li>
* 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.
* <p>
* 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.
* </ul>
*/
public class Term extends JComponent implements Accessible {
public static class ExternalCommandsConstants {
public static final String EXECUTION_ENV_PROPERTY_KEY = "ExecutionEnvironment_KEY"; //NOI18N
public static final String COMMAND_PREFIX = "ext[::] "; //NOI18N
public static final String IDE_OPEN = "ideopen"; //NOI18N
}
private State st = new State();
private Sel sel = new Sel(this, st);
private transient 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]'.
private 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;
private boolean fixedFont = false;
private MyFontMetrics metrics = null;
private Map<?, ?> renderingHints;
private 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();
private static final Color TRANSPARENT = new Color(0, 0, 0, 0);
//
// The palette maps color indexes into actual Color's.
//
// Attr's FGCOLOR and BGCOLOR store either 0 to imply "default" or
// the palette index + 1. So use methods Attr.foregroundColor() and
// backgroudnColor() to convert Attr values to the indexes into the palette.
//
// The domain of this map is divided as follows:
//
// "ESC [ ... m" codes
// index fg bg
//--------------------------------------------------------------------------
// 0-7 ANSI colors 30-37 40-47 PAL_ANSI
// 8-15 DtTerm custom colors 50-57 58-65 PAL_BRIGHT
// 8-15 ANSI "bright" colors 90-97 100-107 PAL_BRIGHT
// 16-231 RGB cube PAL_RGB
// 232-255 Greyscale PAL_GREY
//--------------------------------------------------------------------------
// 256 default foreground PAL_FG
// 257 default background PAL_BG
// 258 default bold PAL_BOLD
// 259 palette size PAL_SIZE
//
private final Color palette[] = new Color[Attr.PAL_SIZE];
/**
* ScrollWrapper is a HACK that allows us to make pairs of scrollbars
* look nice.
* <p>
* 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;
}
@Override
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
//......................................................................
@Override
public AccessibleContext getAccessibleContext() {
if (accessible_context == null) {
accessible_context = new AccessibleScrollWrapper();
}
return accessible_context;
}
private AccessibleContext accessible_context;
protected class AccessibleScrollWrapper extends AccessibleJComponent {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.PANEL;
}
}
}
private class BaseTermStream extends TermStream {
@Override
public void flush() {
repaint(true);
}
@Override
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');
}
@Override
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);
}
@Override
public void sendChar(char c) {
fireChar(c);
}
@Override
public void sendChars(char buf[], int offset, int count) {
fireChars(buf, offset, count);
}
}
// head is closer to Term
// pushes extend tail
private transient TermStream base_stream = new BaseTermStream();
private transient TermStream dce_end = base_stream;
private transient TermStream dte_end = base_stream;
/**
*
* @param 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
*/
private static final Integer DEFAULT_DEBUG = Integer.getInteger("org.netbeans.lib.terminalemulator.Term.debug");
@SuppressWarnings("PointlessBitwiseExpression")
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;
public static final int DEBUG_KEYPASS = 1 << 6;
private int debug_gutter_width = 0;
public void setDebugFlags(int flags) {
debug = flags;
}
private int debug = DEFAULT_DEBUG != null ? DEFAULT_DEBUG : 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 debugKeypass() {
return (debug & DEBUG_KEYPASS) == DEBUG_KEYPASS;
}
private boolean debugMargins() {
return (debug & DEBUG_MARGINS) == DEBUG_MARGINS;
}
/**
* Return true if DEBUG_INPUT flag has been set.
* @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.
* @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 void resetMargins() {
top_margin = 0;
bot_margin = 0;
}
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.
* @param word_delineator Property to set.
*/
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.
* @return Property to get.
*/
public WordDelineator getWordDelineator() {
return this.word_delineator;
}
private WordDelineator default_word_delineator = WordDelineator.createNewlineDelineator();
private WordDelineator word_delineator = default_word_delineator;
/**
* Set/unset input listener.
* Entered text gets sent via this listener
*
* @param l
* @deprecated Replaced by {@link #addInputListener(TermInputListener)}.
*/
@Deprecated
public void setInputListener(TermInputListener l) {
addInputListener(l);
}
/**
* Add an input listener to this.
* <p>
* Text entered via the keyboard gets sent via this listener.
* @param l
*/
public void addInputListener(TermInputListener l) {
input_listeners.add(l);
}
public void removeInputListener(TermInputListener l) {
input_listeners.remove(l);
}
private void fireChar(char c) {
ListIterator<TermInputListener> iter = input_listeners.listIterator();
while (iter.hasNext()) {
TermInputListener l = iter.next();
l.sendChar(c);
}
}
private void fireChars(char buf[], int offset, int count) {
ListIterator<TermInputListener> iter = input_listeners.listIterator();
while (iter.hasNext()) {
TermInputListener l = iter.next();
l.sendChars(buf, offset, count);
}
}
private LinkedList<TermInputListener> input_listeners = new LinkedList<>();
/**
* Set/unset misc listener.
* The following events gets sent via this listener:
* window size changes
*
* @param l
* @deprecated Replaced by{@link #addListener(TermListener)}.
*/
@Deprecated
public void setListener(TermListener l) {
addListener(l);
}
/**
* Add a TermListener to this.
* @param l
*/
public void addListener(TermListener l) {
listeners.add(l);
updateTtySize();
}
/**
* Remove the given TermListener from this.
* @param l
*/
public void removeListener(TermListener l) {
listeners.remove(l);
}
private void fireSizeChanged(Dimension cells, Dimension pixels) {
for (TermListener l : listeners) {
l.sizeChanged(cells, pixels);
}
}
private void fireTitleChanged(String title) {
for (TermListener l : listeners) {
l.titleChanged(title);
}
}
private void fireCwdChanged(String cwd) {
for (TermListener l : listeners) {
l.cwdChanged(cwd);
}
}
private void fireExternalCommand(String command) {
for (TermListener l : listeners) {
l.externalToolCalled(command);
}
}
private final java.util.List<TermListener> listeners = new CopyOnWriteArrayList<>();
/**
* Set/unset focus policy.
* <br>
* When set, the Term screen will grab focus when clicked on, otherwise
* it will grab focus when the mouse moves into it.
* @param click_to_type Set the property.
*/
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.
* @param read_only Set the property.
*/
public void setReadOnly(boolean read_only) {
this.read_only = read_only;
}
/**
* Return whether keystrokes are ignored.
* @return The property value.
*/
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.
* <p>
* 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;
setAttribute(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.
* <p>
* Use {@link #clearHistoryNoRefresh()} if you find that clearHistory
* causes flickering.
*/
public void clearHistory() {
clearHistoryNoRefresh();
repaint(true);
}
/**
* Return the RegionManager associated with this Term
* @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 aBuf = new StringBuffer();
visitLines(begin, end, false, new LineVisitor() {
@Override
public boolean visit(Line l, int row, int bcol, int ecol) {
// buf.append(l.charArray(), bcol, ecol-bcol+1);
l.accumulateInto(bcol, ecol, aBuf);
return true;
}
});
return aBuf.toString();
}
public String getRowText(int row) {
Line line = buf.lineAt(row);
if (line == null) {
return null;
}
return line.stringBuffer().toString();
}
private Keymap keymap;
private Set<String> allowedActions;
/**
* Set keymap and allowed actions
* @param keymap - use this to check if a keystroke is used outside the terminal
* @param allowedActions - if not null we only allow specified actions in the terminal
*/
public void setKeymap(Keymap keymap, Set<String> allowedActions) {
this.keymap = keymap;
this.allowedActions = allowedActions;
}
/**
* Get KeyStroke set.
* <p>
* Be default Term consumes all keystrokes.
* Any KeyStroke added to this set will be passed through and not consumed.
* <p>
* Be careful with control characters you need to create the keystroke
* as follows (note the - 64):
* <pre>
* KeyStroke.getKeyStroke(new Character((char)('T'-64)), Event.CTRL_MASK)
* </pre>
* @return Property value.
*/
public HashSet<KeyStroke> getKeyStrokeSet() {
return keystroke_set;
}
/*
* Set the KeyStroke set.
* <p>
* 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> keystroke_set) {
this.keystroke_set = keystroke_set;
if (debugKeypass()) {
System.out.println("---- setKeyStrokeSet --------------------");//NOI18N
for (KeyStroke ks : keystroke_set) {
System.out.println("--- " + ks);//NOI18N
}
}
}
private HashSet<KeyStroke> 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 something into
* keystroke_set.
*/
private boolean maybeConsume(KeyEvent e) {
if (read_only || e.isConsumed()) {
return false;
}
KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e);
if (debugKeypass()) {
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'.
* <p>
* 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.
* <p>
* If begin is null, then the start of the buffer is assumed.
* If end is null, then the end of the buffer is assumed.
* @param begin
* @param end
* @param llv visit() is called on 'llv' for each line.
*/
@SuppressWarnings("AssignmentToMethodParameter")
public void visitLogicalLines(Coord begin, Coord end,
final LogicalLineVisitor llv) {
// Create a trampoline visitor
LineVisitor tramp = new LineVisitor() {
private String text = ""; // NOI18N
private int lineno = 0;
private Coord begin = null;
private Coord end = null;
@Override
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.
* <p>
* If begin is null, then the start of the buffer is assumed.
* If end is null, then the end of the buffer is assumed.
* @param begin
* @param end
* @param llv visit() is called on 'llv' for each line.
*/
@SuppressWarnings("AssignmentToMethodParameter")
public void reverseVisitLogicalLines(Coord begin, Coord end,
final LogicalLineVisitor llv) {
// Create a trampoline visitor
LineVisitor tramp = new LineVisitor() {
private String text = ""; // NOI18N
private int lineno = 0;
private Coord begin = null;
private Coord end = null;
@Override
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.
* <p>
* 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().
* <p>
* Note that 'offset' is relative to begin.col!
* <p>
* 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.
* <pre>
* row | columns |
* ----+----------+
* 0 |0123456789|
* 1 | |
* 2 |aaaaaxxxxx| wrap
* 3 |xxxxXXXxxx| wrap
* 4 |xxxxx |
* 5 | |
*
* </pre>
* 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();
* <p>
* 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.
* @param begin
* @param offset
* @param length
* @return
*/
@SuppressWarnings({"AssignmentToMethodParameter", "ValueOfIncrementOrDecrementUsed"})
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.
* <p>
* 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-screen.
* @param target Coord to be checked.
*/
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.
* <p>
* 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-screen
* @param region Region against which to normalize.
*/
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.
* @param target Coord to check visibility of.
* @return true if 'target' is visible within the screen.
*/
public boolean isCoordVisible(Coord target) {
BCoord btarget = target.toBCoord(firsta);
return btarget.row >= st.firstx && btarget.row < st.firstx + st.rows;
}
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 <n> 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.
* <p>
* Unrecognized values silently ignored.
* @param value attribute value
*/
public void setAttribute(int value) {
st.attr = Attr.setAttribute(st.attr, value);
}
/**
* Return the complete state of attributes.
* @return Complete state of attributes.
*/
int attrSave() {
return st.attr;
}
/**
* Restore the complete set of attributes.
* @param attr Attributes to be restored.
*/
void attrRestore(int attr) {
st.attr = attr;
}
/**
* Set or unset the display attribute for characters from 'begin' to 'end'
* inclusive to 'value' depending on the value of 'on'.
* <p>
* Will cause a repaint.
* @param begin
* @param end
* @param value
* @param on
*/
public void setCharacterAttribute(Coord begin, Coord end,
final int value, final boolean on) {
visitLines(begin, end, false, new LineVisitor() {
@Override
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.
* <p>
* Will not repaint.
*/
public void setGlyph(int glyph_id, int background_id) {
Line l = cursor_line();
l.setGlyphId(glyph_id);
l.setBackgroundColor(background_id);
}
/*
* Set the glyph and line background colors for the give line.
* <p>
* 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.setGlyphId(glyph_id);
l.setBackgroundColor(background_id);
possibly_repaint(false);
}
/**
* Adjust lines when the widget is resized (and also at construction time)
* <p>
* 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.
* <p>
* 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.
*/
@SuppressWarnings("ValueOfIncrementOrDecrementUsed")
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 current history size of buffer.
* @return current history size of buffer.
*/
public int getHistoryBuffSize() {
return buf.nlines() - st.rows;
}
@SuppressWarnings("ValueOfIncrementOrDecrementUsed")
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.
* <p>
* 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 static final int UP = 1 << 1;
public static final int DOWN = 1 << 2;
public static final int LEFT = 1 << 3;
public static final 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;
int c;
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;
}
}
@Override
@SuppressWarnings("SleepWhileInLoop")
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;
}
private static Boolean onMac = null;
private boolean onMac() {
if (onMac == null) {
String osName = System.getProperty("os.name").toLowerCase();
if (osName.startsWith("mac os x")) { //NOI18N
onMac = Boolean.TRUE;
} else {
onMac = Boolean.FALSE;
}
}
return onMac;
}
private void initializePalette() {
palette[Attr.PAL_ANSI+0] = new Color(0x000000);
palette[Attr.PAL_ANSI+1] = new Color(0xCD0000);
palette[Attr.PAL_ANSI+2] = new Color(0x00CD00);
palette[Attr.PAL_ANSI+3] = new Color(0xCDCD00);
palette[Attr.PAL_ANSI+4] = new Color(0x1E90FF);
palette[Attr.PAL_ANSI+5] = new Color(0xCD00CD);
palette[Attr.PAL_ANSI+6] = new Color(0x00CDCD);
palette[Attr.PAL_ANSI+7] = new Color(0xE5E5E5);
palette[Attr.PAL_BRIGHT+0] = new Color(0x0D0D0D);
palette[Attr.PAL_BRIGHT+1] = new Color(0xFF0000);
palette[Attr.PAL_BRIGHT+2] = new Color(0x00FF00);
palette[Attr.PAL_BRIGHT+3] = new Color(0xFFFF00);
palette[Attr.PAL_BRIGHT+4] = new Color(0x1FF0FF);
palette[Attr.PAL_BRIGHT+5] = new Color(0xFF00FF);
palette[Attr.PAL_BRIGHT+6] = new Color(0x00FFFF);
palette[Attr.PAL_BRIGHT+7] = new Color(0xFFFFFF);
// Fill RGB cube
for (int r = 0; r <= 5; r++) {
for (int g = 0; g <= 5; g++) {
for (int b = 0; b <= 5; b++) {
int number = 36 * r + 6 * g + b;
palette[Attr.PAL_RGB+number] = new Color(r*51, b*51, b*51);
}
}
}
// Fill greyscale portion.
for (int g = 0; g < 24; g++) {
int g2 = g+1;
palette[Attr.PAL_GREY+g] = new Color(g2*10, g2*10, g2*10);
}
palette[Attr.PAL_FG] = UIManager.getColor("TextArea.foreground"); // NOI18N
palette[Attr.PAL_BG] = UIManager.getColor("TextArea.background"); // NOI18N
palette[Attr.PAL_BOLD] = palette[Attr.PAL_FG];
// Ensure nothing is left unfilled.
for (int px = 0; px < palette.length; px++) {
if (palette[px] == null) {
System.out.printf("palette[%d] is null\n", px); //NOI18N
palette[px] = Color.GRAY;
}
}
}
/**
* Constructor
*/
public Term() {
st.rows = 25;
st.firstx = 0;
st.firsty = 0;
initializePalette();
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() {
@Override
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() {
@Override
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() {
@Override
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.
private boolean saw_return;
@SuppressWarnings("AssignmentToMethodParameter")
private void charTyped(char c, KeyEvent e) {
if (read_only) {
return;
}
if (c == 10 && saw_return) {
saw_return = false;
c = (char) 13;
}
// Consume ctrl+tab, ctrl+shift+tab event, see #237990
if (keymap != null && (c == KeyEvent.VK_TAB)
&& ((e.getModifiers() == KeyEvent.CTRL_MASK)
|| (e.getModifiers() == (KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK)))) {
e.consume();
}
if (passOn && maybeConsume(e)) {
if ((e.getModifiers() & KeyEvent.ALT_MASK) == KeyEvent.ALT_MASK) {
if (getAltSendsEscape()) {
on_char(ESC);
on_char(c);
} else {
if (c >=0 || c <= 127)
c += 128;
on_char(c);
}
} else {
on_char(c);
}
possiblyScrollOnInput();
}
hscrollReset(c);
}
@Override
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (debugKeys()) {
System.out.printf("term: keyTyped: %s\n", e); // NOI18N
System.out.printf("term: keyTyped: char '%c' %04x\n",// NOI18N
c, (int) c);
}
charTyped(c, e);
}
@Override
public void keyPressed(KeyEvent e) {
if (debugKeys()) {
System.out.printf("keyPressed %2d %s\n", e.getKeyCode(), KeyEvent.getKeyText(e.getKeyCode())); // NOI18N
}
KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e);
// At first check the keymap (higher priority)
if (keymap != null) {
Action action = keymap.getAction(ks);
if (action != null) {
if (allowedActions == null || allowedActions.contains(action.getClass().getName())) {
return;
}
}
}
// Then let Interp's have go at special keys
interp.keyPressed(e);
if (e.isConsumed()) {
passOn = false;
return;
}
switch (e.getKeyCode()) {
case KeyEvent.VK_ESCAPE:
if (onMac()) {
// On a Mac AWT doesn't provide us with a keyTyped()
// for VK_ESCAPE so we simulate it.
charTyped(ESC, e);
}
break;
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;
case KeyEvent.VK_LEFT:
break;
case KeyEvent.VK_RIGHT:
break;
}
passOn = maybeConsume(e);
}
@Override
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() {
@Override
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);
}
@Override
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() {
@Override
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();
// See IZ 193527
if (click_to_type) {
requestFocus();
}
}
}
@Override
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);
}
switch (e.getClickCount()) {
case 1:
left_down_point = (Point) e.getPoint().clone();
break;
case 2:
{
BCoord bcoord = toBufCoords(toViewCoord(e.getPoint()));
BExtent word = buf.find_word(word_delineator, bcoord);
sel.select_word(word.toExtent(firsta));
repaint(false);
break;
}
case 3:
{
BCoord bcoord = toBufCoords(toViewCoord(e.getPoint()));
BExtent line = buf.find_line(bcoord);
sel.select_line(line.toExtent(firsta));
repaint(false);
break;
}
default:
break;
}
fireSelectionExtentChanged();
}
}
@Override
public void mouseReleased(MouseEvent e) {
/* DEBUG
System.out.println("mouseReleased"); // NOI18N
*/
if (SwingUtilities.isLeftMouseButton(e)) {
if (click_to_type) {
requestFocus();
}
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
*/
@Override
public void mouseEntered(MouseEvent e) {
/* DEBUG
System.out.println("mouseEntered"); // NOI18N
*/
if (!click_to_type) {
requestFocus();
}
}
@Override
public void mouseExited(MouseEvent e) {
/* DEBUG
System.out.println("mouseExited"); // NOI18N
*/
}
});
screen.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
/* DEBUG
System.out.println("Focus gained >>>>>>>>>>>>>>>>>>>>>>>"); // NOI18N
*/
has_focus = true;
repaint(false);
}
@Override
public void focusLost(FocusEvent e) {
/* DEBUG
Component o = e.getOppositeComponent();
System.out.println("Focus lost <<<<<<<<<<<<<<<<<<<<<<<" + o);
*/
has_focus = false;
repaint(false);
}
});
}
static void indent(boolean indented) {
if (indented) {
System.out.print("\t"); // NOI18N
}
}
private MemUse lastMemUse = new MemUse();
/*
* Print interesting statistics and facts about this Term
*/
public void printStats(String message) {
boolean indented = (message != null);
if (message != null) {
System.out.println(message);
}
buf.printStats(indented);
indent(indented);
System.out.println(" View:" + // NOI18N
" rows " + st.rows + // NOI18N
" v cols " + buf.visibleCols() + // NOI18N
" t cols " + buf.totalCols() + // NOI18N
" history " + history_size); // NOI18N
indent(indented);
System.out.println(" " + // NOI18N
" firstx " + st.firstx + // NOI18N
" firsty " + st.firsty + // NOI18N
" firsta " + firsta); // NOI18N
indent(indented);
System.out.println(" " + // NOI18N
" gutter " + glyph_gutter_width); // NOI18N
indent(indented);
System.out.println("Cursor:" + // NOI18N
" " + st.cursor + // NOI18N
" topMargin " + topMargin() + // NOI18N
" botMargin " + botMargin()); // NOI18N
printCounts(indented);
MemUse memUse = new MemUse();
MemUse delta = memUse.changeFrom(lastMemUse);
indent(indented);
memUse.print("Memory:"); // NOI18N
indent(indented);
delta.print(" Delta:"); // NOI18N
}
public void printCounts(boolean indented) {
indent(indented);
System.out.println("Counts:" + // NOI18N
" putChar() " + n_putchar + // NOI18N
" putChars() " + n_putchars); // NOI18N
indent(indented);
System.out.println(" " + // NOI18N
" linefeeds " + n_linefeeds); // NOI18N
indent(indented);
System.out.println(" " + // NOI18N
" repaint() " + n_repaint + // NOI18N
" paint() " + n_paint); // NOI18N
}
public void resetStats() {
n_putchar = 0;
n_putchars = 0;
n_linefeeds = 0;
n_repaint = 0;
n_paint = 0;
lastMemUse = new MemUse();
}
private static class MemUse {
private final long free;
private final long max;
private final long total;
public MemUse() {
free = Runtime.getRuntime().freeMemory();
max = Runtime.getRuntime().maxMemory();
total = Runtime.getRuntime().totalMemory();
}
private MemUse(long free, long max, long total) {
this.free = free;
this.max = max;
this.total = total;
}
@SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
public MemUse changeFrom(MemUse old) {
return new MemUse(this.free - old.free,
this.max - old.max,
this.total - old.total);
}
private long unused() {
return max - (total + free);
}
private void print(String msg) {
System.out.println(msg +
" max " + max / 1024 + "K" + " = " + // NOI18N
" total " + total / 1024 + "K" + " + " + // NOI18N
" free " + free / 1024 + "K" + " + " + // NOI18N
" unused " + unused() / 1024 + "K" // 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);
// bug #237034
if (string == null) {
return;
}
/* DEBUG
System.out.println("System selection contains '" + string + "'"); // NOI18N
*/
char ca[] = string.toCharArray();
sendChars(ca, 0, ca.length);
} catch (UnsupportedFlavorException | IOException e) {
//
}
}
/**
* Transfer contents of selection to Terms input stream.
* <p>
* The pasted content is sent through sendChars.
* <br>
* The paste will silently fail if:
* <ul>
* <li> The selection has null contents.
* <li> The selections data flavor is not string.
* </ul>
*/
public void pasteFromSelection() {
/* DEBUG
System.out.println("Term: pasteFromSelection()"); // NOI18N
*/
if (systemSelection == null) {
return;
}
pasteHelp(systemSelection);
}
/**
* Transfer contents of clipboard to Terms input stream.
* <p>
* The pasted content is sent through sendChars.
* <br>
* The paste will silently fail if:
* <ul>
* <li> The selection has null contents.
* <li> The selections data flavor is not string.
* </ul>
*/
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); // NOI18N
old_extent = new_extent;
}
/**
* Set the number of character rows in the screen.
* <p>
* See setRowsColumns() for some additional important information.
* @param rows The property value.
*/
public void setRows(int rows) {
if (old_rows == -1) {
old_rows = st.rows;
}
st.rows = rows;
resetMargins();
updateScreenSize();
}
/**
* Get the number of character rows in the screen
* @return The property value.
*/
public int getRows() {
return st.rows;
}
/**
* Set the number of character columns in the screen.
* <p>
* See setRowsColumns() for some additional important information.
* @param cols The property value.
*/
public void setColumns(int cols) {
buf.setVisibleCols(cols);
updateScreenSize();
}
/**
* Get the number of character columns in the screen
* @return The property value.
*/
public int getColumns() {
return buf.visibleCols();
}
/**
* 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);
}
/**
* Simultaneously set the number of character rows and columns.
* <p>
* 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.
* <p>
* 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 behavior.
* <p>
* 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.
* <p>
* Alternatively if this Term is embedded in a Window (like JFrame)
* the window will need to be re-pack()ed as it does not accommodate it's
* children's 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.
* @param rows
* @param columns
*/
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;
resetMargins();
buf.setVisibleCols(columns);
updateScreenSize();
}
/**
* Governs whether Term will round down resize requests to character
* cell size.
* <p>
* 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.
* <p>
* The layout manager might not necessarily 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)}.
* @param size_rounded The property value.
*/
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.
* <p>
* See {@link #setSizeRounded(boolean)} for more info.
* @return The property value.
*/
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.
* @param target
* @return
*/
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
* 0-origin).
* <p>
* In the returned Point, x represents the column, y the row.
* @param p
* @return
*/
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).
* <p>
* In the returned Point, x represents the column, y the row.
* @param p
* @return
*/
public Point mapToBufRowCol(Point p) {
BCoord c = toBufCoords(toViewCoord(p));
return new Point(c.col, c.row);
}
private Color rendition_to_color(int rendition) {
final int px = Attr.rendition_to_pindex(rendition);
if (px == -1)
return null;
else
return palette[px];
}
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, /* OLD 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.REVERSE.isSet(attr);
boolean active = Attr.ACTIVE.isSet(attr);
// choose background color
Color bg;
if (active) {
bg = active_color;
} else {
if (Attr.BGCOLOR.isSet(attr) || Attr.REVERSE.isSet(attr)) {
bg = backgroundColor(reverse, attr);
} else if (l.getBackgroundColor() != 0) {
bg = rendition_to_color(l.getBackgroundColor());
} else {
// Allow existing BG, i.e. selection, to show through.
bg = null;
}
}
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.UNDERSCORE.isSet(attr)) {
int h = metrics.height - metrics.leading - 1;
g.drawLine(x, yoff + h, x + xlength, yoff + h);
}
// Draw the foreground character glyphs
myDrawChars(g, /* OLD buf, */ l, rbegin, rlength, x, baseline);
// Draw fake bold characters by redrawing one pixel to the right
if (Attr.BRIGHT.isSet(attr)) {
myDrawChars(g, /* OLD 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 char[] xferBuf = new char[80];
private void myDrawChars(Graphics g, /* OLD char buf[], */ Line l,
int start, int howmany, int xoff, int baseline) {
if (xferBuf.length < l.length()) {
xferBuf = new char[l.length()];
}
// OLD final char buf[] = l.XcharArray();
l.getChars(xferBuf);
// Use rendering hints (antialiasing etc.)
Map<?,?> hints = renderingHints;
if ((hints != null) && (g instanceof Graphics2D)) {
((Graphics2D) g).setRenderingHints(hints);
}
// This looks expensive but it is in fact a whole lot faster than
// issuing a g.drawChars() _per_ character. drawChars can not be used
// directly, as it was observed, that characters are not placed
// correctly (observed on HiDPI displays)
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(xferBuf, start, tmp, 0, howmany);
GlyphVector gv = getFont().createGlyphVector(frc, tmp);
massage_glyphs(gv, start, howmany, l);
g2.drawGlyphVector(gv, xoff, baseline);
}
/*
* Render one line
* Draw the line on this brow (buffer row 0-origin)
*/
@SuppressWarnings("AssignmentToMethodParameter")
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
// OLD 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, /* OLD 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;
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.ALT;
if (sbegin == -1 || send < rbegin || sbegin > rend) {
// run is not in selection
do_run(g, yoff, xoff,
baseline, brow, /* OLD 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, /* OLD 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, /* OLD buf, */ l, attr, rbegin, sbegin - 1);
do_run(g, yoff, xoff,
baseline, brow, /* OLD buf, */ l, alt_attr, sbegin, send);
do_run(g, yoff, xoff,
baseline, brow, /* OLD 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, /* OLD buf, */ l, alt_attr, rbegin, send);
do_run(g, yoff, xoff,
baseline, brow, /* OLD 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, /* OLD buf, */ l, attr, rbegin, sbegin - 1);
do_run(g, yoff, xoff,
baseline, brow, /* OLD buf, */ l, alt_attr, sbegin, rend);
} else {
/* DEBUG
System.out.println("Odd run/selection overlap"); // NOI18N
*/
}
if (rend >= 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++;
actual_foreground = palette[Attr.PAL_FG];
actual_background = palette[Attr.PAL_BG];
// 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.getBackgroundColor());
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 aBuf = "" + (firsta + st.firstx + vrow); // NOI18N
g.drawString(aBuf, xoff, baseline);
}
xoff += debug_gutter_width;
// draw any glyphs that we might have
if (glyph_gutter_width > 0) {
Image image = glyph_images[l.getGlyphId()];
if (image != null) {
// xy passed to drawImage() is the top-left of the image
int gyoff = yoff;
g.drawImage(image, xoff, gyoff, TRANSPARENT, 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
*/
}
/**
* Debug helper to draw a coordinate grid over the terminal to visualise the
* cells of the terminal and where the characters are expected to appear
*
* @param g
*/
private void paint_margins(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.setStroke(new BasicStroke(1.0f));
int width = getWidth();
int height = getHeight();
int currX = 0;
while(currX <= width) {
g2d.drawLine(currX, 0, currX, height);
currX += metrics.width;
}
int currY = 0;
while(currY <= height) {
g2d.drawLine(0, currY, width, currY);
currY += metrics.height;
}
g2d.dispose();
}
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 static 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.
* @param c The character to send.
*/
public void putChar(char c) {
dce_end.putChar(c);
}
/**
* Send several characters to the terminal screen.
* <br>
* While 'buf' will have a size it may not be fully filled, hence
* the explicit 'nchar'.
* @param buf
* @param offset
* @param count
*/
public void putChars(char buf[], int offset, int count) {
dce_end.putChars(buf, offset, count);
}
/**
* Force a repaint.
* <p>
* 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:
* <pre>
* term.setRefreshEnabled(false);
* for (cx = 0; cx < buf.length; cx++) {
* term.putChar(c);
* if (c % 10 == 0)
* term.flush();
* }
* term.setRefreshEnabled(true);
* </pre>
*/
public void flush() {
dce_end.flush();
}
/**
* Send a message back to DCE.
* <p>
* 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 {
@Override
@SuppressWarnings("CallToThreadYield")
public void op_pause() {
// This yields slighlty more reasonable results.
Thread.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) {
;
}
*/
}
@Override
public void op_char(char inChar) {
char c = mapChar(inChar);
if (debugOps()) {
// OLD System.out.println("op_char('" + c + "') = " + (int) c); // NOI18N
System.out.printf("op_char('%c' %#x) maps to '%c' %#x\n",// NOI18N
inChar, (int) inChar, c, (int) c);
}
// 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, Attr.BGCOLOR.get(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;
}
}
/**
* map G1 into GL (~ switch to graphic font)
*/
@Override
public void op_as() {
op_selectGL(1);
}
/**
* map G0 into GL (~ switch to normal font)
*/
@Override
public void op_ae() {
op_selectGL(0);
}
@Override
public void op_attr(int attr) {
if (debugOps()) {
System.out.println("op_attr(" + attr + ")"); // NOI18N
}
setAttribute(attr);
}
@Override
public void op_bel() {
// ring the bell
if (debugOps()) {
System.out.println("op_bel()"); // NOI18N
} // SHOULD implement
}
@Override
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);
}
}
}
}
}
@Override
public void op_line_feed() {
// \LF 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
}
op_ind(1);
n_linefeeds++;
}
@Override
public void op_tab() {
// TAB/HT
if (debugOps()) {
System.out.println("op_tab"); // NOI18N
}
cht();
}
private boolean cht() {
// 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 (st.cursor.col == buf.visibleCols() - 1 && !horizontally_scrollable) {
return false;
}
Line l = cursor_line();
int insert_col = l.cellToBuf(metrics, st.cursor.col);
st.cursor.col++;
insert_col++;
while ((st.cursor.col < buf.visibleCols() - 1 || horizontally_scrollable) &&
(st.cursor.col % tab_size) != 0) {
st.cursor.col++;
insert_col++;
}
return true;
}
private boolean cbt() {
if (st.cursor.col <= 0)
return false;
Line l = cursor_line();
int insert_col = l.cellToBuf(metrics, st.cursor.col);
st.cursor.col--;
insert_col--;
while ((st.cursor.col > 0) &&
st.cursor.col % tab_size != 0) {
st.cursor.col--;
insert_col--;
}
return true;
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
public void op_cbt(int n) {
if (debugOps()) {
System.out.printf("op_cbt(%d)\n", n); // NOI18N
}
while (n-- > 0) {
if (!cbt())
break;
}
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "ValueOfIncrementOrDecrementUsed"})
public void op_cht(int n) {
if (debugOps()) {
System.out.printf("op_cht(%d)\n", n); // NOI18N
}
while (n-- > 0) {
if (!cht())
break;
}
}
@Override
public void op_carriage_return() {
if (debugOps()) {
System.out.println("op_carriage_return"); // NOI18N
}
st.cursor.col = 0;
cursor_line().setAboutToWrap(false);
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
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() /*OLD-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;
}
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
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);
}
@Override
@SuppressWarnings("AssignmentToMethodParameter")
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?
}
@Override
public void op_ce() {
// clear to end of line
if (debugOps()) {
System.out.println("op_ce ="); // NOI18N
}
op_el(0);
}
@Override
public void op_el(int code) {
// Erase in Line
if (debugOps()) {
System.out.printf("op_el(%d)\n", code); // NOI18N
}
Line l = cursor_line();
switch (code) {
case 0: // from cursor to end
l.clearToEndFrom(Term.this,
l.cellToBuf(metrics, st.cursor.col),
buf.visibleCols()-1,
Attr.BGCOLOR.get(st.attr));
break;
case 1: // from beginning to cursor (inclusive)
l.clearTo(Term.this,
l.cellToBuf(metrics, st.cursor.col),
Attr.BGCOLOR.get(st.attr));
break;
case 2: // whole line
l.clearToEndFrom(Term.this,
0,
buf.visibleCols()-1,
Attr.BGCOLOR.get(st.attr));
break;
}
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;
}
}
@Override
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;
}
}
@Override
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;
}
@Override
public void op_ed(int code) {
// Erase in Line
if (debugOps()) {
System.out.printf("op_ed(%d)\n", code); // NOI18N
}
Line l;
switch (code) {
case 0: // from cursor to end
l = cursor_line();
// l.setAboutToWrap(false);
l.clearToEndFrom(Term.this,
l.cellToBuf(metrics, st.cursor.col),
buf.visibleCols()-1,
Attr.BGCOLOR.get(st.attr));
for (int lx = st.cursor.row+1; lx < beginx() + st.rows; lx++) {
l = buf.lineAt(lx);
// l.setAboutToWrap(false);
l.reset(Term.this, buf.visibleCols()-1, Attr.BGCOLOR.get(st.attr));
}
break;
case 1: // from beginning to cursor (inclusive)
for (int lx = beginx(); lx < st.cursor.row; lx++) {
l = buf.lineAt(lx);
// l.setAboutToWrap(false);
l.reset(Term.this, buf.visibleCols()-1, Attr.BGCOLOR.get(st.attr));
}
l = cursor_line();
// l.setAboutToWrap(false);
l.clearTo(Term.this,
l.cellToBuf(metrics, st.cursor.col),
Attr.BGCOLOR.get(st.attr));
break;
case 2: // whole screen
for (int lx = beginx(); lx < beginx() + st.rows; lx++) {
l = buf.lineAt(lx);
// l.setAboutToWrap(false);
l.reset(Term.this, buf.visibleCols()-1, Attr.BGCOLOR.get(st.attr));
}
break;
}
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;
}
}
@Override
@SuppressWarnings({"AssignmentToMethodParameter", "ValueOfIncrementOrDecrementUsed"})
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));
}
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
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)/*OLD-1*/);
} else {
l = buf.moveLineFromTo(st.cursor.row,
(beginx() + botMargin())/*OLD-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;
}
}
@Override
@SuppressWarnings({"AssignmentToMethodParameter", "ValueOfIncrementOrDecrementUsed"})
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);
}
@Override
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;
}
@Override
@SuppressWarnings({"AssignmentToMethodParameter", "ValueOfIncrementOrDecrementUsed"})
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
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
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;
}
@Override
public void op_up(int count) {
// cursor up - scroll
if (debugOps()) {
System.out.println("op_up(" + count + ")"); // NOI18N
}
op_ri(count);
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
public void op_ri(int count) {
// cursor up - scroll
// Opposite of op_ind()
if (debugOps()) {
System.out.printf("op_ri(%d)\n", count);//NOI18N
}
boolean old_atw = cursor_line().setAboutToWrap(false);
Line l;
while (count-- > 0) {
if (st.cursor.row == st.firstx + topMargin()) {
// scroll down, Rotate a line from bottom to top
l = buf.moveLineFromTo(st.firstx + botMargin(), st.cursor.row);
l.reset();
// SHOULD note and do something about the selection?
} else {
st.cursor.row--;
if (st.cursor.row < st.firstx)
st.cursor.row = st.firstx;
}
}
cursor_line().setAboutToWrap(old_atw);
}
@Override
public void op_cuu(int count) {
// cursor up - no scroll
// Opposite of op_cud()
if (debugOps()) {
System.out.printf("op_cu(%d)\n", count);//NOI18N
}
boolean old_atw = cursor_line().setAboutToWrap(false);
if (top_margin == 0) {
st.cursor.row -= count;
if (st.cursor.row < st.firstx)
st.cursor.row = st.firstx;
} else {
// Only check against margin if we were below it to begin with.
// This is true xterm behaviour. gnome term for example
// will always honor the margin
boolean was_above_margin = (st.cursor.row < st.firstx + topMargin());
st.cursor.row -= count;
if (!was_above_margin) {
if (st.cursor.row < st.firstx + topMargin())
st.cursor.row = st.firstx + topMargin();
} else {
if (st.cursor.row < st.firstx)
st.cursor.row = st.firstx;
}
}
cursor_line().setAboutToWrap(old_atw);
}
@Override
public void op_cud(int count) {
// cursor down - no scroll
// Opposite of op_cuu()
if (debugOps()) {
System.out.printf("op_cud(%d)\n", count); // NOI18N
}
boolean old_atw = cursor_line().setAboutToWrap(false);
if (bot_margin == 0) {
st.cursor.row += count;
if (st.cursor.row > st.firstx + st.rows - 1)
st.cursor.row = st.firstx + st.rows - 1;
} else {
// Only check against margin if we were above it to begin with
// This is true xterm behaviour. gnome term for example
// will always honor the margin
boolean was_below_margin = (st.cursor.row > st.firstx + botMargin());
st.cursor.row += count;
if (!was_below_margin) {
if (st.cursor.row > st.firstx + botMargin())
st.cursor.row = st.firstx + botMargin();
} else {
if (st.cursor.row > st.firstx + st.rows - 1)
st.cursor.row = st.firstx + st.rows - 1;
}
}
cursor_line().setAboutToWrap(old_atw);
}
@Override
@SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "AssignmentToMethodParameter"})
public void op_ind(int count) {
// cursor down - scroll
// Opposite of op_ri()
// \ESCD
if (debugOps()) {
System.out.printf("op_ind(%d)\n", count); // NOI18N
}
boolean old_atw = cursor_line().setAboutToWrap(false);
boolean noMargins = topMargin() == 0 && botMargin() == st.rows-1;
if (noMargins) {
while (count-- > 0) {
st.cursor.row++;
if (st.cursor.row >= buf.nlines()) {
if (scroll_on_output || cursor_was_visible() && track_cursor) {
st.firstx++;
}
buf.addLineAt(st.cursor.row);
limit_lines();
}
}
} else {
while (count-- > 0) {
if (st.cursor.row == st.firstx + botMargin()) {
// scroll up, Rotate a line from top to bottom
Line l;
l = buf.moveLineFromTo(st.firstx + topMargin(), st.cursor.row);
l.reset();
} else {
st.cursor.row++;
if (st.cursor.row > st.firstx + st.rows - 1)
st.cursor.row = st.firstx + st.rows - 1;
}
}
}
cursor_line().setAboutToWrap(old_atw);
}
@Override
public void op_sc() {
// save cursor position
if (debugOps()) {
System.out.println("op_sc()"); // NOI18N
}
st.saveCursor();
// SHOULD defeat repaint?
}
@Override
public void op_rc() {
// restore saved cursor position
if (debugOps()) {
System.out.println("op_rc()"); // NOI18N
}
st.restoreCursor();
}
@Override
public void op_glyph(int glyph, int rendition) {
if (debugOps()) {
System.out.println("op_glyph(glyph " + glyph + // NOI18N
", rendition " + rendition + ")"); // NOI18N
}
setGlyph(glyph, rendition);
}
@Override
public void op_reverse(boolean reverse_video) {
setReverseVideo(reverse_video);
}
@Override
public void op_cursor_visible(boolean visible) {
setCursorVisible(visible);
}
@Override
public void op_icon_name(String iconName) {
if (debugOps()) {
System.out.println("op_icon_name(" + iconName + ")"); // NOI18N
}
}
@Override
public void op_win_title(String winTitle) {
fireTitleChanged(winTitle);
if (debugOps()) {
System.out.println("op_win_title(" + winTitle + ")"); // NOI18N
}
}
@Override
public void op_cwd(String currentWorkingDirectory) {
fireCwdChanged(currentWorkingDirectory);
if (debugOps()) {
System.out.println("op_cwd(" + currentWorkingDirectory + ")"); // NOI18N
}
}
@Override
public void op_ext(String command) {
fireExternalCommand(command);
if (debugOps()) {
System.out.println("op_ext(" + command + ")"); // NOI18N
}
}
@Override
public void op_setG(int gx, int fx) {
if (debugOps()) {
System.out.printf("op_setG(%d, %d)\n", gx, fx); // NOI18N
}
Term.this.st.setG(gx, fx);
}
@Override
public void op_selectGL(int gx) {
if (debugOps()) {
System.out.printf("op_selectGL(%d)\n", gx); // NOI18N
}
Term.this.st.selectGL(gx);
}
@Override
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();
@Override
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);
}
@Override
public void op_hyperlink(String url, String text) {
hyperlink(url, text);
}
@Override
public int op_get_width() {
return horizontally_scrollable ? buf.totalCols() : buf.visibleCols();
}
@Override
public int op_get_column() {
return st.cursor.col;
}
@Override
public void op_soft_reset() {
st.overstrike = true;
st.selectGL(0);
st.setG(0, 0);
st.setG(1, 0);
st.setG(2, 0);
st.setG(3, 0);
resetMargins();
setAttribute(0);
interp.softReset();
repaint(false);
}
@Override
public void op_full_reset() {
op_soft_reset();
op_cl(); // clear screen, home cursor
clearHistoryNoRefresh();
setReverseVideo(false);
repaint(false);
}
@Override
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;
}
}
@Override
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;
}
}
@Override
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;
}
}
@Override
public void logUnrecognizedSequence(String sequence) {
if (debugOps()) {
System.out.printf("Unrecognized sequence '%s'\n", sequence); // NOI18N
}
Term.this.logUnrecognizedSequence(sequence);
}
@Override
public void logCompletedSequence(String sequence) {
Term.this.logCompletedSequence(sequence);
}
@Override
public void op_send_chars(String sequence) {
Term.this.sendChars(sequence.toCharArray(), 0, sequence.length());
}
@Override
@SuppressWarnings("AssignmentToMethodParameter")
public void op_cha(int col) {
// cursor to column
if (debugOps()) {
System.out.printf("op_cha(col %d)\n", col); // NOI18N
}
// 0 is allowed
if (col == 0) {
col = 1;
}
// deal with overflow
if (col > buf.visibleCols()) {
col = buf.visibleCols();
}
cursor_line().setAboutToWrap(false);
st.cursor.col = col - 1;
// Maybe SHOULD setAboutToWrap(true) if on last column?
}
@Override
@SuppressWarnings("AssignmentToMethodParameter")
public void op_vpa(int row) {
// cursor to row
if (debugOps()) {
System.out.printf("op_vpa(row %d)\n", row); // NOI18N
}
// 0 is allowed
if (row == 0) {
row = 1;
}
// deal with overflow
if (row > st.rows) {
row = st.rows;
}
cursor_line().setAboutToWrap(false);
st.cursor.row = beginx() + row - 1;
// Maybe SHOULD setAboutToWrap(true) if on last column?
}
@Override
@SuppressWarnings("AssignmentToMethodParameter")
public void op_ech(int n) {
// erase characters
if (debugOps()) {
System.out.printf("op_ech(%d)\n", n); // NOI18N
}
if (n == 0)
n = 1;
Line l = cursor_line();
int from = l.cellToBuf(metrics, st.cursor.col);
int to = l.cellToBuf(metrics, st.cursor.col+n-1);
if (debugOps())
System.out.printf("op_ech() from %d to %d\n", from, to); // NOI18N
l.clearFromTo(Term.this, from, to, Attr.BGCOLOR.get(st.attr));
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;
}
}
}
/**
* Create a hyperlink.
* @param url
* @param text
*/
protected void hyperlink(String url, String text) {
// default implementation just dumps out the text
for (char c : text.toCharArray()) {
ops.op_char(c);
}
}
/**
* Map a character according to the font attribute
* @param inChar
* @return the unicode character that renders the correct glyph.
*/
private char mapChar(char inChar) {
switch (st.font()) {
case 0:
default:
return inChar;
case 1: {
// Convert the canonical ncurses ACS code to the appropriate unicode character.
// See:
// http://vt100.net/docs/vt220-rm/table2-4.html
// http://en.wikipedia.org/wiki/Box-drawing_character
// http://en.wikipedia.org/wiki/Arrow_%28symbol%29
final char outChar = interp.mapACS(inChar);
if (outChar == '\0')
return inChar;
switch (outChar) {
default:
return inChar;
// xterm and gnome term don't really honor these:
case '+': // ACS_RARROW
return '\u2192'; // RIGHTWARDS ARROW
case ',': // ACS_LARROW
return '\u2190'; // LEFTWARDS ARROW
case '-': // ACS_UARROW
return '\u2191'; // UPWARDS ARROW
case '.': // ACS_DARROW
return '\u2193'; // DOWNWARDS ARROW
case '0': // ACS_BLOCK
return '\u2588'; // FULL BLOCK
case '`': // ACS_DIAMOND
return '\u2666'; // BLACK DIAMOND SUIT
case 'a': // ACS_CKBOARD
return '\u2592'; // MEDIUM SHADE
// return '\u2593'; // DARK SHADE
case 'b': // ?
return '\u240b'; // SYMBOL FOR VERTICAL TABULATION
case 'c': // ?
return '\u240c'; // SYMBOL FOR FORM FEED
case 'd': // ?
return '\u240d'; // SYMBOL FOR CARRIAGE RETURN
case 'e': // ?
return '\u240a'; // SYMBOL FOR LINE FEED
case 'f': // ACS_DEGREE
return '\u00b0'; // DEGREE SIGN
case 'g': // ACS_PLMINUS
return '\u00b1'; // PLUS-MINUS SIGN
case 'h': // ACS_PLMINUS
return '\u2424'; // SYMBOL FOR NEW LINE
case 'i': // ACS_LANTERN
return '\u240b'; // SYMBOL FOR VERTICAL TABULATION
case 'j': // ACS_LRCORNER
return '\u2518'; // BOX DRAWINGS LIGHT UP AND LEFT
case 'k': // ACS_LRCORNER
return '\u2510'; // BOX DRAWINGS LIGHT DOWN AND LEFT
case 'l': // ACS_LRCORNER
return '\u250c'; // BOX DRAWINGS LIGHT DOWN AND RIGHT
case 'm': // ACS_LLCORNER
return '\u2514'; // BOX DRAWINGS LIGHT UP AND RIGHT
case 'n': // ACS_PLUS
return '\u253c'; // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
case 'v': // ACS_RTEE
return '\u2534'; // BOX DRAWINGS LIGHT UP AND HORIZONTAL
case 'w': // ACS_TTEE
return '\u252c'; // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
case 'o': // ACS_S1
case 'p': // ACS_S3
case 'q': // ACS_HLINE
case 'r': // ACS_S7
case 's': // ACS_S9
return '\u2500'; // BOX DRAWINGS LIGHT HORIZONTAL
case 't': // ACS_LTEE
return '\u251c'; // BOX DRAWINGS LIGHT VERTICAL AND RIGHT
case 'u': // ACS_RTEE
return '\u2524'; // BOX DRAWINGS LIGHT VERTICAL AND LEFT
case 'x': // ACS_VLINE
return '\u2502'; // BOX DRAWINGS LIGHT VERTICAL
case 'y': // ACS_LEQUAL
return '\u2264'; // LESS-THAN OR EQUAL TO
case 'z': // ACS_GEQUAL
return '\u2265'; // GREATER-THAN OR EQUAL TO
case '{': // ACS_PI
return '\u03c0'; // GREEK SMALL LETTER PI
case '|': // ACS_NEQUAL
return '\u2260'; // NOT EQUAL TO
case '}': // ACS_STERLING
return '\u00a3'; // POUND SIGN
case '~': // ACS_BULLET
return '\u00b7'; // MIDDLE DOT
}
}
}
}
private void putc_work(char c) {
interp.processChar(c);
possiblyHScroll();
screen.possiblyUpdateCaretText();
}
private void on_char(char c) {
sendChar(c);
}
private static final char ESC = (char) 27;
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;
resetMargins();
// 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
*/
// 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);
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.
* @param adjust_scrollbar
*/
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.
* <p>
* Note: This is independent of characters' reverse attribute.
* @param reverse_video The property value.
*/
public void setReverseVideo(boolean reverse_video) {
if (this.reverse_video == reverse_video)
return; // nothing to do
this.reverse_video = reverse_video;
// Swap PAL_FG and PAL_BG
Color tmp = palette[Attr.PAL_FG];
palette[Attr.PAL_FG] = palette[Attr.PAL_BG];
palette[Attr.PAL_BG] = tmp;
repaint(false);
}
/**
* Return the value set by setReverseVideo().
* @return The property value.
*/
public boolean isReverseVideo() {
return reverse_video;
}
private boolean reverse_video = false;
/**
* Set the color of the hilite (selection) - for non XOR mode
* @param color The property value.
*/
public void setHighlightColor(Color color) {
sel.setColor(color);
repaint(false);
}
/**
* Get the color of the hilite (selection) - for non XOR mode
* @return The property value.
*/
public Color getHighlightColor() {
return sel.getColor();
}
/**
* Set the color of the hilite (selection) - for XOR mode
* @param color The property value.
*/
public void setHighlightXORColor(Color color) {
sel.setXORColor(color);
repaint(false);
}
/**
* Get the color of the hilite (selection) - for XOR mode
* @return The property value.
*/
public Color getHighlightXORColor() {
return sel.getXORColor();
}
/**
* Set the feedback color of active regions.
* @param color The property value.
*/
public void setActiveColor(Color color) {
// SHOULD check for null color
active_color = color;
repaint(false);
}
/**
* Get the feedback color of active regions.
* @return The property value.
*/
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;
@Override
public void setBackground(Color c) {
super.setBackground(c);
// See setReverseVideo()
if (reverse_video) {
palette[Attr.PAL_FG] = c;
} else {
palette[Attr.PAL_BG] = c;
}
}
@Override
public void setForeground(Color c) {
super.setForeground(c);
// See setReverseVideo()
if (reverse_video) {
palette[Attr.PAL_BG] = c;
palette[Attr.PAL_BOLD] = c;
} else {
palette[Attr.PAL_FG] = c;
palette[Attr.PAL_BOLD] = c;
}
}
/**
* Control whether an anchor is set.
* <p>
* 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.
* <p>
* 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.
* <p>
* Repeated enabling of the anchor will discard all text that
* doesn't fit in history and start a new anchor.
* <p>
* When anchoring is disabled any text in excess of setHistorySize()
* is trimmed and the given history size comes into effect again.
* @param anchored The property value.
*/
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.
* @return The property value.
*/
public boolean isAnchored() {
return anchored;
}
private boolean anchored = false;
/**
* Returns the actual drawing area so events can be interposed upon,
* like context menus.
* @return
* @deprecated Replaced by{@link #getScreen()}.
*/
@Deprecated
public JComponent getCanvas() {
return screen;
}
/**
* Returns the actual drawing area so events can be interposed upon,
* like context menus.
* @return
*/
public JComponent getScreen() {
return screen;
}
/**
* Return the terminal operations implementation.
* <b>WARNING! This is temporary</b>
* @return
*/
public Ops ops() {
return ops;
}
/**
* Set the Interpreter type by name.
* @param emulation The property value.
* @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.
* @return The property value.
*/
public String getEmulation() {
return getInterp().name();
}
/**
* Set the emulation interpreter.
* <p>
* 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.
* @param interp The property value.
*/
public void setInterp(Interp interp) {
this.interp = interp;
}
/**
* Return the Interpreter assigned to this.
* @return The property value.
*/
public Interp getInterp() {
return interp;
}
private transient Interp interp = new InterpDumb(ops); // used to InterpANSI
/**
* Set how many lines of history will be available.
* <p>
* If an anchor is in effect the history size will only have an
* effect when the anchor is reset.
* @param new_size The property value.
*/
public void setHistorySize(int new_size) {
history_size = new_size;
limit_lines();
repaint(true);
}
/**
* Return the number of lines in history
* @return The property value.
*/
public int getHistorySize() {
return history_size;
}
private int history_size = 20;
/**
* Set the width of the glyph gutter in pixels
* @param pixels The property value.
*/
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'.
* @param glyph_number
* @param image
*/
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.
* <p>
* This value changes when the gutter width or the font changes
* @return The usable area for drawing glyphs.
*/
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.
* @param number
* @param c
* @deprecated
*/
@Deprecated
public void setCustomColor(int number, Color c) {
if (c == null)
throw new IllegalArgumentException();
if (number < 0 || number >= 8)
throw new IllegalArgumentException();
palette[Attr.PAL_BRIGHT+number] = c;
}
/**
* Get cursor row in buffer coordinates (0-origin).
* @return Cursor row in buffer coordinates (0-origin).
*/
public int getCursorRow() {
return st.cursor.row;
}
/**
* Get cursor column in buffer coordinates (0-origin).
* @return Cursor column in buffer coordinates (0-origin).
*/
public int getCursorCol() {
return cursor_line().cellToBuf(metrics, st.cursor.col);
}
/**
* Get (absolute) cursor coordinates.
* <p>
* The returned Coord is newly allocated and need not be cloned.
* @return The property value.
*/
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)}
*/
@Deprecated
public void goTo(Coord coord) {
setCursorCoord(coord);
}
/**
* Move the cursor to the given (absolute) coordinates
* SHOULD be setCursorCoord!
* @param coord The property value.
*/
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.
* <p>
* We don't want a visible cursor when we're using Term in
* non-interactive mode.
* @param cursor_visible The property value.
*/
public void setCursorVisible(boolean cursor_visible) {
this.cursor_visible = cursor_visible;
}
/**
* Find out if cursor is visible.
* @return The property value.
*/
public boolean isCursorVisible() {
return cursor_visible;
}
private boolean cursor_visible = true;
/**
* Back up the coordinate by one character and return new Coord.
* <p>
* Travels back over line boundaries
* <br>
* Returns null if 'c' is the first character in the buffer.
* @param c Coord to back up from.
* @return New Coord derived from 'c'.
*/
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.
* <p>
* Travels forward over line boundaries.
* <br>
* Returns null if 'c' is the last character in the buffer.
* @param c Coord to advance from.
* @return New Coord derived from 'c'.
*/
public Coord advance(Coord c) {
return new Coord(buf.advance(c.toBCoord(firsta)), firsta);
}
/**
* Get contents of current selection.
* <p>
* Returns 'null' if there is no current selection.
* @return Contents of current selection.
*/
public String getSelectedText() {
return sel.getSelection();
}
/**
* Get the extent of the current selection.
* <p>
* If there is no selection returns 'null'.
* @return The property value.
*/
public Extent getSelectionExtent() {
return sel.getExtent();
}
/**
* Set the extent of the selection.
* @param extent The property value.
*/
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).
* <p>
* This is how xterm and other X-windows selections work.
* <p>
* 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.
*
* @param auto_copy The property value.
* @deprecated selections now always get copied to systemSelection if
* it exists.
*/
@Deprecated
public void setAutoCopy(boolean auto_copy) {
// no-op
}
/**
* Return the value set by setAutoCopy()
*
* @return The property value.
* @deprecated Now always returns 'true'.
*/
@Deprecated
public boolean isAutoCopy() {
return true;
}
/*
* Control whether refreshes are enabled.
* <p>
* 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.
* @param selection_xor The property value.
*/
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;
}
private boolean selection_xor = false;
/**
* Set the TAB size.
* <p>
* The cursor is moved to the next column such that
* <pre>
* column (0-origin) modulo tab_size == 0
* </pre>
* The cursor will not go past the last column.
* <p>
* 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 problems, which, in turn drove the ANT people
* to reinvent the world.
* @param tab_size The property value.
*/
public void setTabSize(int tab_size) {
this.tab_size = tab_size;
}
/**
* Get the TAB size.
* @return The property value.
*/
public int getTabSize() {
return tab_size;
}
private int tab_size = 8;
/**
* Set select-by-word delimiters.
* <p>
* When double-clicking on terminal screen selection will expand on left and
* right until reaches one of a delimiters or the whitespace symbol (which
* is delimiter by default).
* @param delimiters The property value.
*/
public void setSelectByWordDelimiters(String delimiters) {
this.delimiters = delimiters;
word_delineator = WordDelineator.createCustomDelineator(delimiters);
}
public String getSelectByWordDelimiters() {
return delimiters;
}
private String delimiters;
/**
* Control whether Term scrolls to the bottom on keyboard input.
* <p>
* This is analogous to the xterm -sk/+sk option.
* @param scroll_on_input The property value
*/
public void setScrollOnInput(boolean scroll_on_input) {
this.scroll_on_input = scroll_on_input;
}
/**
* Return whether Term scrolls to the bottom on keyboard input.
* @return The property value.
*/
public boolean isScrollOnInput() {
return scroll_on_input;
}
private boolean scroll_on_input = true;
/**
* Control whether Term scrolls on any output.
* <p>
* 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.
* <p>
* However, the tracking of the cursor is controlled by the 'trackCursor'
* property which by default is set to 'true'.
* <p>
* This property is analogous to the xterm -si/+si option.
* @param scroll_on_output The property value.
*/
public void setScrollOnOutput(boolean scroll_on_output) {
this.scroll_on_output = scroll_on_output;
}
/**
* Return whether Term scrolls on any output.
* @return The property value.
*/
public boolean isScrollOnOutput() {
return scroll_on_output;
}
private boolean scroll_on_output = true;
/**
* Control whether the Alt key prefixes a key with an ESC or shifts
* the character.
* <p>
* This is based on XTerms altSendsEscape resource. There's a family
* of interrelated resources as follows. You can read about them in
* <b>Xterm Control Sequences</b> under <b>Alt and Meta Keys</b> for
* a summary or the XTerm man page for more details.
* <table border="1">
* <tr>
* <th>Resource</th>
* <th>Term assumed value</th>
* </tr>
* <tr>
* <td>metaSendsEscape</td>
* <td>N/A</td>
* </tr>
* <tr>
* <td>altIsNotMeta</td>
* <td>false (Alt <u>is</u> Meta)</td>
* </tr>
* <tr>
* <td>modifyOtherKeys</td>
* <td>0 (disable)</td>
* </tr>
* <tr>
* <td>eightBitInput</td>
* <td>true</td>
* </tr>
* </table>
* <p>
* Default value is true.
* @param altSendsEscape Sets the property.
*/
public void setAltSendsEscape(boolean altSendsEscape) {
this.altSendsEscape = altSendsEscape;
}
/**
* Return whether the Alt key prefixes a key with an ESC or shifts
* the character.
* @return The property value.
*/
public boolean getAltSendsEscape() {
return altSendsEscape;
}
private boolean altSendsEscape = true;
/**
* Control whether Term will scroll to track the cursor as text is added.
* <p>
* If set to true, as output is being generated Term will try to keep
* the cursor in view.
* <p>
* This property is only relevant when scrollOnOutput is set to false.
* If scrollOnOutput is true, this property is also implicitly true.
* @param track_cursor The property value.
*/
public void setTrackCursor(boolean track_cursor) {
this.track_cursor = track_cursor;
}
/**
* Return whether Term will scroll to track the cursor as text is added.
* @return The property value
*/
public boolean isTrackCursor() {
return track_cursor;
}
private boolean track_cursor = true;
/**
* Controls horizontal scrolling and line wrapping.
* <p>
* When enabled a horizontal scrollbar becomes visible and line-wrapping
* is disabled.
* @param horizontally_scrollable The property value.
*/
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;
public final void setRenderingHints(Map<?, ?> hints) {
renderingHints = hints;
}
/**
* Clear everything and assign new text.
* <p>
* If the size of the text exceeds history early parts of it will get
* lost, unless an anchor was set using setAnchor().
* @param text The text to put into the terminal buffer.
*/
public void setText(String text) {
// SHOULD make a bit more efficient
clearHistoryNoRefresh();
appendText(text, true);
}
/**
* Add new text at the current cursor position.
* <p>
* Doesn't repaint the view unless 'repaint' is set to 'true'.
* <br>
* Doesn't do anything if 'text' is 'null'.
* @param text
* @param repaint
*/
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.
* <p>
* A page is the height of the view.
* @param n Number of pages to scroll up.
*/
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.
* <p>
* A page is the height of the view.
* @param n Number of pages to scroll down.
*/
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.
* @param n The number of lines to scroll 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.
* @param n The number of lines to scroll 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.
* @param n The number of pages to scroll left.
*/
public void pageLeft(int n) {
columnLeft(n * buf.visibleCols());
}
/**
* Scroll the view 'n' pages to the right.
* @param n The number of pages to scroll right.
*/
public void pageRight(int n) {
columnRight(n * buf.visibleCols());
}
/**
* Scroll the view 'n' columns to the right.
* @param n The number of columns to scroll 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.
* @param n The number of columns to scroll 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.
* @param c
* @return
*/
public int charWidth(char c) {
return metrics.wcwidth(c);
}
/**
* If set to true then it is expected that setFont() will receive a fixed
* width font.
* @param fixedFont Controls whether setFont() expects a fixed-width font.
*/
public void setFixedFont(boolean fixedFont) {
this.fixedFont = fixedFont;
}
/**
* Returns whether setFont() expects a fixed-width font.
* @return whether setFont() expects a fixed-width font.
*/
public boolean isFixedFont() {
return fixedFont;
}
/*
* The following are overrides of JComponent/Component
*/
/**
* Override of JComponent.
* <p>
* 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.
* @param new_font
*/
@Override
public final void setFont(Font new_font) {
Font font;
if (isFixedFont()) {
font = new_font;
} else {
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.
* <p>
* Pass on the request to the screen where all the actual focus
* management happens.
*/
@Override
public void requestFocus() {
screen.requestFocus();
}
@Override
public boolean requestFocusInWindow() {
return screen.requestFocusInWindow();
}
/**
* Override of JComponent.
* <p>
* Pass on enabledness to sub-components (scrollbars and screen)
*/
@Override
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.
* <p>
* 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 ...
* <p>
* <dl>
* <dt>2D vs 1D coordinates
* <dd>
* 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.
* <br>
* 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!
* <p>
* 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.
*
* <dt>WORDs and SENTENCEs
* <dd>
* For AccessibleText.get*Index() functions WORD uses the regular
* Term WordDelineator. SENTENCE translates to just a line.
*
* <dt>Character attributes
* <dd>
* Term uses the ANSI convention of character attributes so when
* AccessibleText.getCharacterAttribute() is used a rough translation
* is made as follows:
* <ul>
* <li> ANSI underscore -> StyleConstants.Underline
* <li> ANSI bright/bold -> StyleConstants.Bold
* <li> Non-black foreground color -> StyleConstants.Foreground
* <li> Explicitly set background color -> StyleConstants.Background
* </ul>
* Font related information is always constant so it is not provided.
*
* <dt>History
* <dd>
* 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.
*
* <dt>Caret and Mark vs Cursor and Selection
* <dd>
* 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.
* </dl>
* <p>
* 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.
* <p>
* 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)
*/
@Override
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 {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.PANEL;
}
@Override
public void setAccessibleName(String name) {
screen.getAccessibleContext().setAccessibleName(name);
}
}
/**
* [DO NOT USE] Convert a 2D Coord to a 1D linear position.
* <p>
* This function really should be private but I need it to be public for
* unit-testing purposes.
* @param c
* @return
*/
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.
* <p>
* This function really should be private but I need it to be public for
* unit-testing purposes.
* @param position
* @return
*/
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.
* <p>
* 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);
// OLD ch = l.charArray()[b.col];
ch = l.charAt(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;
}
private Color csetBG(int attr) {
final int px = Attr.backgroundColor(attr);
final Color c = palette[px];
return c;
}
private Color csetFG(int attr) {
final int px = Attr.foregroundColor(attr);
final Color c = palette[px];
return c;
}
Color backgroundColor(boolean reverse, int attr) {
final Color c;
if (reverse) {
c = csetFG(attr);
} else {
c = csetBG(attr);
}
return c;
}
Color foregroundColor(boolean reverse, int attr) {
final Color c;
if (reverse) {
c = csetBG(attr);
} else {
c = csetFG(attr);
}
return c;
}
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 final JScrollBar scrollbar;
public MouseWheelHandler(JScrollBar scrollbar) {
this.scrollbar = scrollbar;
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int totalScrollAmount = e.getUnitsToScroll() * scrollbar.getUnitIncrement();
scrollbar.setValue(scrollbar.getValue() + totalScrollAmount);
}
}
/*
* Logging.
*/
private boolean sequenceLogging;
private Set<String> completedSequences;
private Set<String> unrecognizedSequences;
public final void setSequenceLogging(boolean sequenceLogging) {
this.sequenceLogging = sequenceLogging;
}
public final boolean isSequenceLogging() {
return sequenceLogging;
}
public final Set<String> getCompletedSequences() {
return completedSequences;
}
public final Set<String> getUnrecognizedSequences() {
return unrecognizedSequences;
}
private void logCompletedSequence(String sequence) {
if (!sequenceLogging)
return;
if (completedSequences == null)
completedSequences = new HashSet<>();
completedSequences.add(sequence);
}
private void logUnrecognizedSequence(String sequence) {
if (!sequenceLogging)
return;
if (unrecognizedSequences == null)
unrecognizedSequences = new HashSet<>();
unrecognizedSequences.add(sequence);
}
/**
* @return the metrics
*/
MyFontMetrics metrics() {
return metrics;
}
/**
* @return the buf
*/
Buffer buf() {
return buf;
}
/**
* @return the firsta
*/
int firsta() {
return firsta;
}
}