2003/05/12 05:10:28
authormegacz <megacz@xwt.org>
Fri, 30 Jan 2004 07:00:21 +0000 (07:00 +0000)
committermegacz <megacz@xwt.org>
Fri, 30 Jan 2004 07:00:21 +0000 (07:00 +0000)
darcs-hash:20040130070021-2ba56-251cc43991738f8e8016e7da2beac2ba9cf6395a.gz

src/org/xwt/builtin/edit_lib.xwt [new file with mode: 0644]

diff --git a/src/org/xwt/builtin/edit_lib.xwt b/src/org/xwt/builtin/edit_lib.xwt
new file mode 100644 (file)
index 0000000..abcc8eb
--- /dev/null
@@ -0,0 +1,680 @@
+<!-- Copyright 2002 NeuronForge Pty Ltd, see COPYING file for licensing [LGPL] -->
+<xwt>
+    <static>
+        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;
+        }
+    </static>
+
+
+    <template>
+        _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;
+            }
+        }
+
+    </template>
+</xwt>