reorganized file layout (part 1: moves and renames)
[org.ibex.core.git] / src / org / ibex / core / builtin / edit.ibex
diff --git a/src/org/ibex/core/builtin/edit.ibex b/src/org/ibex/core/builtin/edit.ibex
new file mode 100644 (file)
index 0000000..b767f42
--- /dev/null
@@ -0,0 +1,234 @@
+<!-- Copyright 2002 NeuronForge Pty Ltd, see COPYING file for licensing [LGPL] -->
+<ibex>
+    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
+
+    <preapply name="org.ibex.builtin.edit_lib"/>
+
+    <template multiline="false" disabled="false" editable="true" wrap="none" font="sansserif"
+              selectcolor="blue" selecttextcolor="#FFFFFF" color="#FFFFFF" orient="vertical">
+
+        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 (ibex.control) {
+                    if (key == 'a') {
+                        selectText(0, 0, content.numchildren -1, content[content.numchildren -1].fulltext.length);
+                        arguments.cascade(null);
+                    } else if (key == 'x') {
+                        ibex.clipboard = selection; deleteSelection();
+                        arguments.cascade(null);
+                    } else if (key == 'c') {
+                        ibex.clipboard = selection;
+                        arguments.cascade(null);
+                    } else if (key == 'v') {
+                        deleteSelection(); insertText(curs.cl, curs.cx, ibex.clipboard);
+                        textChangd = true;
+                        arguments.cascade(null);
+                    } else {
+                        arguments.cascade(k);
+                    }
+                } else {
+                    arguments.cascade(k);
+                }
+            } else if (key == "left") {
+                var cl, cx;
+
+                if (ibex.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 (ibex.shift) { updateSelectionCx(cl, cx); }
+                else           { clearSelection(); moveCursor(cl, cx); }
+                arguments.cascade(null);
+
+            } else if (key == "right") {
+                var cl, cx;
+
+                if (ibex.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 (ibex.shift) { updateSelectionCx(cl, cx); }
+                else           { clearSelection(); moveCursor(cl, cx); }
+                arguments.cascade(null);
+
+            } else if (key == "down") {
+                var cl, cy;
+                if (ibex.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 (ibex.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 (ibex.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 (ibex.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 (ibex.control) { cy = 0; }
+                else             { cy = curs.cy; }
+
+                if (ibex.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 (ibex.control) { cy = content[curs.cl].numchildren -1; }
+                else             { cy = curs.cy; }
+
+                if (ibex.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") {
+                ibex.println("NOT YET IMPLEMENTED: insert key"); // TODO
+                arguments.cascade(null);
+            }
+        }
+
+        <box orient="horizontal">
+            <box id="content" hpad="0" vpad="0" align="topleft" orient="vertical" shrink="true"/>
+            <box id="hspace"/>
+        </box>
+        <box id="vspace"/>
+
+        <box absolute="true" id="sel1" cl="0" cy="0" px="0" shrink="true"/>
+        <box absolute="true" id="sel2" cl="0" cy="0" px="0" shrink="true"/>
+        <box absolute="true" id="curs" cl="0" cx="0" cy="0" px="0" width="1" blink="false" color="black">
+            _blink = function(v) { invisible = (focused and !disabled) ? v : true; }
+        </box>
+    </template>
+</ibex>