1 <!-- Copyright 2002 NeuronForge Pty Ltd, see COPYING file for licensing [LGPL] -->
5 var worddivider = [' ', '-'];
7 xwt.thread = function() {
8 while (true) { xwt.sleep(1000); for (var i=0; cursors.length > i; i++) { cursors[i].blink = !cursors[i].blink; } }
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;
21 // Inital guess based on average character width.
22 var guessch = xwt.math.min(t.length, xwt.math.floor(pxpos / (pxlen / t.length)));
23 var guesspx = xwt.textwidth(tfont, t.substring(0, guessch));
25 if (guesspx > pxpos) {
26 while (guesspx > pxpos) {
27 // Textwidth of individual character must account for font kerning.
28 guesspx -= xwt.textwidth(tfont, t.substring(guessch -1, guessch +1)) - xwt.textwidth(tfont, t.charAt(guessch));
31 } else if (pxpos > guesspx) {
32 while (pxpos > guesspx) {
34 if (guessch >= t.length) break;
35 guesspx += xwt.textwidth(tfont, t.substring(guessch -1, guessch+1)) - xwt.textwidth(tfont, t.charAt(guessch));
37 guessch--; // Round down.
46 _multiline = function(v) {
48 while (content.numchildren > 1) { reapLine(content[content.numchildren -1]); }
53 _disabled = function(v) {
57 _editable = function(v) {
61 // Used on _SizeChange if wrap needs to know.
62 var resize = function() {
63 xwt.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; }
69 if (multiline and v == "line") {
70 content.vshrink = true;
71 content.hshrink = false;
72 content.maxwidth = xwt.maxdim; // Must reset maxwidth after shrink = true.
73 ref_fulltext = fulltext_linewrap;
76 } else if (multiline and v == "word") {
77 content.vshrink = true;
78 content.hshrink = false;
79 content.maxwidth = xwt.maxdim;
80 ref_fulltext = fulltext_wordwrap;
84 content.shrink = true;
85 ref_fulltext = fulltext_nowrap;
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;
96 _selectcolor = function(v) { sel1.color = sel2.color = v; }
97 _selecttextcolor = function(v) { sel1.textcolor = sel2.textcolor = v; }
100 lineheight = xwt.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;
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; }
113 deleteText(0, 0, content.numchildren - 1, content[content.numchildren - 1].fulltext.length);
117 __selection = function(t) {
118 if (arguments.length == 0) {
119 if (sel1.cl == -1) return "";
123 if (sel1.cl == sel2.cl) {
124 for (var i=sel1.cy+1; sel2.cy > i; i++) { s += content[sel1.cl][i].text; }
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; }
129 for (var i=0; sel2.cy > i; i++) { s += content[sel2.cl][i].text; }
137 insertText(curs.cl, curs.cx, t);
141 // PRIVATE VARIABLES //////////////////////////////////////////////////////////////////////////////////////////
143 // Stores the inital point of the current selection.
144 var sel = { cl : 0, cy : 0, px : 0 };
146 // The pixel height of the current font.
147 var _lineheight = function(l) { curs.height = sel1.height = sel2.height = l; }
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; }
155 // Total number of characters stored in this text field.
159 // PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////
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;
168 if (limit > 0 and length + t.length > limit) {
169 xwt.println("WARNING: Limit applied on inserted text.");
170 t = t.substring(0, limit - length);
173 // Make sure there are enough lines before hand.
174 for (var i=content.numchildren; cl >= i; i++) { content[i] = newLine(); }
176 // Parse the carridge returns out of t.
177 var newT = t.split("\n");
179 if (newT.length == 0) {
181 } else if (newT.length == 1) {
182 content[cl].fulltext = content[cl].fulltext.substring(0, cx) + t + content[cl].fulltext.substring(cx);
185 moveCursor(cl, cx+t.length);
188 // Add extra lines required by passed text.
189 for (var i = newT.length - 1; i > 0; i--) { content[cl+1] = newLine(); }
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;
197 moveCursor(cl + newT.length - 1, newT[newT.length -1].length);
199 xwt.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);
202 moveCursor(0, cx + newT[0].length);
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]); }
213 // Select the text within the given range.
214 selectText = function(cl1, cx1, cl2, cx2) {
218 for (; content[cl1].numchildren > cy1; cy1++) {
219 if (content[cl1][cy1].text.length > px1) { break; }
220 else { px1 -= content[cl1][cy1].text.length; }
226 for (; content[cl2].numchildren > cy2; cy2++) {
227 if (content[cl2][cy2].text.length >= px2) { break; }
228 else { px2 -= content[cl2][cy2].text.length; }
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);
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);
242 // Clear any selected lines.
243 for (var i=sel1.cl; sel2.cl >= i; i++) { if (content[i] != null) content[i].selected = false; }
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 = "";
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));
257 // External interface for moving the mouse cursor.
258 moveCursor = function(cl, cx) {
259 // Work out what subline cx is on.
262 if (cl >= content.numchildren) return;
264 if (cx > content[cl].fulltext.length) {
265 if (content.numchildren -1 > cl) {
266 cl++; cx = 0; cy = 0; px = 0;
268 cx = content[cl].fulltext.length;
269 cy = content[cl].numchildren -1;
270 px = content[cl][cy].text.length;
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;
281 // Call internal move function.
282 moveCursorToPos(cl, cx, cy, px);
286 // PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////
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);
293 } else if (cx > content[cl].fulltext.length) {
294 updateSelection(cl, content[cl].numchildren -1, content[cl][content[cl].numchildren -1].text.length);
299 for (cy = 0; cx > 0; cy++) {
300 cx -= content[cl][cy].text.length;
304 updateSelection(cl, cy, content[cl][cy].text.length + cx);
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; }
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; }
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);
332 select(cl, cy, px, sel.cl, sel.cy, sel.px);
335 // Update cursor position.
336 moveCursorToCy(cl, cy, px);
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;
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=xwt.math.max(0, sel1.cl); cl1 >= i; i++) { content[i].selected = false; }
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=xwt.math.max(0, sel2.cl); i >= cl2; i--) { content[i].selected = false; }
356 sel1.cl = cl1; sel1.cy = cy1; sel1.px = px1;
357 sel2.cl = cl2; sel2.cy = cy2; sel2.px = px2;
359 // Place first select box.
360 sel1.y = content[cl1].y + content[cl1][cy1].y;
361 sel1.x = xwt.textwidth(font, content[cl1][cy1].text.substring(0, px1));
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);
368 sel1.text = content[cl1][cy1].text.substring(px1);
369 sel2.y = content[cl2].y + content[cl2][cy2].y;
371 sel2.text = content[cl2][cy2].text.substring(0, px2);
373 // Mark middle lines.
375 for (var i=cy1+1; cy2 > i; i++) { content[cl1][i].selected = true; }
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; }
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); }
388 var calcCx = function(cl, cy, px) { for (cy--; cy >= 0; cy--) { px += content[cl][cy].text.length; } return px; }
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); }
399 if (cl > 0) { cl--; cy = content[cl].numchildren - 1; cx = calcCx(cl, cy, px); }
400 else { cy = 0; cx = calcCx(cl, cy, px); }
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); }
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 + xwt.textwidth(font, content[cl][cy].text.substring(0, px)) -1;
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.
415 if (0 > curs.x) curs.x = 0;
418 // Returns a box ready to be a full line, armed with the current fulltext trap.
419 var newLine = function() {
420 var b = xwt.newBox();
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;
432 b.orient = "vertical";
438 // Returns a box ready to be a soft line; one of the components of a line.
439 var newSoftline = function() {
440 var b = xwt.newBox();
442 b._selected = function(s) {
443 arguments.trapee.color = s ? selectcolor : color;
444 arguments.trapee.textcolor = s ? selecttextcolor : textcolor;
446 b.minheight = lineheight;
449 b.textcolor = textcolor;
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) {
465 // Hard-line, count all softline children.
466 linecount -= toReap.numchildren;
469 toReap.thisbox = null;
473 // SUBLINE FUNCTIONS //////////////////////////////////////////////
475 var fulltext_nowrap = function(t) {
476 arguments.trapee[0].text = t;
478 var fulltext_linewrap = function(t) {
484 if (t.length == 0) arguments.trapee[0].text = "";
486 for (; t.length > 0; i++) {
487 if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
489 // TODO: Switch to getpos
490 var nl = static.getpos(t, cw, xwt.textwidth(font, t), font);
491 arguments.trapee[i].text = t.substring(0, nl);
495 // Remove any excess lines.
497 while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
499 var fulltext_wordwrap = function(t) {
505 if (t.length == 0) arguments.trapee[0].text = "";
507 for (; t.length > 0; i++) {
508 if (i == arguments.trapee.numchildren) { arguments.trapee[i] = newSoftline(); }
510 var nl = static.getpos(t, cw, xwt.textwidth(font, t), font);
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; }
519 if (0 >= rl || rl > nl) rl = nl;
522 arguments.trapee[i].text = t.substring(0, rl);
526 // Remove any excess lines.
528 while (arguments.trapee.numchildren > i) { reapLine(arguments.trapee[i]); }
531 // Reference to the current function to use for the fulltext trap.
532 var ref_fulltext = fulltext_nowrap;
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;
539 root._Move = function() {
541 var linediff = xwt.math.floor((content.mousey - (content[sel.cl].y + content[sel.cl][sel.cy].y)) / lineheight);
546 // End of selection comes after start.
547 while (linediff > 0) {
549 if (cy >= content[cl].numchildren) { cl++; cy = 0; }
550 if (cl >= content.numchildren) { cl--; break; }
554 // End of selection comes before start.
555 while (0 > linediff) {
557 if (0 > cy) { cl--; cy = content[cl].numchildren -1; }
558 if (0 > cl) { cl=0; cy = 0; break; }
562 var px = static.getpos(content[cl][cy].text, content[cl][cy].mousex, content[cl][cy].width, font);
564 updateSelection(cl, cy, px);
567 root._Release1 = function() {
568 root._Move = root._Release1 = null;
569 // TODO: Put selection to clipboard.
572 // Set selection root position.
574 sel.cl = content.indexof(refline);
575 sel.cy = xwt.math.floor(refline.mousey / lineheight);
577 if (sel.cy >= refline.numchildren) sel.cy = refline.numchildren -1;
578 else if (0 > sel.cy) sel.cy = 0;
580 sel.px = static.getpos(refline[sel.cy].text, refline[sel.cy].mousex, refline[sel.cy].width, font);
582 moveCursorToCy(sel.cl, sel.cy, sel.px);
586 // HELPER FUNCTIONS //////////////////////////////////////////////
588 // Nessesary for when a used clicks in the middle of a current selection.
589 _sel1 = _sel2 = function(s) {
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; };
596 _content = function(c) { if (c != null and c.numchildren == 0) c[0] = newLine(); }
598 _curs = function(c) {
600 for (var i=0; static.cursors.length > i; i++) {
601 if (static.cursors[i] == arguments.cascade()) { static.cursors[i] = null; break; }
604 // Add cursor to static array for 'blinking'.
605 static.cursors[static.cursors.length] = c;
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 xwt.thread = function() { moveCursor(curs.cl +1, 0); }
617 key_back_space = function() {
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.
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.
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]);
638 content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx) + content[curs.cl].fulltext.substring(curs.cx +1);
642 // KEY HANDLER //////////////////////////////////////////////
643 _keypress = function(k) {
644 if (k == null || !editable || disabled) return;
646 // Process shortcut for single character entries.
650 if (k.charAt(0) == '\n') {
651 insertText(curs.cl, curs.cx, k);
653 content[curs.cl].fulltext = content[curs.cl].fulltext.substring(0, curs.cx)
654 + k + content[curs.cl].fulltext.substring(curs.cx);
655 xwt.thread = function() { moveCursor(curs.cl, curs.cx+1); }
662 k = k.substring(k.lastIndexOf('-')+1);
664 // Process movement commands.
666 deleteSelection(); key_enter();
668 } else if (k == "back_space") {
669 if (sel1.cl > -1) { deleteSelection(); }
670 else { key_back_space(); }
672 } else if (k == "delete") {
673 if (sel1.cl > -1) { deleteSelection(); }
674 else { key_delete(); }