var cursors = [];
var worddivider = [' ', '-'];
xwt.thread = function() {
while (true) { xwt.sleep(1000); for (var i=0; cursors.length > i; i++) { cursors[i].blink = !cursors[i].blink; } }
}
// Returns the number of characters the pixel position pos is into text t. end is the pixel with of t.
// t : string -- basis for character counting.
// pxpos : int -- pixel position on the text from string t.
// pxlen : int -- pixel width of text t (known in form of box length, so passed for speed).
// tfont : string -- name of font used for width of text.
var getpos = function(t, pxpos, pxlen, tfont) {
// Short circuit extremes.
if (0 >= pxpos) return 0;
if (pxpos >= pxlen) return t.length;
// Inital guess based on average character width.
var guessch = xwt.math.min(t.length, xwt.math.floor(pxpos / (pxlen / t.length)));
var guesspx = xwt.textwidth(tfont, t.substring(0, guessch));
if (guesspx > pxpos) {
while (guesspx > pxpos) {
// Textwidth of individual character must account for font kerning.
guesspx -= xwt.textwidth(tfont, t.substring(guessch -1, guessch +1)) - xwt.textwidth(tfont, t.charAt(guessch));
guessch--;
}
} else if (pxpos > guesspx) {
while (pxpos > guesspx) {
guessch++;
if (guessch >= t.length) break;
guesspx += xwt.textwidth(tfont, t.substring(guessch -1, guessch+1)) - xwt.textwidth(tfont, t.charAt(guessch));
}
guessch--; // Round down.
}
return guessch;
}
_multiline = function(v) {
if (!v) {
while (content.numchildren > 1) { reapLine(content[content.numchildren -1]); }
wrap = null;
}
}
_disabled = function(v) {
curs.disabled = v;
}
_editable = function(v) {
}
_wrap = function(v) {
// Used on _SizeChange if wrap needs to know.
var resize = function() {
xwt.thread = function() {
// TODO: Only run this change if the width is different.
for (var i = 0; content.numchildren > i; i++) { content[i].fulltext = content[i].fulltext; }
};
};
if (multiline and v == "line") {
content.vshrink = true;
content.hshrink = false;
content.maxwidth = xwt.maxdim; // Must reset maxwidth after shrink = true.
ref_fulltext = fulltext_linewrap;
_SizeChange = resize;
} else if (multiline and v == "word") {
content.vshrink = true;
content.hshrink = false;
content.maxwidth = xwt.maxdim;
ref_fulltext = fulltext_wordwrap;
_SizeChange = resize;
} else {
content.shrink = true;
ref_fulltext = fulltext_nowrap;
_SizeChange = null;
}
// Reset functions on current lines.
for (var i = 0; content.numchildren > i; i++) {
content[i]._fulltext = ref_fulltext;
content[i].fulltext = content[i].fulltext;
}
}
_selectcolor = function(v) { sel1.color = sel2.color = v; }
_selecttextcolor = function(v) { sel1.textcolor = sel2.textcolor = v; }
_font = function(f) {
lineheight = xwt.textheight(f);
if (lineheight > 0) { minheight = content.minheight = linecount * lineheight; }
for (var i=0; content.numchildren > i; i++) { content[i].font = f; }
sel1.font = sel2.font = curs.font = f;
}
__text = function(t) {
if (arguments.length == 0) {
var s = content[0].fulltext;
for (var i=1; content.numchildren > i; i++) { s = s + '\n' + content[i].fulltext; }
return s;
}
deleteText(0, 0, content.numchildren - 1, content[content.numchildren - 1].fulltext.length);
insertText(0, 0, t);
}
__selection = function(t) {
if (arguments.length == 0) {
if (sel1.cl == -1) return "";
var s = sel1.text;
if (sel1.cl == sel2.cl) {
for (var i=sel1.cy+1; sel2.cy > i; i++) { s += content[sel1.cl][i].text; }
} else {
for (var i=sel1.cy+1; content[sel1.cl].numchildren > i; i++) { s += content[sel1.cl][i].text; }
for (var i=sel1.cl+1; sel2.cl > i; i++) { s += '\n' + content[i].fulltext; }
s += '\n';
for (var i=0; sel2.cy > i; i++) { s += content[sel2.cl][i].text; }
s += sel2.text;
}
return s;
}
deleteSelection();
insertText(curs.cl, curs.cx, t);
}
// PRIVATE VARIABLES //////////////////////////////////////////////////////////////////////////////////////////
// Stores the inital point of the current selection.
var sel = { cl : 0, cy : 0, px : 0 };
// The pixel height of the current font.
var _lineheight = function(l) { curs.height = sel1.height = sel2.height = l; }
// Number of soft lines currently in this edit widget. Controlled by newLine() and reapLine().
var _linecount = function(l) {
arguments.cascade(l); if (l == 0) l = 1;
if (lineheight > 0) { minheight = content.minheight = l * lineheight; }
}
// Total number of characters stored in this text field.
var length = 0;
// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////
// Insert the given text at the given position.
insertText = function(cl, cx, t) {
// Check passed string.
if (t == null || 1 > t.length) return;
t = t.toString();
// Limit checking.
if (limit > 0 and length + t.length > limit) {
xwt.println("WARNING: Limit applied on inserted text.");
t = t.substring(0, limit - length);
}
// Make sure there are enough lines before hand.
for (var i=content.numchildren; cl >= i; i++) { content[i] = newLine(); }
// Parse the carridge returns out of t.
var newT = t.split("\n");
if (newT.length == 0) {
return;
} else if (newT.length == 1) {
content[cl].fulltext = content[cl].fulltext.substring(0, cx) + t + content[cl].fulltext.substring(cx);
length += t.length;
moveCursor(cl, cx+t.length);
} else {
if (multiline) {
// Add extra lines required by passed text.
for (var i = newT.length - 1; i > 0; i--) { content[cl+1] = newLine(); }
// Add the new text
var lastline = newT[newT.length - 1] + content[cl].fulltext.substring(cx);
content[cl].fulltext = content[cl].fulltext.substring(0, cx) + newT[0];
for (var i=1; newT.length-1 > i; i++) { content[cl+i].fulltext = newT[i]; }
content[cl + newT.length - 1].fulltext = lastline;
moveCursor(cl + newT.length - 1, newT[newT.length -1].length);
} else {
xwt.println("WARNING: Single line edit, ignoring all text after the carrige return.");
content[0].fulltext = content[0].fulltext.substring(0, cx) + newT[0] + content[0].fulltext.substring(cx);
moveCursor(0, cx + newT[0].length);
}
}
}
// Delete the text within the given range.
deleteText = function(cl1, cx1, cl2, cx2) {
content[cl1].fulltext = content[cl1].fulltext.substring(0, cx1) + content[cl2].fulltext.substring(cx2);
for (; cl2 > cl1; cl2--) { reapLine(content[cl2]); }
}
// Select the text within the given range.
selectText = function(cl1, cx1, cl2, cx2) {
// Find cy1 and px1.
var cy1 = 0;
var px1 = cx1;
for (; content[cl1].numchildren > cy1; cy1++) {
if (content[cl1][cy1].text.length > px1) { break; }
else { px1 -= content[cl1][cy1].text.length; }
}
// Find cy2 and px2.
var cy2 = 0;
var px2 = cx2;
for (; content[cl2].numchildren > cy2; cy2++) {
if (content[cl2][cy2].text.length >= px2) { break; }
else { px2 -= content[cl2][cy2].text.length; }
}
// Call the internal select function.
sel.cl = cl1; sel.cy = cy1; sel.px = px1;
select(cl1, cy1, px1, cl2, cy2, px2);
moveCursorToCy(cl2, cy2, px2);
}
// Clear the current selection.
clearSelection = function() {
if (sel.cl == -1 || sel1.cl == -1) return;
moveCursorToCy(sel1.cl, sel1.cy, sel1.px);
// Clear any selected lines.
for (var i=sel1.cl; sel2.cl >= i; i++) { if (content[i] != null) content[i].selected = false; }
// Clear the selection values
sel.cl = sel.px = sel.cy = sel1.cl = sel1.cx = sel1.cy = sel2.cl = sel2.cx = sel2.cy = -1;
sel1.text = sel2.text = "";
}
// Delete the text currently within the selected range.
deleteSelection = function() {
if (sel1.cl == -1 || sel2.cl == -1) return;
deleteText(sel1.cl, calcCx(sel1.cl, sel1.cy, sel1.px), sel2.cl, calcCx(sel2.cl, sel2.cy, sel2.px));
clearSelection();
}
// External interface for moving the mouse cursor.
moveCursor = function(cl, cx) {
// Work out what subline cx is on.
var cy, px;
if (cl >= content.numchildren) return;
if (cx > content[cl].fulltext.length) {
if (content.numchildren -1 > cl) {
cl++; cx = 0; cy = 0; px = 0;
} else {
cx = content[cl].fulltext.length;
cy = content[cl].numchildren -1;
px = content[cl][cy].text.length;
}
} else {
px = cx;
for (cy = 0; content[cl].numchildren > cy; cy++) {
if (content[cl][cy].text.length >= px) break;
px -= content[cl][cy].text.length;
}
}
// Call internal move function.
moveCursorToPos(cl, cx, cy, px);
}
// PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////
var updateSelectionCx = function(cl, cx) {
if (cl >= content.numchildren) {
var t = content[content.numchildren -1];
updateSelection(content.numchildren -1, t.numchildren, t[t.numchildren -1].text.length);
return;
} else if (cx > content[cl].fulltext.length) {
updateSelection(cl, content[cl].numchildren -1, content[cl][content[cl].numchildren -1].text.length);
return;
}
var cy;
for (cy = 0; cx > 0; cy++) {
cx -= content[cl][cy].text.length;
}
cy--;
updateSelection(cl, cy, content[cl][cy].text.length + cx);
}
// Used in the _KeyPress trap to literally update a current selection. The new 'floating point' is passed, and
// the original value stored in the private property sel is used to balance with.
var updateSelection = function(cl, cy, px) {
// Very very very padentic checking. I dare you to do more checking. :-)
if (0 > px || 0 > cy || 0 > cl) {
if (cy - 1 >= 0) { cy--; px = content[cl][cy].text.length; }
else if (cl - 1 >= 0) { cl--; cy = content[cl].numchildren - 1; px = content[cl][cy].text.length; }
else { cl = 0; cy = 0; px = 0; }
} else if (cl >= content.numchildren) {
cl = content.numchildren - 1;
cy = content[cl].numchildren - 1;
px = content[cl][cy].text.length;
} else if (cy >= content[cl].numchildren || px > content[cl][cy].text.length) {
if (content[cl].numchildren > cy + 1) { cy++; px = 0; }
else if (content.numchildren > cl + 1) { cl++; cy = 0; px = 0; }
else { cl = content.numchildren - 1; cy = content[cl].numchildren - 1; px = content[cl][cy].text.length; }
}
// If there is no current selection, set to current mouse position.
if (sel.cl == -1) { sel.cl = curs.cl; sel.cy = curs.cy; sel.px = curs.px; }
// Decide on dominant point and call internal select function.
if (cl > sel.cl || (cl == sel.cl and cy > sel.cy) || (cl == sel.cl and cy == sel.cy and px > sel.px)) {
select(sel.cl, sel.cy, sel.px, cl, cy, px);
} else {
select(cl, cy, px, sel.cl, sel.cy, sel.px);
}
// Update cursor position.
moveCursorToCy(cl, cy, px);
}
// The meat behind all select calls. This function draws the select boxes on top of the edit widget.
var select = function(cl1, cy1, px1, cl2, cy2, px2) {
if (disabled) return;
// Deselect the current full-selection lines that are outside the new selection area.
if (sel1.cl == cl1) {
for (var i=sel1.cy+1; cy1 >= i; i++) { content[cl1][i].selected = false; }
} else if (cl1 > sel1.cl) {
for (var i=xwt.math.max(0, sel1.cl); cl1 >= i; i++) { content[i].selected = false; }
}
if (sel2.cl == cl2) {
for (var i=sel2.cy-1; i >= cy2; i--) { content[cl2][i].selected = false; }
} else if (sel2.cl > cl2) {
for (var i=xwt.math.max(0, sel2.cl); i >= cl2; i--) { content[i].selected = false; }
}
// Store point data.
sel1.cl = cl1; sel1.cy = cy1; sel1.px = px1;
sel2.cl = cl2; sel2.cy = cy2; sel2.px = px2;
// Place first select box.
sel1.y = content[cl1].y + content[cl1][cy1].y;
sel1.x = xwt.textwidth(font, content[cl1][cy1].text.substring(0, px1));
if (cl1 == cl2 and cy1 == cy2) {
// Only the first select box is required.
sel1.text = content[cl1][cy1].text.substring(px1, px2);
sel2.text = "";
} else {
sel1.text = content[cl1][cy1].text.substring(px1);
sel2.y = content[cl2].y + content[cl2][cy2].y;
sel2.x = 0;
sel2.text = content[cl2][cy2].text.substring(0, px2);
// Mark middle lines.
if (cl1 == cl2) {
for (var i=cy1+1; cy2 > i; i++) { content[cl1][i].selected = true; }
} else {
for (var i=cy1+1; content[cl1].numchildren > i; i++) { content[cl1][i].selected = true; }
for (var i=cl1+1; cl2 > i; i++) { content[i].selected = true; }
for (var i=0; cy2 > i; i++) { content[cl2][i].selected = true; }
}
}
}
// Internal reference function. Calculates the cx position of the cursor based on cl, cy and px,
// and then passes it to the primary internal function moveCursorToPos() for movement.
var moveCursorToCy = function(cl, cy, px) { moveCursorToPos(cl, calcCx(cl, cy, px), cy, px); }
var calcCx = function(cl, cy, px) { for (cy--; cy >= 0; cy--) { px += content[cl][cy].text.length; } return px; }
// Internal function for moving the mouse cursor. px represents number of characters over in specified subline.
// NOTE: The mouse cursor is the closest the external functions get to affecting the internal structure of a line.
var moveCursorToPos = function(cl, cx, cy, px) {
// Check the passed values are within reasonable constaints.
if (cl >= content.numchildren) { cl = content.numchildren - 1; }
if (cy >= content[cl].numchildren) {
if (content.numchildren - 1 > cl) { cl++; cy = 0; cx = calcCx(cl, cy, px); }
else { cy = content[cl].numchildren -1; cx = calcCx(cl, cy, px); }
} else if (0 > cy) {
if (cl > 0) { cl--; cy = content[cl].numchildren - 1; cx = calcCx(cl, cy, px); }
else { cy = 0; cx = calcCx(cl, cy, px); }
}
if (0 > px) { px = 0; cx = calcCx(cl, cy, px); }
else if (px > content[cl][cy].text.length) { px = content[cl][cy].text.length; cx = calcCx(cl, cy, px); }
// Move the cursor.
curs.cl = cl; curs.cx = cx; curs.cy = cy; curs.px = px;
curs.y = content.y + content[cl].y + (lineheight * cy);
curs.x = content.x + xwt.textwidth(font, content[cl][cy].text.substring(0, px)) -1;
curs.blink = false;
// Speed Hack: As the cursor has values that match the names used by the focusarea variable, we
// simply pass the curs reference as the focusarea.
focusarea = curs;
if (0 > curs.x) curs.x = 0;
}
// Returns a box ready to be a full line, armed with the current fulltext trap.
var newLine = function() {
var b = xwt.newBox();
b.color = color;
b.align = "topleft";
b.invisible = false;
b[0] = newSoftline();
b._Press1 = function() { ref_press(arguments.trapee); }
b._selected = function(s) { for (var i=0; b.numchildren > i; i++) { b[i].selected = s; } }
b._font = function(f) { for (var i=0; b.numchildren > i; i++) { b[i].font = f; } }
b._fulltext = ref_fulltext;
b.fulltext = "";
b.orient = "vertical";
b.vshrink = true;
return b;
}
// Returns a box ready to be a soft line; one of the components of a line.
var newSoftline = function() {
var b = xwt.newBox();
b._selected = function(s) {
arguments.trapee.color = s ? selectcolor : color;
arguments.trapee.textcolor = s ? selecttextcolor : textcolor;
};
b.minheight = lineheight;
b.shrink = true;
b.color = color;
b.textcolor = textcolor;
b.font = font;
b.align = "topleft";
b.invisible = false;
linecount++;
return b;
}
// Takes the given line and strips it of traps and returns it to the static stack.
var reapLine = function(toReap) {
if (content.indexof(toReap) == -1) {
// Soft-line
linecount--;
} else {
// Hard-line, count all softline children.
linecount -= toReap.numchildren;
}
toReap.thisbox = null;
}
// SUBLINE FUNCTIONS //////////////////////////////////////////////
var fulltext_nowrap = function(t) {
arguments.trapee[0].text = t;
}
var fulltext_linewrap = function(t) {
var cw = width;
if (cw == 0) return;
var i = 0;
if (t.length == 0) arguments.trapee[0].text = "";
for (; t.length > 0; i++) {
if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
// TODO: Switch to getpos
var nl = static.getpos(t, cw, xwt.textwidth(font, t), font);
arguments.trapee[i].text = t.substring(0, nl);
t = t.substring(nl);
}
// Remove any excess lines.
if (i == 0) i++;
while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
}
var fulltext_wordwrap = function(t) {
var cw = width;
if (cw == 0) return;
var i = 0;
if (t.length == 0) arguments.trapee[0].text = "";
for (; t.length > 0; i++) {
if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
var nl = static.getpos(t, cw, xwt.textwidth(font, t), font);
var rl = nl;
if (t.length > nl) {
// TODO: Clean up, make it work off a static array of possible break points.
// TODO: Make it themeable as well.
for (; rl > 0; rl--) {
if (t.charAt(rl) == ' ' || t.charAt(rl) == '-') { rl++; break; }
}
if (0 >= rl || rl > nl) rl = nl;
}
arguments.trapee[i].text = t.substring(0, rl);
t = t.substring(rl);
}
// Remove any excess lines.
if (i == 0) i++;
while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
}
// Reference to the current function to use for the fulltext trap.
var ref_fulltext = fulltext_nowrap;
// Handles selection/cursor movement from mouse events.
// The passed value is a reference to the selected hard line.
var ref_press = function(refline) {
if (disabled) return;
root._Move = function() {
// Update Selection.
var linediff = xwt.math.floor((content.mousey - (content[sel.cl].y + content[sel.cl][sel.cy].y)) / lineheight);
var cl = sel.cl;
var cy = sel.cy;
// End of selection comes after start.
while (linediff > 0) {
cy++;
if (cy >= content[cl].numchildren) { cl++; cy = 0; }
if (cl >= content.numchildren) { cl--; break; }
linediff--;
}
// End of selection comes before start.
while (0 > linediff) {
cy--;
if (0 > cy) { cl--; cy = content[cl].numchildren -1; }
if (0 > cl) { cl=0; cy = 0; break; }
linediff++;
}
var px = static.getpos(content[cl][cy].text, content[cl][cy].mousex, content[cl][cy].width, font);
updateSelection(cl, cy, px);
}
root._Release1 = function() {
root._Move = root._Release1 = null;
// TODO: Put selection to clipboard.
}
// Set selection root position.
clearSelection();
sel.cl = content.indexof(refline);
sel.cy = xwt.math.floor(refline.mousey / lineheight);
if (sel.cy >= refline.numchildren) sel.cy = refline.numchildren -1;
else if (0 > sel.cy) sel.cy = 0;
sel.px = static.getpos(refline[sel.cy].text, refline[sel.cy].mousex, refline[sel.cy].width, font);
moveCursorToCy(sel.cl, sel.cy, sel.px);
}
// HELPER FUNCTIONS //////////////////////////////////////////////
// Nessesary for when a used clicks in the middle of a current selection.
_sel1 = _sel2 = function(s) {
if (s != null) {
s._Press1 = function(t) { if (arguments.trapee.cl >= 0) content[arguments.trapee.cl].Press1 = t; };
s._Release1 = function(t) { if (arguments.trapee.cl >= 0) content[arguments.trapee.cl].Release1 = t; };
}
}
_content = function(c) { if (c != null and c.numchildren == 0) c[0] = newLine(); }
_curs = function(c) {
if (c == null) {
for (var i=0; static.cursors.length > i; i++) {
if (static.cursors[i] == arguments.cascade()) { static.cursors[i] = null; break; }
}
} else {
// Add cursor to static array for 'blinking'.
static.cursors[static.cursors.length] = c;
}
}
key_enter = function() {
if (!multiline) return;
content[curs.cl +1] = newLine();
content[curs.cl +1].fulltext = content[curs.cl].fulltext.substring(curs.cx);
content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx);
xwt.thread = function() { moveCursor(curs.cl +1, 0); }
}
key_back_space = function() {
if (curs.cx == 0) {
if (curs.cl > 0) {
var px = content[curs.cl -1].fulltext.length;
content[curs.cl -1].fulltext = content[curs.cl -1].fulltext + content[curs.cl].fulltext;
reapLine(content[curs.cl]);
moveCursor(curs.cl -1, px); // Safe, moving up not down.
}
} else {
content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx -1) + content[curs.cl].fulltext.substring(curs.cx);
moveCursor(curs.cl, curs.cx -1); // Safe, not moving cl.
}
}
key_delete = function() {
if (curs.cx == content[curs.cl].fulltext.length) {
if (content.numchildren > 1) {
content[curs.cl].fulltext = content[curs.cl].fulltext + content[curs.cl +1].fulltext;
reapLine(content[curs.cl +1]);
}
} else {
content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx) + content[curs.cl].fulltext.substring(curs.cx +1);
}
}
// KEY HANDLER //////////////////////////////////////////////
_keypress = function(k) {
if (k == null || !editable || disabled) return;
// Process shortcut for single character entries.
if (k.length == 1) {
deleteSelection();
if (k.charAt(0) == '\n') {
insertText(curs.cl, curs.cx, k);
} else {
content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx)
+ k + content[curs.cl].fulltext.substring(curs.cx);
xwt.thread = function() { moveCursor(curs.cl, curs.cx+1); }
textChanged = true;
}
return;
}
k = k.substring(k.lastIndexOf('-')+1);
// Process movement commands.
if (k == "enter") {
deleteSelection(); key_enter();
textChanged = true;
} else if (k == "back_space") {
if (sel1.cl > -1) { deleteSelection(); }
else { key_back_space(); }
textChanged = true;
} else if (k == "delete") {
if (sel1.cl > -1) { deleteSelection(); }
else { key_delete(); }
textChanged = true;
}
}