resolve darcs stupidity
[org.ibex.core.git] / src / org / ibex / builtin / edit_lib.ibex
1 <!-- Copyright 2002 NeuronForge Pty Ltd, see COPYING file for licensing [LGPL] -->
2 <ibex>
3     <static>
4         var cursors = [];
5         var worddivider = [' ', '-'];
6
7         ibex.thread = function() {
8             while (true) { ibex.sleep(1000); for (var i=0; cursors.length > i; i++) { cursors[i].blink = !cursors[i].blink; } }
9         }
10
11         // Returns the number of characters the pixel position pos is into text t. end is the pixel with of t.
12         //  t     : string -- basis for character counting.
13         //  pxpos : int    -- pixel position on the text from string t.
14         //  pxlen : int    -- pixel width of text t (known in form of box length, so passed for speed).
15         //  tfont : string -- name of font used for width of text.
16         var getpos = function(t, pxpos, pxlen, tfont) {
17             // Short circuit extremes.
18             if (0 >= pxpos) return 0;
19             if (pxpos >= pxlen) return t.length;
20
21             // Inital guess based on average character width.
22             var guessch = ibex.math.min(t.length, ibex.math.floor(pxpos / (pxlen / t.length)));
23             var guesspx = ibex.textwidth(tfont, t.substring(0, guessch));
24
25             if (guesspx > pxpos) {
26                 while (guesspx > pxpos) {
27                     // Textwidth of individual character must account for font kerning.
28                     guesspx -= ibex.textwidth(tfont, t.substring(guessch -1, guessch +1)) - ibex.textwidth(tfont, t.charAt(guessch));
29                     guessch--;
30                 }
31             } else if (pxpos > guesspx) {
32                 while (pxpos > guesspx) {
33                     guessch++;
34                     if (guessch >= t.length) break;
35                     guesspx += ibex.textwidth(tfont, t.substring(guessch -1, guessch+1)) - ibex.textwidth(tfont, t.charAt(guessch));
36                 }
37                 guessch--;  // Round down.
38             }
39
40             return guessch;
41         }
42     </static>
43
44
45     <template>
46         _multiline = function(v) {
47             if (!v) {
48                 while (content.numchildren > 1) { reapLine(content[content.numchildren -1]); }
49                 wrap = null;
50             }
51         }
52
53         _disabled = function(v) {
54             curs.disabled = v;
55         }
56
57         _editable = function(v) {
58         }
59
60         _wrap = function(v) {
61             // Used on _SizeChange if wrap needs to know.
62             var resize = function() {
63                 ibex.thread = function() {
64                     // TODO: Only run this change if the width is different.
65                     for (var i = 0; content.numchildren > i; i++) { content[i].fulltext = content[i].fulltext; }
66                 };
67             };
68
69             if (multiline and v == "line") {
70                 content.vshrink = true;
71                 content.hshrink = false;
72                 content.maxwidth = ibex.maxdim; // Must reset maxwidth after shrink = true.
73                 ref_fulltext = fulltext_linewrap;
74                 _SizeChange = resize;
75
76             } else if (multiline and v == "word") {
77                 content.vshrink = true;
78                 content.hshrink = false;
79                 content.maxwidth = ibex.maxdim;
80                 ref_fulltext = fulltext_wordwrap;
81                 _SizeChange = resize;
82
83             } else {
84                 content.shrink = true;
85                 ref_fulltext = fulltext_nowrap;
86                 _SizeChange = null;
87             }
88
89             // Reset functions on current lines.
90             for (var i = 0; content.numchildren > i; i++) {
91                 content[i]._fulltext = ref_fulltext;
92                 content[i].fulltext  = content[i].fulltext;
93             }
94         }
95
96         _selectcolor = function(v) { sel1.color = sel2.color = v; }
97         _selecttextcolor = function(v) { sel1.textcolor = sel2.textcolor = v; }
98
99         _font = function(f) {
100             lineheight = ibex.textheight(f);
101             if (lineheight > 0) { minheight = content.minheight = linecount * lineheight; }
102             for (var i=0; content.numchildren > i; i++) { content[i].font = f; }
103             sel1.font = sel2.font = curs.font = f;
104         }
105
106         __text = function(t) {
107             if (arguments.length == 0) {
108                 var s = content[0].fulltext;
109                 for (var i=1; content.numchildren > i; i++) { s = s + '\n' + content[i].fulltext; }
110                 return s;
111             }
112
113             deleteText(0, 0, content.numchildren - 1, content[content.numchildren - 1].fulltext.length);
114             insertText(0, 0, t);
115         }
116
117         __selection = function(t) {
118             if (arguments.length == 0) {
119                 if (sel1.cl == -1) return "";
120
121                 var s = sel1.text;
122
123                 if (sel1.cl == sel2.cl) {
124                     for (var i=sel1.cy+1; sel2.cy > i; i++) { s += content[sel1.cl][i].text; }
125                 } else {
126                     for (var i=sel1.cy+1; content[sel1.cl].numchildren > i; i++) { s += content[sel1.cl][i].text; }
127                     for (var i=sel1.cl+1; sel2.cl > i; i++) { s += '\n' + content[i].fulltext; }
128                     s += '\n';
129                     for (var i=0; sel2.cy > i; i++) { s += content[sel2.cl][i].text; }
130                     s += sel2.text;
131                 }
132
133                 return s;
134             }
135
136             deleteSelection();
137             insertText(curs.cl, curs.cx, t);
138         }
139
140
141         // PRIVATE VARIABLES //////////////////////////////////////////////////////////////////////////////////////////
142
143         // Stores the inital point of the current selection.
144         var sel = { cl : 0, cy : 0, px : 0 };
145
146         // The pixel height of the current font.
147         var _lineheight = function(l) { curs.height = sel1.height = sel2.height = l; }
148
149         // Number of soft lines currently in this edit widget. Controlled by newLine() and reapLine().
150         var _linecount = function(l) {
151             arguments.cascade(l);  if (l == 0) l = 1;
152             if (lineheight > 0) { minheight = content.minheight = l * lineheight; }
153         }
154
155         // Total number of characters stored in this text field.
156         var length = 0;
157
158
159         // PUBLIC FUNCTIONS  //////////////////////////////////////////////////////////////////////////////////////////
160
161         // Insert the given text at the given position.
162         insertText = function(cl, cx, t) {
163             // Check passed string.
164             if (t == null || 1 > t.length) return;
165             t = t.toString();
166
167             // Limit checking.
168             if (limit > 0 and length + t.length > limit) {
169                 ibex.println("WARNING: Limit applied on inserted text.");
170                 t = t.substring(0, limit - length);
171             }
172
173             // Make sure there are enough lines before hand.
174             for (var i=content.numchildren; cl >= i; i++) { content[i] = newLine(); }
175
176             // Parse the carridge returns out of t.
177             var newT = t.split("\n");
178
179             if (newT.length == 0) {
180                 return;
181             } else if (newT.length == 1) {
182                 content[cl].fulltext = content[cl].fulltext.substring(0, cx) + t + content[cl].fulltext.substring(cx);
183                 length += t.length;
184
185                 moveCursor(cl, cx+t.length);
186             } else {
187                 if (multiline) {
188                     // Add extra lines required by passed text.
189                     for (var i = newT.length - 1; i > 0; i--) { content[cl+1] = newLine(); }
190
191                     // Add the new text
192                     var lastline = newT[newT.length - 1] + content[cl].fulltext.substring(cx);
193                     content[cl].fulltext = content[cl].fulltext.substring(0, cx) + newT[0];
194                     for (var i=1; newT.length-1 > i; i++) { content[cl+i].fulltext = newT[i]; }
195                     content[cl + newT.length - 1].fulltext = lastline;
196
197                     moveCursor(cl + newT.length - 1, newT[newT.length -1].length);
198                 } else {
199                     ibex.println("WARNING: Single line edit, ignoring all text after the carrige return.");
200                     content[0].fulltext = content[0].fulltext.substring(0, cx) + newT[0] + content[0].fulltext.substring(cx);
201
202                     moveCursor(0, cx + newT[0].length);
203                 }
204             }
205         }
206
207         // Delete the text within the given range.
208         deleteText = function(cl1, cx1, cl2, cx2) {
209             content[cl1].fulltext = content[cl1].fulltext.substring(0, cx1) + content[cl2].fulltext.substring(cx2);
210             for (; cl2 > cl1; cl2--) { reapLine(content[cl2]); }
211         }
212
213         // Select the text within the given range.
214         selectText = function(cl1, cx1, cl2, cx2) {
215             // Find cy1 and px1.
216             var cy1 = 0;
217             var px1 = cx1;
218             for (; content[cl1].numchildren > cy1; cy1++) {
219                 if (content[cl1][cy1].text.length > px1) { break; }
220                 else { px1 -= content[cl1][cy1].text.length; }
221             }
222
223             // Find cy2 and px2.
224             var cy2 = 0;
225             var px2 = cx2;
226             for (; content[cl2].numchildren > cy2; cy2++) {
227                 if (content[cl2][cy2].text.length >= px2) { break; }
228                 else { px2 -= content[cl2][cy2].text.length; }
229             }
230
231             // Call the internal select function.
232             sel.cl = cl1; sel.cy = cy1; sel.px = px1;
233             select(cl1, cy1, px1, cl2, cy2, px2);
234             moveCursorToCy(cl2, cy2, px2);
235         }
236
237         // Clear the current selection.
238         clearSelection = function() {
239             if (sel.cl == -1 || sel1.cl == -1) return;
240             moveCursorToCy(sel1.cl, sel1.cy, sel1.px);
241
242             // Clear any selected lines.
243             for (var i=sel1.cl; sel2.cl >= i; i++) { if (content[i] != null) content[i].selected = false; }
244
245             // Clear the selection values
246             sel.cl = sel.px = sel.cy = sel1.cl = sel1.cx = sel1.cy = sel2.cl = sel2.cx = sel2.cy = -1;
247             sel1.text  = sel2.text  = "";
248         }
249
250         // Delete the text currently within the selected range.
251         deleteSelection = function() {
252             if (sel1.cl == -1 || sel2.cl == -1) return;
253             deleteText(sel1.cl, calcCx(sel1.cl, sel1.cy, sel1.px), sel2.cl, calcCx(sel2.cl, sel2.cy, sel2.px));
254             clearSelection();
255         }
256
257         // External interface for moving the mouse cursor.
258         moveCursor = function(cl, cx) {
259             // Work out what subline cx is on.
260             var cy, px;
261
262             if (cl >= content.numchildren) return;
263
264             if (cx > content[cl].fulltext.length) {
265                 if (content.numchildren -1 > cl) {
266                     cl++; cx = 0; cy = 0; px = 0;
267                 } else {
268                     cx = content[cl].fulltext.length;
269                     cy = content[cl].numchildren -1;
270                     px = content[cl][cy].text.length;
271                 }
272             } else {
273                 px = cx;
274
275                 for (cy = 0; content[cl].numchildren > cy; cy++) {
276                     if (content[cl][cy].text.length >= px) break;
277                     px -= content[cl][cy].text.length;
278                 }
279             }
280
281             // Call internal move function.
282             moveCursorToPos(cl, cx, cy, px);
283         }
284
285
286         // PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////
287
288         var updateSelectionCx = function(cl, cx) {
289             if (cl >= content.numchildren) {
290                 var t = content[content.numchildren -1];
291                 updateSelection(content.numchildren -1, t.numchildren, t[t.numchildren -1].text.length);
292                 return;
293             } else if (cx > content[cl].fulltext.length) {
294                 updateSelection(cl, content[cl].numchildren -1, content[cl][content[cl].numchildren -1].text.length);
295                 return;
296             }
297
298             var cy;
299             for (cy = 0; cx > 0; cy++) {
300                 cx -= content[cl][cy].text.length;
301             }
302             cy--;
303
304             updateSelection(cl, cy, content[cl][cy].text.length + cx);
305         }
306
307         // Used in the _KeyPress trap to literally update a current selection. The new 'floating point' is passed, and
308         // the original value stored in the private property sel is used to balance with.
309         var updateSelection = function(cl, cy, px) {
310             // Very very very padentic checking. I dare you to do more checking. :-)
311             if (0 > px || 0 > cy || 0 > cl) {
312                 if (cy - 1 >= 0) { cy--; px = content[cl][cy].text.length; }
313                 else if (cl - 1 >= 0) { cl--; cy = content[cl].numchildren - 1; px = content[cl][cy].text.length; }
314                 else { cl = 0; cy = 0; px = 0; }
315             } else if (cl >= content.numchildren) {
316                 cl = content.numchildren - 1;
317                 cy = content[cl].numchildren - 1;
318                 px = content[cl][cy].text.length;
319             } else if (cy >= content[cl].numchildren || px > content[cl][cy].text.length) {
320                 if (content[cl].numchildren > cy + 1) { cy++; px = 0; }
321                 else if (content.numchildren > cl + 1) { cl++; cy = 0; px = 0; }
322                 else { cl = content.numchildren - 1; cy = content[cl].numchildren - 1; px = content[cl][cy].text.length; }
323             }
324
325             // If there is no current selection, set to current mouse position.
326             if (sel.cl == -1) { sel.cl = curs.cl; sel.cy = curs.cy; sel.px = curs.px; }
327
328             // Decide on dominant point and call internal select function.
329             if (cl > sel.cl || (cl == sel.cl and cy > sel.cy) || (cl == sel.cl and cy == sel.cy and px > sel.px)) {
330                 select(sel.cl, sel.cy, sel.px, cl, cy, px);
331             } else {
332                 select(cl, cy, px, sel.cl, sel.cy, sel.px);
333             }
334
335             // Update cursor position.
336             moveCursorToCy(cl, cy, px);
337         }
338
339         // The meat behind all select calls. This function draws the select boxes on top of the edit widget.
340         var select = function(cl1, cy1, px1, cl2, cy2, px2) {
341             if (disabled) return;
342
343             // Deselect the current full-selection lines that are outside the new selection area.
344             if (sel1.cl == cl1) {
345                 for (var i=sel1.cy+1; cy1 >= i; i++) { content[cl1][i].selected = false; }
346             } else if (cl1 > sel1.cl) {
347                 for (var i=ibex.math.max(0, sel1.cl); cl1 >= i; i++) { content[i].selected = false; }
348             }
349             if (sel2.cl == cl2) { 
350                 for (var i=sel2.cy-1; i >= cy2; i--) { content[cl2][i].selected = false; }
351             } else if (sel2.cl > cl2) {
352                 for (var i=ibex.math.max(0, sel2.cl); i >= cl2; i--) { content[i].selected = false; }
353             }
354
355             // Store point data.
356             sel1.cl = cl1; sel1.cy = cy1; sel1.px = px1;
357             sel2.cl = cl2; sel2.cy = cy2; sel2.px = px2;
358
359             // Place first select box.
360             sel1.y = content[cl1].y + content[cl1][cy1].y;
361             sel1.x = ibex.textwidth(font, content[cl1][cy1].text.substring(0, px1));
362
363             if (cl1 == cl2 and cy1 == cy2) {
364                 // Only the first select box is required.
365                 sel1.text = content[cl1][cy1].text.substring(px1, px2);
366                 sel2.text = "";
367             } else {
368                 sel1.text = content[cl1][cy1].text.substring(px1);
369                 sel2.y = content[cl2].y + content[cl2][cy2].y;
370                 sel2.x = 0;
371                 sel2.text = content[cl2][cy2].text.substring(0, px2);
372
373                 // Mark middle lines.
374                 if (cl1 == cl2) {
375                     for (var i=cy1+1; cy2 > i; i++) { content[cl1][i].selected = true; }
376                 } else {
377                     for (var i=cy1+1; content[cl1].numchildren > i; i++) { content[cl1][i].selected = true; }
378                     for (var i=cl1+1; cl2 > i; i++) { content[i].selected = true; }
379                     for (var i=0; cy2 > i; i++) { content[cl2][i].selected = true; }
380                 }
381             }
382         }
383
384         // Internal reference function. Calculates the cx position of the cursor based on cl, cy and px,
385         // and then passes it to the primary internal function moveCursorToPos() for movement.
386         var moveCursorToCy = function(cl, cy, px) { moveCursorToPos(cl, calcCx(cl, cy, px), cy, px); }
387
388         var calcCx = function(cl, cy, px) { for (cy--; cy >= 0; cy--) { px += content[cl][cy].text.length; } return px; }
389
390         // Internal function for moving the mouse cursor. px represents number of characters over in specified subline.
391         // NOTE: The mouse cursor is the closest the external functions get to affecting the internal structure of a line.
392         var moveCursorToPos = function(cl, cx, cy, px) {
393             // Check the passed values are within reasonable constaints.
394             if (cl >= content.numchildren) { cl = content.numchildren - 1; }
395             if (cy >= content[cl].numchildren) {
396                 if (content.numchildren - 1 > cl) { cl++; cy = 0; cx = calcCx(cl, cy, px); }
397                 else { cy = content[cl].numchildren -1; cx = calcCx(cl, cy, px); }
398             } else if (0 > cy) {
399                 if (cl > 0) { cl--; cy = content[cl].numchildren - 1; cx = calcCx(cl, cy, px); }
400                 else { cy = 0; cx = calcCx(cl, cy, px); }
401             }
402             if (0 > px) { px = 0; cx = calcCx(cl, cy, px); }
403             else if (px > content[cl][cy].text.length) { px = content[cl][cy].text.length; cx = calcCx(cl, cy, px); }
404
405             // Move the cursor.
406             curs.cl = cl; curs.cx = cx; curs.cy = cy; curs.px = px;
407             curs.y = content.y + content[cl].y + (lineheight * cy);
408             curs.x = content.x + ibex.textwidth(font, content[cl][cy].text.substring(0, px)) -1;
409             curs.blink = false;
410
411             // Speed Hack: As the cursor has values that match the names used by the focusarea variable, we
412             //             simply pass the curs reference as the focusarea.
413             focusarea = curs;
414
415             if (0 > curs.x) curs.x = 0;
416         }
417
418         // Returns a box ready to be a full line, armed with the current fulltext trap.
419         var newLine = function() {
420             var b = ibex.newBox();
421
422             b.color     = color;
423             b.align     = "topleft";
424             b.invisible = false;
425
426             b[0] = newSoftline();
427             b._Press1   = function() { ref_press(arguments.trapee); }
428             b._selected = function(s) { for (var i=0; b.numchildren > i; i++) { b[i].selected = s; } }
429             b._font     = function(f) { for (var i=0; b.numchildren > i; i++) { b[i].font     = f; } }
430             b._fulltext = ref_fulltext;
431             b.fulltext  = "";
432             b.orient    = "vertical";
433             b.vshrink   = true;
434
435             return b;
436         }
437
438         // Returns a box ready to be a soft line; one of the components of a line.
439         var newSoftline = function() {
440             var b = ibex.newBox();
441
442             b._selected = function(s) {
443                 arguments.trapee.color     = s ? selectcolor : color;
444                 arguments.trapee.textcolor = s ? selecttextcolor : textcolor;
445             };
446             b.minheight = lineheight;
447             b.shrink    = true;
448             b.color     = color;
449             b.textcolor = textcolor;
450             b.font      = font;
451             b.align     = "topleft";
452             b.invisible = false;
453
454             linecount++;
455
456             return b;
457         }
458
459         // Takes the given line and strips it of traps and returns it to the static stack.
460         var reapLine = function(toReap) {
461             if (content.indexof(toReap) == -1) {
462                 // Soft-line
463                 linecount--;
464             } else {
465                 // Hard-line, count all softline children.
466                 linecount -= toReap.numchildren;
467             }
468
469             toReap.thisbox = null;
470         }
471
472
473         // SUBLINE FUNCTIONS //////////////////////////////////////////////
474
475         var fulltext_nowrap = function(t) {
476             arguments.trapee[0].text = t;
477         }
478         var fulltext_linewrap = function(t) {
479             var cw = width;
480
481             if (cw == 0) return;
482
483             var i = 0;
484             if (t.length == 0) arguments.trapee[0].text = "";
485
486             for (; t.length > 0; i++) {
487                 if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
488
489                 // TODO: Switch to getpos
490                 var nl = static.getpos(t, cw, ibex.textwidth(font, t), font);
491                 arguments.trapee[i].text = t.substring(0, nl);
492                 t = t.substring(nl);
493             }
494
495             // Remove any excess lines.
496             if (i == 0) i++;
497             while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
498         }
499         var fulltext_wordwrap = function(t) {
500             var cw = width;
501
502             if (cw == 0) return;
503
504             var i = 0;
505             if (t.length == 0) arguments.trapee[0].text = "";
506
507             for (; t.length > 0; i++) {
508                 if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
509
510                 var nl = static.getpos(t, cw, ibex.textwidth(font, t), font);
511
512                 var rl = nl;
513                 if (t.length > nl) {
514                     // TODO: Clean up, make it work off a static array of possible break points.
515                     // TODO: Make it themeable as well.
516                     for (; rl > 0; rl--) {
517                         if (t.charAt(rl) == ' ' || t.charAt(rl) == '-') { rl++; break; }
518                     }
519                     if (0 >= rl || rl > nl) rl = nl;
520                 }
521
522                 arguments.trapee[i].text = t.substring(0, rl);
523                 t = t.substring(rl);
524             }
525
526             // Remove any excess lines.
527             if (i == 0) i++;
528             while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
529         }
530
531         // Reference to the current function to use for the fulltext trap.
532         var ref_fulltext = fulltext_nowrap;
533
534         // Handles selection/cursor movement from mouse events.
535         //  The passed value is a reference to the selected hard line.
536         var ref_press = function(refline) {
537             if (disabled) return;
538
539             root._Move = function() {
540                 // Update Selection.
541                 var linediff = ibex.math.floor((content.mousey - (content[sel.cl].y + content[sel.cl][sel.cy].y)) / lineheight);
542
543                 var cl = sel.cl;
544                 var cy = sel.cy;
545
546                 // End of selection comes after start.
547                 while (linediff > 0) {
548                     cy++;
549                     if (cy >= content[cl].numchildren) { cl++; cy = 0; }
550                     if (cl >= content.numchildren) { cl--; break; }
551                     linediff--;
552                 }
553
554                 // End of selection comes before start.
555                 while (0 > linediff) {
556                     cy--;
557                     if (0 > cy) { cl--; cy = content[cl].numchildren -1; }
558                     if (0 > cl) { cl=0; cy = 0; break; }
559                     linediff++;
560                 }
561
562                 var px = static.getpos(content[cl][cy].text, content[cl][cy].mousex, content[cl][cy].width, font);
563
564                 updateSelection(cl, cy, px);
565             }
566
567             root._Release1 = function() {
568                 root._Move = root._Release1 = null;
569                 // TODO: Put selection to clipboard.
570             }
571
572             // Set selection root position.
573             clearSelection();
574             sel.cl = content.indexof(refline);
575             sel.cy = ibex.math.floor(refline.mousey / lineheight);
576
577             if (sel.cy >= refline.numchildren) sel.cy = refline.numchildren -1;
578             else if (0 > sel.cy)               sel.cy = 0;
579
580             sel.px = static.getpos(refline[sel.cy].text, refline[sel.cy].mousex, refline[sel.cy].width, font);
581
582             moveCursorToCy(sel.cl, sel.cy, sel.px);
583         }
584
585
586         // HELPER FUNCTIONS  //////////////////////////////////////////////
587
588         // Nessesary for when a used clicks in the middle of a current selection.
589         _sel1 = _sel2 = function(s) {
590             if (s != null) {
591                 s._Press1 = function(t) { if (arguments.trapee.cl >= 0) content[arguments.trapee.cl].Press1 = t; };
592                 s._Release1 = function(t) { if (arguments.trapee.cl >= 0) content[arguments.trapee.cl].Release1 = t; };
593             }
594         }
595
596         _content = function(c) { if (c != null and c.numchildren == 0) c[0] = newLine(); }
597
598         _curs = function(c) {
599             if (c == null) {
600                 for (var i=0; static.cursors.length > i; i++) {
601                     if (static.cursors[i] == arguments.cascade()) { static.cursors[i] = null; break; }
602                 }
603             } else {
604                 // Add cursor to static array for 'blinking'.
605                 static.cursors[static.cursors.length] = c;
606             }
607         }
608
609         key_enter = function() {
610             if (!multiline) return;
611             content[curs.cl +1] = newLine();
612             content[curs.cl +1].fulltext = content[curs.cl].fulltext.substring(curs.cx);
613             content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx);
614             ibex.thread = function() { moveCursor(curs.cl +1, 0); }
615         }
616
617         key_back_space = function() {
618             if (curs.cx == 0) {
619                 if (curs.cl > 0) {
620                     var px = content[curs.cl -1].fulltext.length;
621                     content[curs.cl -1].fulltext = content[curs.cl -1].fulltext + content[curs.cl].fulltext;
622                     reapLine(content[curs.cl]);
623                     moveCursor(curs.cl -1, px); // Safe, moving up not down.
624                 }
625             } else {
626                 content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx -1) + content[curs.cl].fulltext.substring(curs.cx);
627                 moveCursor(curs.cl, curs.cx -1); // Safe, not moving cl.
628             }
629         }
630
631         key_delete = function() {
632             if (curs.cx == content[curs.cl].fulltext.length) {
633                 if (content.numchildren > 1) {
634                     content[curs.cl].fulltext = content[curs.cl].fulltext + content[curs.cl +1].fulltext;
635                     reapLine(content[curs.cl +1]);
636                 }
637             } else {
638                 content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx) + content[curs.cl].fulltext.substring(curs.cx +1);
639             }
640         }
641
642         // KEY HANDLER       //////////////////////////////////////////////
643         _keypress = function(k) {
644             if (k == null || !editable || disabled) return;
645
646             // Process shortcut for single character entries.
647             if (k.length == 1) {
648                 deleteSelection();
649
650                 if (k.charAt(0) == '\n') {
651                     insertText(curs.cl, curs.cx, k);
652                 } else {
653                     content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx)
654                         + k + content[curs.cl].fulltext.substring(curs.cx);
655                     ibex.thread = function() { moveCursor(curs.cl, curs.cx+1); }
656                     textChanged = true;
657                 }
658
659                 return;
660             }
661
662             k = k.substring(k.lastIndexOf('-')+1);
663
664             // Process movement commands.
665             if (k == "enter") {
666                 deleteSelection(); key_enter();
667                 textChanged = true;
668             } else if (k == "back_space") {
669                 if (sel1.cl > -1) { deleteSelection(); }
670                 else              { key_back_space(); }
671                 textChanged = true;
672             } else if (k == "delete") {
673                 if (sel1.cl > -1) { deleteSelection(); }
674                 else              { key_delete(); }
675                 textChanged = true;
676             }
677         }
678
679     </template>
680 </ibex>