A single-line or multi-line edit widget. Only handles text with one font and one color, and is capable of line or word wrapping.
TRAPS:
- multiline : boolean -- If true, edit widget will expand vertically to handle newlines.
- editable : boolean -- If true, the user can insert, paste and delete text.
- disabled : boolean -- If false, user can select text and copy to clipboard.
- wrap : string -- Either "none", "line" or "word", specifing the form of text wrap to use.
- selection : string -- Returns the currently selected text. Putting values does nothing, see FUNCTIONS, selectText().
- text : string -- Represents the complete text of this edit widget. Can be read and written to.
- limit : int -- A limit imposed on the total number of characters in the edit widget. 0 > limit means no limit.
- textcolor : color -- Color of the text.
- selectcolor : color -- Background Color of the currently selected text.
- selecttextcolor : color -- Color of the currently selected text.
- textChanged : boolean -- Set to true when the contents of the edit widget has changed.
FUNCTIONS:
If you wish to directly manipulate the contents of the edit widget consider using these functions to speed up manipulation.
All line references are based on hard carrige returns. You do not have to consider soft line wraps when making calculations.
- insertText(line, index, text) - Insert text at given char index on given line.
- deleteText(startline, startindex, endline, endindex) - Delete text in given range.
- selectText(startline, startindex, endline, endindex) - Select text in given range.
- clearSelection() - Deselect any current selection.
- deleteSelection() - Delete the text within the current selection range.
- moveCursor(line, index) - Move the cursor to the given position.
THEME NOTES:
- Most of the implementation of this widget does not need to be considered in a theme, however a particular theme may wish
to override _KeyPressed to add extra theme features (eg. Monopoly word skip on Ctrl+Left Arrow).
IMPLEMENTATION NOTES:
- In the implementation of the edit widget, there are two systems (loosely similar to model-view) used to reference text.
The first, referencing each 'hard' line (from one carrige return to the next) as a box inside $content
with the string property fulltext representing the contents of the row is used as the text model.
Text is inserted, removed and read using the $content[cl].fulltext properties.
The second is the on screen reprsentation of the text. Each $content[cl] box can have any number of children ( must be > 0)
called 'soft' lines. These softlines are created/managed at the discretion of the $content[cl]._fulltext trap. This means
different wrapping systems can be completely isolated inside the _fulltext trap. The only components of the system that
work outside of the model are cursor and selection positioning. The functions that manipulate these features have public
functions that mimic the 'model' style functions, but also have private internal functions that use cy and px variables
to reference location.
- A reference to each cursor is stored in the static area, and a global thread is used to call the _blink trap on cursors.
- Boxes are also stored in a static array when unused, however the effective value of this is questionable, and should be
properly benchmarked.
- If moving the cursor to a different cl line after changing the structure of the edit box, be sure to call the funciton from
a background thread. Otherwise, the position x,y values will be wrong.
TODO:
- keep length value up to date so limit checking works.
- insert key
content = $content;
curs = $curs;
sel1 = $sel1;
sel2 = $sel2;
// Handles key events that occur in the surrounding whitespace.
$vspace._Press1 = function(v) { ref_press(content[content.numchildren -1]); }
$hspace._Press1 = function(v) {
for (var i=0; content.numchildren > i; i++) {
if (content[i].y + content[i].height >= content.mousey) { ref_press(content[i]); return; }
}
ref_press(content[content.numchildren -1]);
}
_focused = function(f) {
curs.focused = f; arguments.cascade(f); curs.blink = false;
if (!f) { clearSelection(); }
}
_disabled = function(d) { if (d) { cursor = "default"; } else { cursor = "text"; } }
_keypress = function(k) {
if (k == null || disabled) return;
var key = k.substring(k.lastIndexOf('-')+1).toLowerCase();
if (key.length == 1) {
if (xwt.control) {
if (key == 'a') {
selectText(0, 0, content.numchildren -1, content[content.numchildren -1].fulltext.length);
arguments.cascade(null);
} else if (key == 'x') {
xwt.clipboard = selection; deleteSelection();
arguments.cascade(null);
} else if (key == 'c') {
xwt.clipboard = selection;
arguments.cascade(null);
} else if (key == 'v') {
deleteSelection(); insertText(curs.cl, curs.cx, xwt.clipboard);
textChangd = true;
arguments.cascade(null);
} else {
arguments.cascade(k);
}
} else {
arguments.cascade(k);
}
} else if (key == "left") {
var cl, cx;
if (xwt.control) {
// Skip word algorithm. Ugly to look at.
cl = curs.cl; cx = curs.cx;
while (true) {
for (cx--; cx >= 0; cx--) { if (content[cl].fulltext.charAt(cx) != ' ') break; }
if (0 > cx) { if (cl > 0) { cl--; cx = content[cl].fulltext.length; } else { break; } }
findendchar: for (cx--; cx >= 0; cx--) {
switch (content[cl].fulltext.charAt(cx)) {
case ' ': break findendchar;
case '-': break findendchar;
}
}
cx++; break;
}
} else {
// Use right boundry of selection, otherwise use the cursor.
if (sel1.cl != -1) { cl = sel1.cl; cx = calcCx(cl, sel1.cy, sel1.px) -1; }
else { cl = curs.cl; cx = curs.cx -1; }
}
if (xwt.shift) { updateSelectionCx(cl, cx); }
else { clearSelection(); moveCursor(cl, cx); }
arguments.cascade(null);
} else if (key == "right") {
var cl, cx;
if (xwt.control) {
// Skip word algorithm. Ugly to look at.
cl = curs.cl; cx = curs.cx;
while (true) {
for (cx++; content[cl].fulltext.length > cx; cx++) { if (content[cl].fulltext.charAt(cx) != ' ') break; }
if (cx > content[cl].fulltext.length) { if (content.numchildren > cl) { cl++; cx = 0; } else { break; } }
findendchar: for (cx++; content[cl].fulltext.length > cx; cx++) {
switch (content[cl].fulltext.charAt(cx)) {
case ' ': break findendchar;
case '-': break findendchar;
}
}
break;
}
} else {
// Use right boundry of selection, otherwise use the cursor.
if (sel2.cl != -1) { cl = sel2.cl; cx = calcCx(cl, sel2.cy, sel2.px) +1; }
else { cl = curs.cl; cx = curs.cx +1; }
}
if (xwt.shift) { updateSelectionCx(cl, cx); }
else { clearSelection(); moveCursor(cl, cx); }
arguments.cascade(null);
} else if (key == "down") {
var cl, cy;
if (xwt.control) { cl = content.numchildren -1; cy = content[cl].numchildren -1; }
else { if (curs.cy == content[curs.cl].numchildren -1) { cl = curs.cl +1; cy = 0; } else { cl = curs.cl; cy = curs.cy + 1; } }
if (xwt.shift) { updateSelection(cl, cy, curs.px); }
else { var px = curs.px; clearSelection(); moveCursorToCy(cl, cy, px); }
arguments.cascade(null);
} else if (key == "up") {
var cl, cy;
if (xwt.control) { cl = 0; cy = content[0].numchildren -1; }
else {
if (curs.cy == 0) { if (curs.cl == 0) { cl=0; cy=0; } else { cl = curs.cl -1; cy = content[cl].numchildren -1; } }
else { cl = curs.cl; cy = curs.cy -1; }
}
if (xwt.shift) { updateSelection(cl, cy, curs.px); }
else { var px = curs.px; clearSelection(); moveCursorToCy(cl, cy, px); }
arguments.cascade(null);
} else if (key == "home") {
var cy;
if (xwt.control) { cy = 0; }
else { cy = curs.cy; }
if (xwt.shift) { updateSelection(curs.cl, cy, 0); }
else { var cl = curs.cl; clearSelection(); moveCursorToCy(cl, cy, 0); }
arguments.cascade(null);
} else if (key == "end") {
var cy;
if (xwt.control) { cy = content[curs.cl].numchildren -1; }
else { cy = curs.cy; }
if (xwt.shift) { updateSelection(curs.cl, cy, content[curs.cl][cy].text.length); }
else { var cl = curs.cl; clearSelection(); moveCursorToCy(cl, cy, content[cl][cy].text.length); }
arguments.cascade(null);
} else if (key == "insert") {
xwt.println("NOT YET IMPLEMENTED: insert key"); // TODO
arguments.cascade(null);
}
}
_blink = function(v) { invisible = (focused and !disabled) ? v : true; }