A text edit box.
multiline : boolean -- if true, lines will be broken at newline characters
editable : boolean -- if false, the text cannot be changed, although it can be copied
var boxen = [];
var cr_regexp = /[\n\r]/g;
var nonwhitespace = /\S/;
// Structural Stuff //////////////////////////////////////////////////////////////////////
// Cursor1 is the cursor -- when there is no selection, its width is
// 1, and its height is the height of one line. When the user selects
// a one-line region, Cursor1 is expanded to cover that region (but
// placed behind the text on the z-axis). When the user selects more
// than one line, Cursor1 covers the background of the first line of
// the selection, Cursor3 covers the last line, and the intermediate
// lines get their color set to "blue".
var curs = $curs; // the main cursor
var curs2 = $curs2; // the "backup" cursor for multiline selections
var numcursors = 2; // the number of non-textline children of this box
var master = 0;
// Cursor Manipulation //////////////////////////////////////////////////////////////////////
// cx1, cy1 is the coordinates (in characters, not pixels) of the start of the selection
// cx2, cy2 is the coordinates (in characters, not pixels) of the end of the selection
cx2 = cx1 = cy2 = cy1 = 0;
// the x-coordinate of the beginning of the selection, in characters
_cx1 = function(a) {
a = xwt.math.floor(a);
if (0 > a) { if (cy1 > 0) cy1--; a = thisbox[cy1].text.length; }
if (numchildren - numcursors > cy1 and a > thisbox[cy1].text.length) { cy1++; a = 0; }
curs.x = xwt.textwidth(font, thisbox[cy1].text.substring(0, a));
arguments.cascade(a);
sync();
}
// the x-coordinate of the end of the selection, in characters
_cx2 = function(a) {
a = xwt.math.floor(a);
if (0 > a) { cy2--; a = thisbox[cy2].text.length; }
if (numchildren - numcursors > cy2 and a > thisbox[cy2].text.length) { cy2++; a = 0; }
arguments.cascade(a);
sync();
}
// the y-coordinate of the beginning of the selection, in characters
_cy1 = function(a) {
a = xwt.math.floor(a);
if (0 > a) { a = 0; cx1 = 0; }
arguments.cascade(a);
if (cx1 > thisbox[a].text.length) cx1 = thisbox[a].text.length;
sync();
}
// the y-coordinate of the end of the selection, in characters
_cy2 = function(a) {
a = xwt.math.floor(a);
arguments.cascade(a);
while (a >= numchildren - numcursors) {
var b = getbox();
b.text = "";
thisbox[numchildren - numcursors] = b;
}
if (cx2 > thisbox[a].text.length) cx2 = thisbox[a].text.length;
sync();
}
// the x-coordinate of the beginning of the selection, measured in _pixels_
__px1 = function() {
if (cy1 > numchildren || thisbox[cy1].text == null) return 0;
return xwt.textwidth(font, thisbox[cy1].text.substring(0, cx1));
}
// the x-coordinate of the end of the selection, measured in _pixels_
__px2 = function() {
if (cy2 > numchildren || thisbox[cy2].text == null) return 0;
return xwt.textwidth(font, thisbox[cy2].text.substring(0, cx2));
}
// on the r-th row, this will determine how many characters are to the left of a
// point b pixels from the left edge of the edit Algorithm is a binary search,
// making educated guesses based on the average width of a character on that line.
getboundary = function(r, b) {
if (o >= b) return 0;
var s = thisbox[r].text;
var left = 0; // the left boundary of the search region, in characters
var right = s.length; // the right boundary of the search region, in characters
var start = 0; // the left boundary of the search region, in pixels
// the right boundary of the search region, in pixels
var end = xwt.textwidth(font, s);
if (b >= end) return right; // short circuit if we're off the end
var avgwidth = end / right; // average width of one character
while(true) {
var middle = left + (b - start) / avgwidth; // make a guess at where we should look
// extra safety guard against infinite loops
if (left == right) return left;
if (left >= middle) middle = left + 1;
if (middle >= right) middle = left - 1;
start = xwt.textwidth(font, s.substring(0, middle));
if (start > b) {
right = middle;
} else if (b >= start + xwt.textwidth(font, s.charAt(middle))) {
left = middle;
} else {
return middle;
}
}
}
// returns the row containing the point yp pixels from the top of the widget
getrow = function(yp) { return xwt.math.min(numchildren - numcursors - 1, xwt.math.floor(yp / lineheight)); }
sync = function() {
curs.x = px1;
curs.y = thisbox[cy1].y; //lineheight * cy1 + thisbox[0].y;
curs.width = px2 - px1;
if (1 > curs.width) { curs.width = 1; curs.text = ""; }
curs2.invisible = true;
if (cy1 != cy2) {
curs.width = width - curs.x;
curs2.invisible = false;
curs2.x = 0;
curs2.y = thisbox[cy2].y; //lineheight * cy2;
curs2.height = lineheight;
curs2.width = px2;
}
for(var i=0; numchildren - numcursors>i; i++) {
if (i>cy1 and cy2>i) {
thisbox[i].textcolor = "white";
thisbox[i].color = "blue";
} else {
thisbox[i].textcolor = "black";
thisbox[i].color = null;
}
}
curs.text = (thisbox[cy1] == null || thisbox[cy1].text == null) ? null :
thisbox[cy1].text.substring(getboundary(cy1, curs.x),
getboundary(cy1, curs.x + curs.width));
curs2.text = (thisbox[cy2] == null || thisbox[cy2].text == null) ? null :
thisbox[cy2].text.substring(getboundary(cy2, curs2.x),
getboundary(cy2, curs2.x + curs2.width));
}
// Externally Visible Traps //////////////////////////////////////////////////////////
_focused = function(f) {
if (!f) {
if (curs.width == 1) curs.invisible = true;
} else {
curs.invisible = false;
}
}
// returns the currently-selected text
__selection = function() {
var ret = "";
if (cy1 == cy2) {
ret += thisbox[cy1].text.substring(cx1, cx2);
} else {
ret += thisbox[cy1].text.substring(cx1) + "\n";
for(var i=cy1 + 1; cy2 > i; i++) ret += thisbox[i].text + "\n";
ret += thisbox[cy2].text.substring(0, cx2);
}
return ret;
}
_font = function(f) {
curs.height = curs2.height = lineheight = f == null ? xwt.textheight() : xwt.textheight(f);
for(var i=0; numchildren>i; i++) {
thisbox[i].font = f;
if (numchildren - numcursors > i) thisbox[i].minheight = lineheight;
}
}
font = null;
_editable = function(e) {
if (e) {
auto_focus = true;
color = "white";
} else {
auto_focus = false;
focused = false;
curs.invisible = true;
curs2.invisible = true;
}
}
_multiline = function(m) {
if (m and m != "false") {
while(numchildren > numcursors + 1) thisbox[1].thisbox = null;
arguments.cascade(true);
} else {
arguments.cascade(false);
}
}
__text = function(t) {
if (arguments.length == 0) {
var ret = "";
for(var i=0; numchildren - numcursors>i; i++) ret = ret + thisbox[i].text + "\n";
return ret.substring(0, ret.length - 1); // chop off trailing CR
}
var me = this;
for(var i = numchildren - numcursors - 1; i >= 0; i--) {
static.boxen[static.boxen.length] = me[i];
me[i].thisbox = null;
}
cy1 = 0; cy2 = 0; cx1 = 0; cx2 = 0;
xwt.thread = function() {
try { insert_text(t); } catch (e) { xwt.println(e); }
}
}
last_was_a_kill = false; // indicates if the last "action" was a kill -- lets us know if we
// we should append newly killed text to the kill buffer or clear it
// Text Manipulation /////////////////////////////////////////////////////////////
// deletes the region between the start and end of the selection
var nuke_selection = function() {
if (cy1 != cy2 and thisbox[cy1].text != null and thisbox[cy2].text != null) {
thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) + thisbox[cy2].text.substring(cx2);
for(var i=cy1 + 1; cy2 >= i; i++) thisbox[cy1 + 1].thisbox = null;
cy2 = cy1;
cx2 = cx1;
} else {
thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) + thisbox[cy1].text.substring(cx2);
cx2 = cx1;
}
}
var getbox = function() {
if (static.boxen.length == 0) static.boxen[0] = xwt.newBox("box");
var ret = static.boxen[static.boxen.length - 1];
static.boxen.length--;
ret.minheight = lineheight;
ret.vshrink = true;
ret.textcolor = "black";
ret.font = font;
ret.align = "topleft";
ret.invisible = false;
ret.absolute = false;
return ret;
}
// nukes the selected region and inserts arg in its place
var insert_text = function(arg) {
var mine = master = xwt.math.random();
if (!multiline) arg = (arg + "").replace(static.cr_regexp, "");
if (curs.width > 1) nuke_selection();
while(!(arg == null || arg == "" || arg.replace == null)) {
cy2 = cy1; cx2 = cx1;
// insert a new line if necessary
if (arg.charAt(0) == '\n') {
var n = getbox();
thisbox[cy1 + 1] = n;
n.text = thisbox[cy1].text.substring(cx1);
thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1);
cy1++; cy2 = cy1; cx1 = 0; cx2 = 0;
arg = arg.substring(1);
xwt.yield();
if (master != mine) return;
}
var upto = arg.indexOf('\n') == -1 ? arg.length : arg.indexOf('\n');
var itext = arg.substring(0, upto);
thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) + itext + thisbox[cy1].text.substring(cx1);
cx1 += itext.length;
arg = arg.substring(upto);
}
cy2 = cy1;
cx2 = cx1;
master = 0;
}
// XWT Event Handlers //////////////////////////////////////////////////////////////
_Press2 = function() {
xwt.println("curs.x = " + curs.x);
xwt.println("curs.y = " + curs.y);
xwt.println("curs.w = " + curs.width);
xwt.println("curs.h = " + curs.height);
xwt.println("curs.i = " + curs.invisible);
xwt.println("curs[] = " + indexof(curs));
xwt.println("numchi = " + numchildren);
}
_Press1 = function() {
focused = true;
if (master != 0) return;
last_was_a_kill = false;
root._Move = function() {
curs.invisible = false;
var mousey_row = getrow(mousey);
var pressy_row = getrow(pressy);
if (pressy_row > mousey_row || (mousey_row == pressy_row and pressx > mousex)) {
cy1 = mousey_row;
cy2 = pressy_row;
cx1 = getboundary(cy1, mousex);
cx2 = getboundary(cy2, pressx);
} else {
var newcy2 = mousey_row;
if (newcy2 > numchildren - numcursors - 1) newcy2 = numchildren - numcursors - 1;
cy2 = newcy2;
cy1 = pressy_row;
cx2 = xwt.math.min(getboundary(cy2, mousex), thisbox[cy2].text.length);
cx1 = getboundary(cy1, pressx);
}
}
root.__Release1 = function() {
root._Move = null;
root._Release1 = null;
if (cx1 != cx2 || cy1 != cy2) xwt.clipboard = selection;
}
pressx = mousex;
pressy = mousey;
cy1 = getrow(mousey);
cy2 = getrow(mousey);
cx2 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
cx1 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
}
__Press3 = function() {
last_was_a_kill = false;
cy1 = cy2 = getrow(mousey);
cx2 = cx1 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
if (xwt.clipboard != null and editable) insert_text(xwt.clipboard);
}
_DoubleClick1 = function() {
cy1 = cy2 = getrow(mousey);
cx1 = 0; cx2 = thisbox[cy1].text.length;
xwt.clipboard = selection;
}
var key_C_Q = function() {
// C-q => fill
var start = cy1;
var stop = cy2;
// if a region wasn't selected, fill this paragraph alone
if (cy1 == cy2) {
while (start > 0 and thisbox[start].text.match(static.nonwhitespace)) start--;
while (numchildren - numcursors > stop and thisbox[stop].text.match(static.nonwhitespace)) stop++;
}
// pull out the text-to-be filled
var filltext = "";
for(var i=start; stop >= i; i++) {
filltext += thisbox[start].text + "\n";
if (numchildren > numcursors) {
static.boxen[static.boxen.length] = thisbox[start];
thisbox[start].thisbox = null;
} else {
thisbox[start].text = "";
}
}
xwt.println("prefill text:\n" + filltext);
filltext = filltext.replace(/^\\s+/);
var quoted = false;
if (filltext.substring(0, 2) == "> ") {
filltext = filltext.substring(2).replace(/\n> /g, "\n");
quoted = true;
}
filltext = filltext.replace(/\s+/g, " ");
var newfilltext = "";
while (filltext.length > 0) {
var tmp = filltext.substring(0, 78);
var tmp2;
if (tmp.lastIndexOf(' ') != -1) {
filltext = tmp.substring(tmp.lastIndexOf(' ') + 1) + filltext.substring(78);
tmp2 = tmp.substring(0, tmp.lastIndexOf(' '));
} else {
tmp2 = filltext;
filltext = "";
}
newfilltext += (quoted ? "> " : "") + tmp2 + "\n";
}
cy1 = start; cy2 = start;
cx1 = 0; cx2 = 0;
xwt.println("newfilltext:\n" + newfilltext);
insert_text("\n" + newfilltext + "\n");
}
var key_C_W = function() { if (xwt.clipboard != null) xwt.clipboard = selection;
if (editable) nuke_selection();
}
var key_C_K = function() { if (!editable) return;
if (1 >= curs.width and cx1 == thisbox[cy1].text.length) cx2++;
else if (1 >= curs.width) cx2 = thisbox[cy2].text.length;
if (!last_was_a_kill and xwt.clipboard != null) xwt.clipboard = "";
if (xwt.clipboard != null) xwt.clipboard += selection;
else xwt.clipboard = selection;
nuke_selection();
}
_KeyPressed = function(key) {
if (master != 0) return;
if (!focused) return;
var kill = false;
if (key == "C-a") { cx1 = 0; cx2 = 0; }
else if (key == "C-e") { cx1 = thisbox[cy2].text.length; cx2 = cx1; cy2 = cy1; }
else if (key == "C-d" || key == "delete") { if (!editable) return; if (1 >= curs.width) cx2++; nuke_selection(); }
else if (key == "C-y") { if (xwt.clipboard != null and editable) insert_text(xwt.clipboard); }
else if (key == "C-k") key_C_K();
else if (key == "C-w") key_C_W();
else if (key == "C-h" || key == "back_space") {
if (!editable) return;
if (cx1 == 0 and cx2 == 0 and cy1 == 0 and cy2 == 0) return;
if (1 >= curs.width) cx1--;
nuke_selection();
}
else if (key == "C-b" || key == "left") { cx1--; cy2 = cy1; cx2 = cx1; }
else if (key == "C-p" || key == "up") { cy1--; cy2 = cy1; cx2 = cx1; }
else if (key == "C-f" || key == "right") { cx2++; cy1 = cy2; cx1 = cx2; }
else if (key == "C-m" || key == "C-o" || key == "enter" || key == "C-j") { if (editable) insert_text("\n"); }
else if (key == "C-n" || key == "down") { cy2++; cy1 = cy2; cx1 = cx2; }
else if (key == "C-q") key_C_Q();
else if (key.length == 1) {
if (!editable) return;
if (curs.width > 1) nuke_selection();
thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) +
key +
thisbox[cy1].text.substring(cx1);
cx2 = cx1 + 1; cx1 = cx2;
}
last_was_a_kill = key == "C-k";
}