2002/08/11 04:27:41
[org.ibex.core.git] / src / org / xwt / builtin / edit.xwt
1 <!-- Copyright 2002 Adam Megacz, see the COPYING file for licensing [LGPL] -->
2 <xwt>
3
4     A text edit box.
5
6         multiline : boolean       -- if true, lines will be broken at newline characters
7         editable  : boolean       -- if false, the text cannot be changed, although it can be copied
8
9     <redirect target="self"/>
10
11     <static>
12         var boxen = [];
13         var cr_regexp = /[\n\r]/g;
14         var nonwhitespace = /\S/;
15     </static>
16
17     <template textcolor="black" orient="vertical" vpad="2" editable="true" multiline="false" text="" cursor="text">
18
19         // Structural Stuff //////////////////////////////////////////////////////////////////////
20
21         // Cursor1 is the cursor -- when there is no selection, its width is
22         // 1, and its height is the height of one line. When the user selects
23         // a one-line region, Cursor1 is expanded to cover that region (but
24         // placed behind the text on the z-axis). When the user selects more
25         // than one line, Cursor1 covers the background of the first line of
26         // the selection, Cursor3 covers the last line, and the intermediate
27         // lines get their color set to "blue".
28
29         var curs = $curs;                      // the main cursor
30         var curs2 = $curs2;                    // the "backup" cursor for multiline selections
31         var numcursors = 2;                    // the number of non-textline children of this box
32         var master = 0;
33
34         <box text="" align="topleft"/>
35         <box id="curs" absolute="true" textcolor="white" color="blue" width="1" y="0" x="1" invisible="true"/>
36         <box id="curs2" absolute="true" textcolor="white" color="blue" width="1" y="0" x="1" invisible="true"/>
37
38         // Cursor Manipulation //////////////////////////////////////////////////////////////////////
39
40         // cx1, cy1 is the coordinates (in characters, not pixels) of the start of the selection
41         // cx2, cy2 is the coordinates (in characters, not pixels) of the end   of the selection
42         cx2 = cx1 = cy2 = cy1 = 0;
43
44         // the x-coordinate of the beginning of the selection, in characters
45         _cx1 = function(a) {
46             a = xwt.math.floor(a);
47             if (0 > a) { if (cy1 > 0) cy1--; a = thisbox[cy1].text.length; }
48             if (numchildren - numcursors > cy1 and a > thisbox[cy1].text.length) { cy1++; a = 0; }
49             curs.x = xwt.textwidth(font, thisbox[cy1].text.substring(0, a));
50             arguments.cascade(a);
51             sync();
52         }
53
54         // the x-coordinate of the end of the selection, in characters
55         _cx2 = function(a) {
56             a = xwt.math.floor(a);
57             if (0 > a) { cy2--; a = thisbox[cy2].text.length; }
58             if (numchildren - numcursors > cy2 and a > thisbox[cy2].text.length) { cy2++; a = 0; }
59             arguments.cascade(a);
60             sync();
61         }
62
63         // the y-coordinate of the beginning of the selection, in characters
64         _cy1 = function(a) {
65             a = xwt.math.floor(a);
66             if (0 > a) { a = 0; cx1 = 0; }
67             arguments.cascade(a);
68             if (cx1 > thisbox[a].text.length) cx1 = thisbox[a].text.length;
69             sync();
70         }
71
72         // the y-coordinate of the end of the selection, in characters
73         _cy2 = function(a) {
74             a = xwt.math.floor(a);
75             arguments.cascade(a);
76             while (a >= numchildren - numcursors) {
77                 var b = getbox();
78                 b.text = "";
79                 thisbox[numchildren - numcursors] = b;
80             }
81             if (cx2 > thisbox[a].text.length) cx2 = thisbox[a].text.length;
82             sync();
83         }
84
85         // the x-coordinate of the beginning of the selection, measured in _pixels_
86         __px1 = function() {
87             if (cy1 > numchildren || thisbox[cy1].text == null) return 0;
88             return xwt.textwidth(font, thisbox[cy1].text.substring(0, cx1));
89         }
90
91         // the x-coordinate of the end of the selection, measured in _pixels_
92         __px2 = function() {
93             if (cy2 > numchildren || thisbox[cy2].text == null) return 0;
94             return xwt.textwidth(font, thisbox[cy2].text.substring(0, cx2));
95         }
96
97         // on the r-th row, this will determine how many characters are to the left of a
98         // point b pixels from the left edge of the edit  Algorithm is a binary search,
99         // making educated guesses based on the average width of a character on that line.
100         getboundary = function(r, b) {
101
102             if (o >= b) return 0;
103             var s = thisbox[r].text;
104
105             var left = 0;                                 // the left boundary of the search region, in characters
106             var right = s.length;                         // the right boundary of the search region, in characters
107             var start = 0;                                // the left boundary of the search region, in pixels
108             // the right boundary of the search region, in pixels
109             var end = xwt.textwidth(font, s);
110             if (b >= end) return right;                            // short circuit if we're off the end
111
112             var avgwidth = end / right;                            // average width of one character
113
114             while(true) {
115                 var middle = left + (b - start) / avgwidth;        // make a guess at where we should look
116
117                 // extra safety guard against infinite loops
118                 if (left == right) return left;
119                 if (left >= middle) middle = left + 1;
120                 if (middle >= right) middle = left - 1;
121
122                 start = xwt.textwidth(font, s.substring(0, middle));
123                 if (start > b) {
124                     right = middle;
125                 } else if (b >= start + xwt.textwidth(font, s.charAt(middle))) {
126                     left = middle;
127                 } else {
128                     return middle;
129                 }
130             }
131         }
132
133         // returns the row containing the point yp pixels from the top of the widget
134         getrow = function(yp) { return xwt.math.min(numchildren - numcursors - 1, xwt.math.floor(yp / lineheight)); }
135
136         sync = function() {
137             curs.x = px1;
138             curs.y = thisbox[cy1].y; //lineheight * cy1 + thisbox[0].y;
139             curs.width = px2 - px1;
140             if (1 > curs.width) { curs.width = 1; curs.text = ""; }
141             curs2.invisible = true;
142
143             if (cy1 != cy2) {
144                 curs.width = width - curs.x;
145                 curs2.invisible = false;
146                 curs2.x = 0;
147                 curs2.y = thisbox[cy2].y; //lineheight * cy2;
148                 curs2.height = lineheight;
149                 curs2.width = px2;
150             }
151
152             for(var i=0; numchildren - numcursors>i; i++) {
153                 if (i>cy1 and cy2>i) { 
154                     thisbox[i].textcolor = "white";
155                     thisbox[i].color = "blue";
156                 } else {
157                     thisbox[i].textcolor = "black";
158                     thisbox[i].color = null;
159                 }
160             }
161
162             curs.text = (thisbox[cy1] == null || thisbox[cy1].text == null) ? null :
163                             thisbox[cy1].text.substring(getboundary(cy1, curs.x),
164                                                      getboundary(cy1, curs.x + curs.width));
165
166             curs2.text = (thisbox[cy2] == null || thisbox[cy2].text == null) ? null :
167                             thisbox[cy2].text.substring(getboundary(cy2, curs2.x),
168                                                      getboundary(cy2, curs2.x + curs2.width));
169         }
170
171
172
173         // Externally Visible Traps //////////////////////////////////////////////////////////
174
175         _focused = function(f) {
176             if (!f) {
177                 if (curs.width == 1) curs.invisible = true;
178             } else {
179                 curs.invisible = false;
180             }
181         }
182
183         // returns the currently-selected text
184         __selection = function() {
185             var ret = "";
186             if (cy1 == cy2) {
187                 ret += thisbox[cy1].text.substring(cx1, cx2);
188             } else {
189                 ret += thisbox[cy1].text.substring(cx1) + "\n";
190                 for(var i=cy1 + 1; cy2 > i; i++) ret += thisbox[i].text + "\n";
191                 ret += thisbox[cy2].text.substring(0, cx2);
192             }
193             return ret;
194         }
195
196         _font = function(f) {
197             curs.height = curs2.height = lineheight = f == null ? xwt.textheight() : xwt.textheight(f);
198             for(var i=0; numchildren>i; i++) {
199                 thisbox[i].font = f;
200                 if (numchildren - numcursors > i) thisbox[i].minheight = lineheight;
201             }
202         }
203         font = null;
204
205         _editable = function(e) {
206             if (e) {
207                 auto_focus = true;
208                 color = "white";
209             } else {
210                 auto_focus = false;
211                 focused = false;
212                 curs.invisible = true;
213                 curs2.invisible = true;
214             }
215         }
216
217         _multiline = function(m) {
218             if (m and m != "false") {
219                 while(numchildren > numcursors + 1) thisbox[1].thisbox = null;
220                 arguments.cascade(true);
221             } else {
222                 arguments.cascade(false);
223             }
224         }
225
226         __text = function(t) {
227             if (arguments.length == 0) {
228                 var ret = "";
229                 for(var i=0; numchildren - numcursors>i; i++) ret = ret + thisbox[i].text + "\n";
230                 return ret.substring(0, ret.length - 1); // chop off trailing CR
231             }
232             var me = this;
233             for(var i = numchildren - numcursors - 1; i >= 0; i--) {
234                 static.boxen[static.boxen.length] = me[i];
235                 me[i].thisbox = null;
236             }
237             cy1 = 0; cy2 = 0; cx1 = 0; cx2 = 0;
238             xwt.thread = function() {
239                 try { insert_text(t); } catch (e) { xwt.println(e); }
240             }
241         }
242
243         last_was_a_kill = false;               // indicates if the last "action" was a kill -- lets us know if we
244                                                // we should append newly killed text to the kill buffer or clear it
245
246
247
248         // Text Manipulation /////////////////////////////////////////////////////////////
249
250         // deletes the region between the start and end of the selection
251         var nuke_selection = function() {
252             if (cy1 != cy2 and thisbox[cy1].text != null and thisbox[cy2].text != null) {
253                 thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) + thisbox[cy2].text.substring(cx2);
254                 for(var i=cy1 + 1; cy2 >= i; i++) thisbox[cy1 + 1].thisbox = null;
255                 cy2 = cy1;
256                 cx2 = cx1;
257             } else {
258                 thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) +  thisbox[cy1].text.substring(cx2);
259                 cx2 = cx1;
260             }
261         }
262
263         var getbox = function() {
264             if (static.boxen.length == 0) static.boxen[0] = xwt.newBox("box");
265             var ret = static.boxen[static.boxen.length - 1];
266             static.boxen.length--;
267             ret.minheight = lineheight;
268             ret.vshrink = true;
269             ret.textcolor = "black";
270             ret.font = font;
271             ret.align = "topleft";
272             ret.invisible = false;
273             ret.absolute = false;
274             return ret;
275         }
276
277         // nukes the selected region and inserts arg in its place
278         var insert_text = function(arg) {
279             var mine = master = xwt.math.random();
280             if (!multiline) arg = (arg + "").replace(static.cr_regexp, "");
281             if (curs.width > 1) nuke_selection();
282             while(!(arg == null || arg == "" || arg.replace == null)) {
283                 cy2 = cy1; cx2 = cx1;
284                 // insert a new line if necessary
285                 if (arg.charAt(0) == '\n') {
286                     var n = getbox();
287                     thisbox[cy1 + 1] = n;
288                     n.text = thisbox[cy1].text.substring(cx1); 
289                     thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1); 
290                     cy1++; cy2 = cy1; cx1 = 0; cx2 = 0;
291                     arg = arg.substring(1);
292                     xwt.yield();
293                     if (master != mine) return;
294                 }
295                 var upto = arg.indexOf('\n') == -1 ? arg.length : arg.indexOf('\n');
296                 var itext = arg.substring(0, upto);
297                 thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) + itext + thisbox[cy1].text.substring(cx1);
298                 cx1 += itext.length;
299                 arg = arg.substring(upto);
300             }   
301             cy2 = cy1;
302             cx2 = cx1;
303             master = 0;
304         }
305
306             
307         // XWT Event Handlers //////////////////////////////////////////////////////////////
308
309 _Press2 = function() {
310 xwt.println("curs.x = " + curs.x);
311 xwt.println("curs.y = " + curs.y);
312 xwt.println("curs.w = " + curs.width);
313 xwt.println("curs.h = " + curs.height);
314 xwt.println("curs.i = " + curs.invisible);
315 xwt.println("curs[] = " + indexof(curs));
316 xwt.println("numchi = " + numchildren);
317 }
318
319         _Press1 = function() {
320             focused = true;
321             if (master != 0) return;
322             last_was_a_kill = false;
323
324             root._Move = function() {
325                 curs.invisible = false;
326                 var mousey_row = getrow(mousey);
327                 var pressy_row = getrow(pressy);
328
329                 if (pressy_row > mousey_row || (mousey_row == pressy_row and pressx > mousex)) {
330                     cy1 = mousey_row;
331                     cy2 = pressy_row;
332                     cx1 = getboundary(cy1, mousex);
333                     cx2 = getboundary(cy2, pressx);
334
335                 } else {
336                     var newcy2 = mousey_row;
337                     if (newcy2 > numchildren - numcursors - 1) newcy2 = numchildren - numcursors - 1;
338                     cy2 = newcy2;
339                     cy1 = pressy_row;
340                     cx2 = xwt.math.min(getboundary(cy2, mousex), thisbox[cy2].text.length);
341                     cx1 = getboundary(cy1, pressx);
342                 }
343             }
344
345             root.__Release1 = function() {
346                 root._Move = null;
347                 root._Release1 = null;
348                 if (cx1 != cx2 || cy1 != cy2) xwt.clipboard = selection;
349             }
350
351             pressx = mousex;
352             pressy = mousey;
353             cy1 = getrow(mousey);
354             cy2 = getrow(mousey);
355             cx2 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
356             cx1 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
357         }
358
359         __Press3 = function() {
360             last_was_a_kill = false;
361             cy1 = cy2 = getrow(mousey);
362             cx2 = cx1 = xwt.math.min(getboundary(cy1, mousex), thisbox[cy1].text.length);
363             if (xwt.clipboard != null and editable) insert_text(xwt.clipboard);
364         }
365
366         _DoubleClick1 = function() {
367             cy1 = cy2 = getrow(mousey);
368             cx1 = 0; cx2 = thisbox[cy1].text.length;
369             xwt.clipboard = selection;
370         }
371
372         var key_C_Q = function() {
373
374             // C-q => fill
375             var start = cy1;
376             var stop = cy2;
377
378             // if a region wasn't selected, fill this paragraph alone
379             if (cy1 == cy2) {
380                 while (start > 0 and thisbox[start].text.match(static.nonwhitespace)) start--;
381                 while (numchildren - numcursors > stop and thisbox[stop].text.match(static.nonwhitespace)) stop++;
382             }
383
384             // pull out the text-to-be filled
385             var filltext = "";
386             for(var i=start; stop >= i; i++) {
387                 filltext += thisbox[start].text + "\n";
388                 if (numchildren > numcursors) {
389                     static.boxen[static.boxen.length] = thisbox[start];
390                     thisbox[start].thisbox = null;
391                 } else {
392                     thisbox[start].text = "";
393                 }
394             }
395
396             xwt.println("prefill text:\n" + filltext);            
397
398             filltext = filltext.replace(/^\\s+/);
399
400             var quoted = false;
401             if (filltext.substring(0, 2) == "> ") {
402                 filltext = filltext.substring(2).replace(/\n> /g, "\n");
403                 quoted = true;
404             }
405
406             filltext = filltext.replace(/\s+/g, " ");
407
408             var newfilltext = "";
409
410             while (filltext.length > 0) {
411                 var tmp = filltext.substring(0, 78);
412                 var tmp2;
413                 if (tmp.lastIndexOf(' ') != -1) {
414                     filltext = tmp.substring(tmp.lastIndexOf(' ') + 1) + filltext.substring(78);
415                     tmp2 = tmp.substring(0, tmp.lastIndexOf(' '));
416                 } else {
417                     tmp2 = filltext;
418                     filltext = "";
419                 }
420                 newfilltext += (quoted ? "> " : "") + tmp2 + "\n";  
421             }
422
423             cy1 = start; cy2 = start;
424             cx1 = 0; cx2 = 0;
425
426             xwt.println("newfilltext:\n" + newfilltext);
427
428             insert_text("\n" + newfilltext + "\n");
429          }
430
431         var key_C_W = function() { if (xwt.clipboard != null) xwt.clipboard = selection;
432                                    if (editable) nuke_selection();
433                                  }
434         var key_C_K = function() { if (!editable) return;
435                                    if (1 >= curs.width and cx1 == thisbox[cy1].text.length) cx2++;
436                                    else if (1 >= curs.width) cx2 = thisbox[cy2].text.length;
437                                    if (!last_was_a_kill and xwt.clipboard != null) xwt.clipboard = "";
438                                    if (xwt.clipboard != null) xwt.clipboard += selection;
439                                    else xwt.clipboard = selection;
440                                    nuke_selection();
441                                  }
442
443         _KeyPressed = function(key) {
444             if (master != 0) return;
445             if (!focused) return;
446             var kill = false;
447             if (key == "C-a") { cx1 = 0; cx2 = 0; }
448             else if (key == "C-e") { cx1 = thisbox[cy2].text.length; cx2 = cx1; cy2 = cy1; }
449             else if (key == "C-d" || key == "delete") { if (!editable) return; if (1 >= curs.width) cx2++; nuke_selection(); }
450             else if (key == "C-y") { if (xwt.clipboard != null and editable) insert_text(xwt.clipboard); }
451             else if (key == "C-k") key_C_K();
452             else if (key == "C-w") key_C_W();
453             else if (key == "C-h" || key == "back_space") {
454                 if (!editable) return;
455                 if (cx1 == 0 and cx2 == 0 and cy1 == 0 and cy2 == 0) return;
456                 if (1 >= curs.width) cx1--;
457                 nuke_selection();
458             }
459             else if (key == "C-b" || key == "left") { cx1--; cy2 = cy1; cx2 = cx1; }
460             else if (key == "C-p" || key == "up") { cy1--; cy2 = cy1; cx2 = cx1; }
461             else if (key == "C-f" || key == "right") { cx2++; cy1 = cy2; cx1 = cx2; }
462             else if (key == "C-m" || key == "C-o" || key == "enter" || key == "C-j") { if (editable) insert_text("\n"); }
463             else if (key == "C-n" || key == "down") { cy2++; cy1 = cy2; cx1 = cx2; }
464             else if (key == "C-q") key_C_Q();
465             else if (key.length == 1) {
466                 if (!editable) return;
467                 if (curs.width > 1) nuke_selection();
468                 thisbox[cy1].text = thisbox[cy1].text.substring(0, cx1) +
469                                  key +
470                                  thisbox[cy1].text.substring(cx1);
471                 cx2 = cx1 + 1; cx1 = cx2;
472             }
473         
474             last_was_a_kill = key == "C-k";
475         }
476     
477     </template>
478 </xwt>
479