Răsfoiți Sursa

Modules Dir and StrList added; Begin dev of new file dialog

Arthur Yefimov 4 ani în urmă
părinte
comite
cbf1c64d22
11 a modificat fișierele cu 1282 adăugiri și 900 ștergeri
  1. 26 0
      Programs/DirTest.Mod
  2. 2 2
      data/bin/compile.bat
  3. 2 2
      data/bin/compile.sh
  4. 16 9
      data/bin/link_console.bat
  5. 1 0
      data/bin/link_graph.bat
  6. 94 0
      src/Dir.Mod
  7. 891 858
      src/Editor.Mod
  8. 162 12
      src/OV.Mod
  9. 63 0
      src/StrList.Mod
  10. 1 3
      src/Terminal.Mod
  11. 24 14
      src/make.bat

+ 26 - 0
Programs/DirTest.Mod

@@ -0,0 +1,26 @@
+MODULE DirTest;
+IMPORT Dir, Out;
+
+PROCEDURE Do;
+VAR r: Dir.Rec;
+  name: ARRAY 512 OF CHAR;
+BEGIN
+  Dir.First(r, 'C:\');
+  IF r.res = 0 THEN (* Есть такой каталог *)
+    WHILE ~r.eod DO (* Выводим только каталоги *)
+      IF r.isDir THEN Out.Char('['); Out.String(r.name); Out.Char(']'); Out.Ln END;
+      Dir.Next(r)
+    END;
+    Dir.Rewind(r); (* Возвращаемся и *)
+    WHILE ~r.eod DO (* выводим только файлы *)
+      IF ~r.isDir THEN Out.String(r.name); Out.Ln END;
+      Dir.Next(r)
+    END;
+    Dir.Close(r)
+  ELSE Out.String('Could not find directory.'); Out.Ln
+  END
+END Do;
+
+BEGIN
+  Do
+END DirTest.

+ 2 - 2
data/bin/compile.bat

@@ -8,8 +8,8 @@ CD bin >nul 2>&1
 SET CURDIR=%~dp0
 SET OFRDIR=%CURDIR%OfrontPlus\Target\Win64
 SET PATH=%OFRDIR%;%CURDIR%mingw32\bin;%PATH%
-SET OBERON=.;%OFRDIR%\Lib\Sym
-SET OFR=ofront+ -88 -C -s
+SET OBERON=.;%CURDIR%..\..\src;%OFRDIR%\Lib\Sym
+SET OFR=ofront+ -s -88 -7
 
 ECHO ON
 %OFR% %2 ..\Programs\%1

+ 2 - 2
data/bin/compile.sh

@@ -8,8 +8,8 @@ cd bin
 
 OFRDIR="../data/bin/OfrontPlus/Target/Linux_amd64"
 PATH="$OFRDIR:$PATH"
-export OBERON=".:$OFRDIR/Lib/Sym"
-OFR="ofront+ -88 -C -s"
+export OBERON=".:../src:$OFRDIR/Lib/Sym"
+OFR="ofront+ -88 -7 -s"
 
 
 $OFR $2 ../Programs/$1

+ 16 - 9
data/bin/link_console.bat

@@ -1,12 +1,15 @@
 @ECHO OFF
 REM This script is run by Free Oberon on Windows. Current directory of the
 REM script will be where FreeOberon.exe is located. This particular script
-REM is for console programs.
+REM is for graphical programs.
+
 CD bin >nul 2>&1
 @DEL /s %~n1.exe >nul 2>&1
 SET CURDIR=%~dp0
 SET ONAME=%~n1
-SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
+SET OFRDIR=%CURDIR%OfrontPlus\Target\Win64
+SET PATH=%OFRDIR%;%CURDIR%mingw64\bin;%PATH%
+SET CC=gcc
 
 REM Put all arguments starting from 2nd to ARGS.
 SHIFT
@@ -18,13 +21,17 @@ SET ARGS=%ARGS% %1
 SHIFT
 GOTO START
 :FINISH
-REM END Put all... ARGS.
-
+REM END Put all ARGS.
 ECHO ON
-gcc -fPIC -g -I "%CURDIR%voc\C\include"^
- -o %ONAME%.exe %ONAME%.o^
- %ARGS%^
- "%CURDIR%voc/lib/Int.o"^
- -L"%CURDIR%voc\lib" -lvoc-OC -lmingw32
+
+%CC% -g3 -O0 -fno-exceptions ^
+  -I %CURDIR%..\..\src ^
+  -I %OFRDIR%\..\..\Mod\Lib ^
+  -I %OFRDIR%\Lib\Obj ^
+  %ONAME%.c -o %ONAME%.exe ^
+  %ARGS%^
+  %CURDIR%libFreeOberon.a ^
+  %OFRDIR%\Lib\Ofront.a
 @SET RETCODE=%ERRORLEVEL%
+
 @EXIT /b %RETCODE%

+ 1 - 0
data/bin/link_graph.bat

@@ -27,6 +27,7 @@ REM END Put all ARGS.
 ECHO ON
 
 %CC% -g3 -O0 -fno-exceptions ^
+  -I %CURDIR%..\..\src ^
   -I %OFRDIR%\..\..\Mod\Lib ^
   -I %OFRDIR%\Lib\Obj ^
   %ONAME%.c -o %ONAME%.exe ^

+ 94 - 0
src/Dir.Mod

@@ -0,0 +1,94 @@
+MODULE Dir; (*This is Oberon-07 style file*)
+IMPORT C := arC, arCString, SYSTEM, Platform, Out;
+
+TYPE
+  DIR = SYSTEM.ADRINT;
+  DIRENT = SYSTEM.ADRINT;
+
+  Rec* = RECORD
+    eod*: BOOLEAN; (* End of directory *)
+    isDir*: BOOLEAN; (* TRUE if current entry is a directory *)
+    res*: INTEGER; (* 0 means no error *)
+    name*: ARRAY 512 OF CHAR;
+    path: ARRAY 1024 OF CHAR;
+    pathlen: INTEGER;
+    dir: DIR
+  END;
+
+PROCEDURE -AAincludeDirent "#include <dirent.h>";
+
+PROCEDURE -opendir(name: ARRAY OF CHAR): DIR "(SYSTEM_ADRINT) opendir((const char *)name)";
+PROCEDURE -closedir(dir: DIR) "closedir((DIR*) dir)";
+PROCEDURE -readdir(dir: DIR): DIRENT "(SYSTEM_ADRINT) readdir((DIR*) dir)";
+PROCEDURE -rewinddir(dir: DIR) "rewinddir((DIR*) dir)";
+PROCEDURE -getdirname(ent: DIRENT): C.string "(CHAR *)(((struct dirent *) ent) -> d_name)";
+PROCEDURE -getdirtype(ent: DIRENT): C.int "(int)((struct dirent *) ent) -> d_type)";
+PROCEDURE -typeDir "DT_DIR";
+PROCEDURE -typeReg "DT_REG";
+PROCEDURE -typeLink "DT_LNK";
+
+PROCEDURE Close*(VAR r: Rec);
+(* Stop enumerating files. MUST BE CALLED to avoid leaking C directory streams *)
+BEGIN
+  IF r.dir # 0 THEN closedir(r.dir); r.dir := 0 END
+  (*!TODO add this to finalization *)
+END Close;
+
+PROCEDURE Next*(VAR r: Rec);
+VAR ent: DIRENT;
+  i, j: INTEGER;
+BEGIN
+  IF r.dir # 0 THEN
+    r.res := 0;
+    ent := readdir(r.dir);
+    IF ent # 0 THEN
+      arCString.CopyToArray(getdirname(ent), r.name);
+
+      (* To set the value of r.isDir, we append '/' and name to path, pass
+         it to DirExists and then restore the original value of path. *)
+
+      i := 0; j := r.pathlen;
+      IF r.path[j] # Platform.PathDelimiter THEN
+        r.path[j] := Platform.PathDelimiter; INC(j)
+      END;
+      WHILE (j < LEN(r.path) - 1) & (r.name[i] # 0X) DO
+        r.path[j] := r.name[i]; INC(i); INC(j)
+      END;
+      IF j < LEN(r.path) - 1 THEN
+        r.path[j] := 0X;
+        r.isDir := Platform.DirExists(r.path)
+      ELSE r.isDir := FALSE; r.res := 3; r.eod := TRUE
+      END;
+      r.path[r.pathlen] := 0X
+
+    ELSE r.eod := TRUE
+    END
+  ELSE r.res := 4; r.eod := TRUE
+  END
+END Next;
+
+PROCEDURE First*(VAR r: Rec; path: ARRAY OF CHAR);
+VAR i: INTEGER;
+BEGIN
+  r.isDir := FALSE; r.name[0] := 0X;
+  i := 0; WHILE path[i] # 0X DO INC(i) END;
+  IF i < LEN(r.path) - 13 THEN
+    r.pathlen := i; r.dir := opendir(path);
+    IF r.dir # 0 THEN r.eod := FALSE; r.path := path; Next(r)
+    ELSE r.res := 1; r.eod := TRUE; r.path[0] := 0X
+    END
+  ELSE r.res := 2; r.eod := TRUE; r.path[0] := 0X
+  END
+END First;
+
+PROCEDURE Rewind*(VAR r: Rec);
+BEGIN
+  IF r.dir # 0 THEN rewinddir(r.dir); r.res := 0; r.eod := FALSE; Next(r)
+  ELSE First(r, r.path)
+  END
+END Rewind;
+
+PROCEDURE IsDir*(name: ARRAY OF CHAR): BOOLEAN;
+RETURN Platform.DirExists(name) END IsDir;
+
+END Dir.

+ 891 - 858
src/Editor.Mod

@@ -1,858 +1,891 @@
-MODULE Editor;
-(* Copyright 2017-2021 Arthur Yefimov
-
-This file is part of Free Oberon.
-
-Free Oberon is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Free Oberon is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
-*)
-IMPORT OV, T := Terminal, G := Graph, Text := EditorText, Strings, Out;
-CONST
-  (* Direction of Selection *)
-  dirLeft  = 0;
-  dirRight = 1;
-  dirUp    = 2;
-  dirDown  = 3;
-
-  (* Character Classes *)
-  charOther       = 0;
-  charAlpha       = 1;
-  charDigit       = 2;
-  charMinusPlus   = 3;
-  charQuote       = 4;
-  charOpenBracket = 5;
-
-  (* Token Classes *)
-  tokenOther   = 0;
-  tokenKeyword = 1;
-  tokenNumber  = 2;
-  tokenString  = 3;
-  tokenComment = 4;
-
-  (* FileDialog type *)
-  open* = 1;
-  save* = 2;
-
-TYPE
-  CHAR = SHORTCHAR;
-
-  FileDialog* = POINTER TO FileDialogDesc;
-  FileDialogDesc* = RECORD(OV.WindowDesc)
-    type*: INTEGER; (* open or save *)
-    edtFilename*: OV.Edit;
-    btnOk*, btnCancel*: OV.Button;
-    onFileOk*: PROCEDURE (c: OV.Control; filename: ARRAY OF CHAR)
-  END;
-
-  Editor* = POINTER TO EditorDesc;
-  EditorDesc* = RECORD(OV.WindowDesc)
-    text*: Text.Text;
-    filename*: ARRAY 1000 OF CHAR
-  END;
-
-VAR
-  clipboard: ARRAY 16000 OF CHAR;
-  editorMethod-: OV.ControlMethod;
-  fileDialogMethod-: OV.ControlMethod;
-
-(* FileDialog *)
-
-PROCEDURE FileDialogOkClick*(c: OV.Control);
-VAR w: FileDialog;
-BEGIN OV.CloseCurWindow(c);
-  w := c.parent(FileDialog);
-  IF w.onFileOk # NIL THEN
-    w.onFileOk(c, w.edtFilename.caption)
-  END
-END FileDialogOkClick;
-
-PROCEDURE InitFileDialog*(c: FileDialog; type: INTEGER);
-BEGIN OV.InitWindow(c); c.do := fileDialogMethod; c.type := type;
-  IF type = open THEN c.caption := 'Open a File'
-  ELSE c.caption := 'Save File As'
-  END;
-  c.modal := TRUE; c.w := 49; c.h := 19; OV.CenterWindow(c);
-  (* File Name Edit *)
-  c.edtFilename := OV.NewEdit();
-  c.edtFilename.do.resize(c.edtFilename, 3, 3, 28, 1);
-  OV.Add(c, c.edtFilename);
-  (* Open/Save (Ok) button *)
-  IF type = open THEN c.btnOk := OV.NewButton('Open')
-  ELSE c.btnOk := OV.NewButton('Save')
-  END;
-  c.btnOk.do.resize(c.btnOk, 35, 3, 10, 1);
-  c.btnOk.onClick := FileDialogOkClick;
-  OV.Add(c, c.btnOk);
-  (* Cancel button *)
-  c.btnCancel := OV.NewButton('Cancel');
-  c.btnCancel.onClick := OV.CloseCurWindow;
-  c.btnCancel.do.resize(c.btnCancel, 35, 6, 10, 1);
-  OV.Add(c, c.btnCancel)
-END InitFileDialog;
-
-PROCEDURE NewFileDialog*(type: INTEGER): FileDialog;
-VAR c: FileDialog;
-BEGIN NEW(c); InitFileDialog(c, type); RETURN c
-END NewFileDialog;
-
-(* FileDialog Method *)
-
-PROCEDURE FileDialogDraw*(c: OV.Control; x, y: INTEGER);
-BEGIN OV.WindowDraw(c, x, y); INC(x, c.x); INC(y, c.y);
-  T.PutString(x + 3, y + 2, 'Name', 14, 7, 0)
-END FileDialogDraw;
-
-PROCEDURE FileDialogGetFocus(c: OV.Control);
-BEGIN OV.WindowGetFocus(c)
-END FileDialogGetFocus;
-
-PROCEDURE FileDialogKeyDown*(c: OV.Control; key: G.Key);
-BEGIN
-  CASE key.code OF
-    G.kEsc: OV.CloseCurWindow(c)
-  | G.kEnter: c(FileDialog).btnOk.do.click(c(FileDialog).btnOk)
-  ELSE
-  END
-END FileDialogKeyDown;
-
-PROCEDURE InitFileDialogMethod*(m: OV.ControlMethod);
-BEGIN OV.InitWindowMethod(m);
-  m.draw := FileDialogDraw;
-  m.getFocus := FileDialogGetFocus;
-  m.keyDown := FileDialogKeyDown
-END InitFileDialogMethod;
-
-(* Editor *)
-
-PROCEDURE IsEmpty*(e: Editor): BOOLEAN;
-BEGIN
-  RETURN (e.filename[0] = 0X) & (e.text.first = e.text.last) &
-         (e.text.first.len = 0)
-END IsEmpty;
-
-PROCEDURE IntToStr*(n: INTEGER; VAR s: ARRAY OF CHAR); (* !TODO move out *)
-(* LEN(s) > 1 *)
-VAR i, j: INTEGER; tmp: CHAR; neg: BOOLEAN;
-BEGIN
-  IF n = 0 THEN
-    s[0] := '0'; i := 1
-  ELSE i := 0; neg := n < 0; 
-    IF neg THEN n := -n END;
-    WHILE (n > 0) & (i < LEN(s) - 1) DO
-      s[i] := CHR(ORD('0') + n MOD 10);
-      n := n DIV 10; INC(i)
-    END;
-    IF neg & (i < LEN(s) - 1) THEN s[i] := '-'; INC(i) END;
-  END;
-  s[i] := 0X; j := 0; DEC(i);
-  WHILE j < i DO
-    tmp := s[j]; s[j] := s[i]; s[i] := tmp;
-    INC(j); DEC(i)
-  END
-END IntToStr;
-
-PROCEDURE StringsFindPrev* (pattern, stringToSearch: ARRAY OF CHAR; startPos: INTEGER;
-                     VAR patternFound: BOOLEAN; VAR posOfPattern: INTEGER); (* !TODO move out *)
-VAR patternPos, stringLength, patternLength: INTEGER;
-BEGIN
-  (* correct `startPos' if it is larger than the possible searching range *)
-  stringLength := Strings.Length (stringToSearch);
-  patternLength := Strings.Length (pattern);
-  IF (startPos > stringLength-patternLength) THEN
-    startPos := stringLength-patternLength
-  END;
-  
-  IF (startPos >= 0) THEN
-    patternPos := 0;
-    LOOP
-      IF (pattern[patternPos] = 0X) THEN     
-        (* reached end of pattern *)
-        patternFound := TRUE;
-        posOfPattern := startPos-patternPos;
-        EXIT
-      ELSIF (stringToSearch[startPos] # pattern[patternPos]) THEN
-        (* characters differ: reset indices and restart *)
-        IF (startPos > patternPos) THEN
-          startPos := startPos-patternPos-1;
-          patternPos := 0
-        ELSE
-          (* reached beginning of `stringToSearch' without finding a match *)
-          patternFound := FALSE;
-          EXIT
-        END
-      ELSE  (* characters identic, compare next one *)
-        INC (startPos); 
-        INC (patternPos)
-      END
-    END
-  ELSE
-    patternFound := FALSE
-  END
-END StringsFindPrev;
-
-PROCEDURE GetCharClass(ch: CHAR): INTEGER;
-VAR class: INTEGER;
-BEGIN
-  CASE ch OF
-    'a'..'z', 'A'..'Z': class := charAlpha
-  | '0'..'9': class := charDigit
-  | '-', '+': class := charMinusPlus
-  | '"', "'": class := charQuote
-  | '(': class := charOpenBracket
-  ELSE class := charOther END;
-  RETURN class
-END GetCharClass;
-
-PROCEDURE IsHexDigit(ch: CHAR): BOOLEAN;
-VAR result: BOOLEAN;
-BEGIN
-  CASE ch OF
-    'A'..'F': result := TRUE
-  ELSE result := FALSE END;
-  RETURN result
-END IsHexDigit;
-
-PROCEDURE IsKeywordInString(s: ARRAY OF CHAR; x, len: INTEGER): BOOLEAN;
-VAR part: ARRAY 32 OF CHAR; result: BOOLEAN;
-  PROCEDURE KW(keyword: ARRAY OF CHAR): BOOLEAN;
-  BEGIN
-    RETURN keyword = part
-  END KW;
-BEGIN
-  IF (s[x] >= 'A') & (s[x] <= 'Z') THEN
-    Strings.Extract(s, x, len, part);
-    result := KW('ABS') OR KW('ASH') OR KW('BOOLEAN') OR KW('CAP') OR
-      KW('CHAR') OR KW('CHR') OR KW('COPY') OR KW('DEC') OR KW('ENTIER') OR
-      KW('EXCL') OR KW('FALSE') OR KW('HALT') OR KW('INC') OR KW('INCL') OR
-      KW('INTEGER') OR KW('LEN') OR KW('LONG') OR KW('LONGINT') OR
-      KW('LONGREAL') OR KW('MAX') OR KW('MIN') OR KW('NEW') OR KW('ODD') OR
-      KW('ORD') OR KW('REAL') OR KW('SET') OR KW('SHORT') OR
-      KW('SHORTINT') OR KW('SIZE') OR KW('TRUE') OR KW('ARRAY') OR
-      KW('BEGIN') OR KW('BY') OR KW('CASE') OR KW('DIV') OR KW('DO') OR
-      KW('ELSIF') OR KW('END') OR KW('EXIT') OR KW('FOR') OR KW('IF') OR
-      KW('IMPORT') OR KW('IN') OR KW('IS') OR KW('LOOP') OR
-      KW('MODULE') OR KW('NIL') OR KW('OR') OR KW('POINTER') OR
-      KW('PROCEDURE') OR KW('RECORD') OR KW('REPEAT') OR KW('RETURN') OR
-      KW('THEN') OR KW('TO') OR KW('TYPE') OR KW('VAR') OR KW('WHILE') OR
-      KW('ELSE') OR KW('OF') OR KW('WITH') OR KW('LONGSET') OR
-      KW('UNTIL') OR KW('CONST') OR KW('MOD') OR KW('FLOOR')
-  ELSE result := FALSE
-  END;
-  RETURN result
-END IsKeywordInString;
-
-PROCEDURE GetToken(s: ARRAY OF CHAR; x: INTEGER;
-  VAR class, len, comLevel: INTEGER);
-
-VAR i, chClass: INTEGER;
-
-  PROCEDURE TryNumber;
-  VAR ok, hex, hexEnding, point, scale,
-      scaleSign, scaleNum, finish: BOOLEAN;
-  BEGIN
-    IF (x = 0) OR
-       (~(GetCharClass(s[x - 1]) IN {charDigit, charAlpha}) &
-        (s[x - 1] # '.')) THEN
-      ok := TRUE; finish := FALSE; point := FALSE;
-      hex := FALSE; hexEnding := FALSE;
-      scale := FALSE; scaleSign := FALSE; scaleNum := FALSE;
-      REPEAT
-        CASE s[i] OF
-          '0'..'9': IF scale THEN scaleNum := TRUE END
-        | 'D', 'E':
-          IF point THEN
-            IF scale THEN ok := FALSE
-            ELSE scale := TRUE END
-          ELSE hex := TRUE END
-        | 'A', 'B', 'C', 'F':
-          IF point THEN ok := FALSE ELSE hex := TRUE END
-        | 'H', 'X':
-          IF point OR hexEnding THEN ok := FALSE
-          ELSE hexEnding := TRUE END
-        | '.': IF point OR hex THEN ok := FALSE ELSE point := TRUE END
-        | '-', '+':
-          IF hexEnding OR ~scale OR scaleNum THEN finish := TRUE
-          ELSE scaleSign := TRUE END
-        ELSE
-          IF point & scale & ~scaleNum THEN ok := FALSE
-          ELSE finish := TRUE END
-        END;
-        INC(i)
-      UNTIL ~ok OR finish;
-      IF ok & (~hex OR hexEnding) THEN
-        IF GetCharClass(s[i - 1]) # charAlpha THEN
-          len := i - x - 1;
-          class := tokenNumber
-        END
-      END
-    END
-  END TryNumber;
-
-  PROCEDURE TryString;
-  VAR quote: CHAR;
-  BEGIN
-    quote := s[x];
-    WHILE (s[i] # 0X) & (s[i] # quote) DO INC(i) END;
-    IF s[i] # 0X THEN
-      len := i - x + 1;
-      class := tokenString
-    END
-  END TryString;
-
-  PROCEDURE TryComment;
-  BEGIN
-    IF (i > 0) & (s[i] = '*') THEN INC(i); INC(comLevel) END;
-    IF comLevel > 0 THEN
-      REPEAT
-        WHILE (s[i] # 0X) & (s[i] # '*') DO INC(i) END;
-        IF s[i] = '*' THEN
-          IF (i > 0) & (s[i - 1] = '(') THEN INC(comLevel)
-          ELSIF s[i + 1] = ')' THEN DEC(comLevel) END;
-          INC(i)
-        END
-      UNTIL (s[i] = 0X) OR (comLevel <= 0);
-      len := i - x + 1;
-      class := tokenComment
-    END
-  END TryComment;
-
-BEGIN
-  class := tokenOther; len := 0;
-  IF s[x] # 0X THEN
-    IF (x = 0) & (comLevel > 0) THEN i := 0; TryComment END;
-    IF (s[x] # 0X) & (class # tokenComment) THEN
-      i := x + 1;
-      chClass := GetCharClass(s[x]);
-      CASE chClass OF
-        charAlpha:
-        IF (x = 0) OR (GetCharClass(s[x - 1]) # charDigit) THEN
-          WHILE GetCharClass(s[i]) = charAlpha DO INC(i) END;
-          len := i - x;
-          IF IsKeywordInString(s, x, len) THEN class := tokenKeyword END
-        END
-      | charDigit: TryNumber
-      | charQuote: TryString
-      | charOpenBracket: TryComment
-      ELSE len := 1 END
-    END
-  END
-END GetToken;
-
-PROCEDURE GetTokenColor(class: INTEGER): INTEGER;
-VAR color: INTEGER;
-BEGIN
-  CASE class OF
-    tokenKeyword: color := 15
-  | tokenNumber:  color := 3
-  | tokenString:  color := 11
-  | tokenComment: color := 7
-  ELSE color := 14 END;
-  RETURN color
-END GetTokenColor;
-
-(* Like T.PutString, but highlighted *)
-PROCEDURE PutStringH*(x, y: INTEGER; s: ARRAY OF CHAR;
-  bg, limit: INTEGER; VAR comLevel: INTEGER);
-VAR i, fg, class, len: INTEGER;
-BEGIN
-  IF limit = 0 THEN limit := T.charsX END; 
-  GetToken(s, 0, class, len, comLevel);
-  fg := GetTokenColor(class); i := 0;
-  WHILE (s[i] # 0X) & (x < limit) DO
-    T.PutChar(x, y, s[i], fg, bg);
-    INC(i); INC(x); DEC(len);
-    IF len <= 0 THEN
-      GetToken(s, i, class, len, comLevel);
-      fg := GetTokenColor(class)
-    END
-  END;
-  DEC(i);
-  WHILE (i >= 0) & (s[i] = ' ') DO
-    DEC(x);
-    T.PutChar(x, y, 0FAX, 3, bg);
-    DEC(i)
-  END
-END PutStringH;
-
-(* Put selected string (highlighted) *)
-PROCEDURE PutSelStringH*(x, y: INTEGER; s: ARRAY OF CHAR;
-  bg, limit: INTEGER; VAR comLevel: INTEGER;
-  fgSel, bgSel: INTEGER; x0, x1: INTEGER);
-VAR i, fg, class, len: INTEGER; sel: BOOLEAN;
-BEGIN
-  sel := FALSE;
-  GetToken(s, 0, class, len, comLevel);
-  fg := GetTokenColor(class); i := 0;
-  WHILE (s[i] # 0X) & (x < limit) DO
-    IF i = x1 + 1 THEN sel := FALSE
-    ELSIF i = x0 THEN sel := TRUE END;
-    IF sel THEN T.PutChar(x, y, s[i], fgSel, bgSel)
-    ELSE T.PutChar(x, y, s[i], fg, bg) END;
-    INC(i); INC(x); DEC(len);
-    IF len <= 0 THEN
-      GetToken(s, i, class, len, comLevel);
-      fg := GetTokenColor(class)
-    END
-  END
-END PutSelStringH;
-
-PROCEDURE TextChanged*(c: Editor): BOOLEAN;
-BEGIN
-  RETURN c.text.changed
-END TextChanged;
-
-PROCEDURE PrintText*(c: Editor);
-VAR L: Text.Line; scrY: INTEGER;
-  textY, x0, x1, minX, maxX, maxY, comLevel: INTEGER;
-  cursorHandled: BOOLEAN;
-BEGIN T.CharFill(c.x + 1, c.y + 1, c.w - 2, c.h - 2, ' ', 15, c.bg);
-  L := c.text.scrFirst; scrY := c.y + 1; textY := c.text.scrY;
-  minX := c.x + 1; maxX := c.x + c.w - 1;
-  maxY := c.y + c.h - 1; comLevel := L.comLevel;
-  cursorHandled := FALSE;
-  WHILE (L # NIL) & (scrY < maxY) DO
-    IF c.text.selected & (c.text.selT <= textY) & (textY <= c.text.selB) THEN
-      IF textY = c.text.selT THEN x0 := c.text.selL ELSE x0 := 0 END;
-      IF textY = c.text.selB THEN x1 := c.text.selR - 1 ELSE x1 := L.len END;
-      PutSelStringH(minX, scrY, L.s, c.bg, maxX, comLevel, 1, 7, x0, x1)
-    ELSE PutStringH(minX, scrY, L.s, c.bg, maxX, comLevel)
-    END;
-    IF (L = c.text.cur) & (c.text.x + 1 < T.charsX) &
-       ~OV.HasModalWindow(c.app) THEN
-      x0 := minX + c.text.x;
-      IF c.focused & (x0 <= maxX) THEN
-        T.ShowCursor(TRUE); T.GoToXY(x0, scrY);
-        cursorHandled := TRUE
-      END
-    END;
-    L := L.next; INC(scrY); INC(textY);
-    IF L # NIL THEN L.comLevel := comLevel END
-  END;
-  IF c.focused & ~cursorHandled THEN T.ShowCursor(FALSE) END
-END PrintText;
-
-PROCEDURE StartSelection(c: Editor);
-BEGIN c.text.selected := TRUE;
-  c.text.selL := c.text.x; c.text.selR := c.text.x;
-  c.text.selT := c.text.y; c.text.selB := c.text.y
-END StartSelection;
-
-PROCEDURE CheckSelBorders(c: Editor; VAR onLeft, onRight: BOOLEAN);
-BEGIN
-  onLeft := (c.text.x = c.text.selL) & (c.text.y = c.text.selT);
-  onRight := (c.text.x = c.text.selR) & (c.text.y = c.text.selB)
-END CheckSelBorders;
-
-PROCEDURE Swap(VAR a, b: INTEGER);
-VAR tmp: INTEGER;
-BEGIN
-  tmp := a; a := b; b := tmp
-END Swap;
-
-PROCEDURE Order(VAR a, b: INTEGER);
-VAR tmp: INTEGER;
-BEGIN
-  IF a > b THEN tmp := a; a := b; b := tmp END
-END Order;
-
-(* Starts, removes, upgrades selection if Shift held. *)
-(* dir is one of Direction of Selection constants. *)
-PROCEDURE HandleSelection(c: Editor; dir: INTEGER; viaHoriz: BOOLEAN);
-VAR mod: SET; onLeft, onRight: BOOLEAN;
-BEGIN
-  IF ~G.ShiftPressed() THEN c.text.selected := FALSE
-  ELSE CheckSelBorders(c, onLeft, onRight);
-    IF ~c.text.selected OR (~onLeft & ~onRight) THEN
-      StartSelection(c); (* Reset/new selection *)
-      CheckSelBorders(c, onLeft, onRight)
-    END;
-    CASE dir OF
-      dirRight:
-      IF onRight THEN INC(c.text.selR)
-      ELSIF onLeft THEN INC(c.text.selL) END
-    | dirLeft:
-      IF onLeft THEN DEC(c.text.selL)
-      ELSIF onRight THEN DEC(c.text.selR) END
-    | dirUp:
-      IF onLeft THEN
-        DEC(c.text.selT);
-        IF viaHoriz THEN c.text.selL := c.text.cur.prev.len
-        ELSIF c.text.selL > c.text.cur.prev.len
-        THEN c.text.selL := c.text.cur.prev.len END
-      ELSIF viaHoriz THEN
-        DEC(c.text.selB); c.text.selR := c.text.cur.prev.len
-      ELSIF (c.text.selB > c.text.selT + 1) OR
-         ((c.text.selB = c.text.selT + 1) &
-          (c.text.selR >= c.text.selL)) THEN
-        DEC(c.text.selB);
-        IF c.text.selR > c.text.cur.prev.len
-        THEN c.text.selR := c.text.cur.prev.len END
-      ELSIF c.text.selT = c.text.selB THEN
-        DEC(c.text.selT); Order(c.text.selR, c.text.selL);
-        IF c.text.selL > c.text.cur.prev.len
-        THEN c.text.selL := c.text.cur.prev.len END
-      ELSE
-        DEC(c.text.selB); Order(c.text.selL, c.text.selR);
-        IF c.text.selR > c.text.cur.prev.len
-        THEN c.text.selR := c.text.cur.prev.len END
-      END
-    | dirDown:
-      IF onRight THEN
-        INC(c.text.selB);
-        IF viaHoriz THEN c.text.selR := 0
-        ELSIF c.text.selR > c.text.cur.next.len
-        THEN c.text.selR := c.text.cur.next.len END
-      ELSIF viaHoriz THEN
-        INC(c.text.selT); c.text.selL := 0
-      ELSIF (c.text.selB > c.text.selT + 1) OR
-         ((c.text.selB = c.text.selT + 1) &
-          (c.text.selR >= c.text.selL)) THEN
-        INC(c.text.selT);
-        IF c.text.selL > c.text.cur.next.len
-        THEN c.text.selL := c.text.cur.next.len END
-      ELSIF c.text.selT = c.text.selB THEN
-        INC(c.text.selB); Swap(c.text.selL, c.text.selR);
-        IF c.text.selR > c.text.cur.next.len
-        THEN c.text.selR := c.text.cur.next.len END
-      ELSE
-        INC(c.text.selT); Swap(c.text.selL, c.text.selR);
-        IF c.text.selL > c.text.cur.next.len
-        THEN c.text.selL := c.text.cur.next.len END
-      END
-    ELSE
-    END;
-    IF ((c.text.selT >= c.text.selB) & (c.text.selL >= c.text.selR)) OR
-       (c.text.selT > c.text.selB) THEN
-      c.text.selected := FALSE
-    END
-  END
-END HandleSelection;
-
-PROCEDURE MoveScreenByLine(c: Editor; down: BOOLEAN);
-BEGIN
-  IF down THEN
-    ASSERT(c.text.scrFirst.next # NIL, 98);
-    c.text.scrFirst := c.text.scrFirst.next; INC(c.text.scrY)
-  ELSE (* Up *)
-    ASSERT(c.text.scrFirst.prev # NIL, 98); ASSERT(c.text.scrY > 0, 99);
-    c.text.scrFirst := c.text.scrFirst.prev; DEC(c.text.scrY)
-  END;
-  T.ResetCursorBlink
-END MoveScreenByLine;
-
-PROCEDURE MoveScreen(c: Editor; delta: INTEGER);
-BEGIN
-  IF delta > 0 THEN
-    WHILE (delta > 0) & (c.text.scrFirst.next # NIL) DO
-      MoveScreenByLine(c, TRUE); DEC(delta)
-    END
-  ELSE (* Up *)
-    WHILE (delta < 0) & (c.text.scrFirst.prev # NIL) DO
-      MoveScreenByLine(c, FALSE); INC(delta)
-    END
-  END
-END MoveScreen;
-
-(* Moves input cursor up and down *)
-PROCEDURE MoveByLine(c: Editor; down, viaHoriz: BOOLEAN);
-VAR moved: BOOLEAN;
-BEGIN
-  moved := FALSE;
-  IF down THEN
-    IF c.text.cur.next # NIL THEN
-      HandleSelection(c, dirDown, viaHoriz);
-      moved := TRUE;
-      c.text.cur := c.text.cur.next; INC(c.text.y);
-      IF c.text.y >= c.text.scrY + c.h - 2 THEN MoveScreenByLine(c, TRUE) END
-    END
-  ELSE (* Up *)
-    IF c.text.cur.prev # NIL THEN
-      HandleSelection(c, dirUp, viaHoriz);
-      moved := TRUE;
-      IF c.text.scrFirst = c.text.cur THEN MoveScreenByLine(c, FALSE) END;
-      c.text.cur := c.text.cur.prev; DEC(c.text.y)
-    END
-  END;
-  IF moved THEN
-    IF c.text.x > c.text.cur.len THEN c.text.x := c.text.cur.len END;
-    PrintText(c)
-  END;
-  T.ResetCursorBlink
-END MoveByLine;
-
-(* Moves input cursor up and down by page *)
-PROCEDURE MoveByPage(c: Editor; down: BOOLEAN);
-VAR i: INTEGER; moved: BOOLEAN;
-BEGIN
-  i := 1;
-  IF down THEN
-    moved := c.text.cur.next # NIL;
-    WHILE (i < c.h - 2) & (c.text.cur.next # NIL) DO
-      c.text.scrFirst := c.text.scrFirst.next; INC(c.text.scrY);
-      MoveByLine(c, TRUE, FALSE);
-      INC(i)
-    END
-  ELSE (* Up *)
-    moved := c.text.cur.prev # NIL;
-    WHILE (i < c.h - 2) & (c.text.cur.prev # NIL) DO
-      IF c.text.scrY > 0 THEN c.text.scrFirst := c.text.scrFirst.prev; DEC(c.text.scrY) END;
-      MoveByLine(c, FALSE, FALSE);
-      INC(i)
-    END
-  END;
-  T.ResetCursorBlink;
-  IF moved THEN PrintText(c) END
-END MoveByPage;
-
-(* Moves input cursor left and right by one char *)
-PROCEDURE MoveInLine(c: Editor; right: BOOLEAN);
-BEGIN
-  IF right THEN
-    IF c.text.x < c.text.cur.len THEN
-      HandleSelection(c, dirRight, FALSE); INC(c.text.x)
-    ELSIF c.text.cur.next # NIL THEN
-      MoveByLine(c, TRUE, TRUE);
-      c.text.x := 0;
-    END
-  ELSE (* Left *)
-    IF c.text.x > 0 THEN
-      HandleSelection(c, dirLeft, FALSE); DEC(c.text.x)
-    ELSIF c.text.cur.prev # NIL THEN
-      MoveByLine(c, FALSE, TRUE);
-      c.text.x := c.text.cur.len;
-    END
-  END;
-  T.ResetCursorBlink;
-  PrintText(c)
-END MoveInLine;
-
-(* Moves input cursor to start and to end *)
-PROCEDURE MoveToLineEdge(c: Editor; end: BOOLEAN);
-VAR onLeft, onRight: BOOLEAN;
-BEGIN
-  IF G.ShiftPressed() THEN
-    IF ~c.text.selected THEN StartSelection(c)
-    ELSE
-      CheckSelBorders(c, onLeft, onRight);
-      IF ~onLeft & ~onRight THEN StartSelection(c) END
-    END;
-    CheckSelBorders(c, onLeft, onRight);
-    IF end THEN
-      IF onRight THEN c.text.selR := c.text.cur.len
-      ELSE c.text.selL := c.text.cur.len END
-    ELSE (* Home *)
-      IF onRight THEN c.text.selR := 0
-      ELSE c.text.selL := 0 END
-    END;
-    IF c.text.selT = c.text.selB THEN Order(c.text.selL, c.text.selR) END
-  END;
-  IF end THEN
-    IF c.text.x < c.text.cur.len THEN c.text.x := c.text.cur.len END
-  ELSE (* Home *)
-    IF c.text.x > 0 THEN c.text.x := 0 END
-  END;
-  T.ResetCursorBlink;
-  PrintText(c)
-END MoveToLineEdge;
-
-PROCEDURE HandleBackspace(c: Editor);
-BEGIN
-  c.text.HandleBackspace;
-  PrintText(c)
-END HandleBackspace;
-
-PROCEDURE HandleDelete(c: Editor);
-BEGIN
-  c.text.HandleDelete;
-  PrintText(c)
-END HandleDelete;
-
-PROCEDURE HandleTab(e: Editor; shift: BOOLEAN);
-BEGIN
-  IF shift THEN e.text.RemoveIndent
-  ELSIF e.text.WholeLineSelected() THEN
-    IF shift THEN e.text.RemoveIndent
-    ELSE e.text.AddIndent
-    END
-  ELSE
-    IF e.text.selected THEN e.text.DeleteSelection END;
-    e.text.InsertChar(' ');
-    IF ODD(e.text.x MOD 2) THEN e.text.InsertChar(' ') END
-  END;
-  PrintText(e)
-END HandleTab;
-
-PROCEDURE HandleEnter(c: Editor);
-BEGIN
-  c.text.HandleEnter(TRUE);
-  IF c.text.y >= c.text.scrY + c.h - 2 THEN MoveScreenByLine(c, TRUE) END;
-  PrintText(c)
-END HandleEnter;
-
-PROCEDURE InitEditor*(c: Editor);
-BEGIN OV.InitWindow(c);
-  c.filename[0] := 0X;
-  c.bg := 1; c.resizable := TRUE; c.caption := 'NONAME';
-  c.text := Text.NewText();
-  c.do := editorMethod
-END InitEditor;
-
-PROCEDURE NewEditor*(): Editor;
-VAR w: Editor;
-BEGIN NEW(w); InitEditor(w); RETURN w
-END NewEditor;
-
-(* Standard Menu Hanlders *)
-
-PROCEDURE EditCut*(c: OV.Control);
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
-    c.app.windows(Editor).text.CopySelection(clipboard);
-    c.app.windows(Editor).text.DeleteSelection
-  END
-END EditCut;
-
-PROCEDURE EditCopy*(c: OV.Control);
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
-    c.app.windows(Editor).text.CopySelection(clipboard);
-    PrintText(c.app.windows(Editor))
-  END
-END EditCopy;
-
-PROCEDURE EditClear*(c: OV.Control);
-VAR e: OV.Control;
-BEGIN e := c.app.windows;
-  IF (e # NIL) & (e IS Editor) THEN
-    e(Editor).text.DeleteSelection;
-    PrintText(e(Editor))
-  END
-END EditClear;
-
-PROCEDURE EditPaste*(c: OV.Control);
-VAR e: Editor;
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor) &
-      (clipboard[0] # 0X) THEN
-    e := c.app.windows(Editor);
-    e.text.DeleteSelection;
-    e.text.Insert(clipboard, FALSE);
-    IF e.text.y >= e.text.scrY + e.h - 2 THEN
-      MoveScreen(e, e.text.y - e.h + 3 - e.text.scrY)
-    END;
-    PrintText(e)
-  END
-END EditPaste;
-
-PROCEDURE EditSelectAll*(c: OV.Control);
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
-    c.app.windows(Editor).text.SelectAll;
-    PrintText(c.app.windows(Editor))
-  END
-END EditSelectAll;
-
-PROCEDURE EditUnselect*(c: OV.Control);
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
-    c.app.windows(Editor).text.selected := FALSE;
-    PrintText(c.app.cur(Editor))
-  END
-END EditUnselect;
-
-(* EditorMethod *)
-
-PROCEDURE EditorDraw*(c: OV.Control; x, y: INTEGER);
-BEGIN OV.WindowDraw(c, x, y);
-  PrintText(c(Editor))
-END EditorDraw;
-
-PROCEDURE EditorMouseDown*(c: OV.Control; x, y, button: INTEGER);
-VAR t: Text.Text; L: Text.Line; i: INTEGER;
-BEGIN OV.WindowMouseDown(c, x, y, button);
-  IF (x > 0) & (x < c.w - 1) & (y > 0) & (y < c.h - 1) THEN
-    DEC(x); DEC(y); t := c(Editor).text;
-    t.selected := FALSE; L := t.scrFirst; i := y;
-    WHILE (i > 0) & (L # NIL) DO DEC(i); L := L.next END;
-    IF L # NIL THEN
-      t.cur := L; t.y := y + t.scrY;
-      IF x > L.len THEN x := L.len END;
-      t.x := x;
-      T.GoToXY(c.x + x + 1, c.y + y + 1)
-    END;
-    PrintText(c(Editor))
-  END
-END EditorMouseDown;
-
-PROCEDURE EditorMouseUp*(c: OV.Control; x, y, button: INTEGER);
-BEGIN
-  OV.WindowMouseUp(c, x, y, button)
-END EditorMouseUp;
-
-PROCEDURE EditorMouseMove*(c: OV.Control; x, y: INTEGER; buttons: SET);
-BEGIN
-  OV.WindowMouseMove(c, x, y, buttons)
-END EditorMouseMove;
-
-PROCEDURE EditorTextInput(c: OV.Control; s: ARRAY OF CHAR; sym: INTEGER);
-BEGIN
-  IF sym # 0 THEN c(Editor).text.InsertChar(CHR(sym)); T.ResetCursorBlink;
-    c(Editor).text.selected := FALSE; PrintText(c(Editor))
-  END
-END EditorTextInput;
-
-PROCEDURE EditorKeyDown*(c: OV.Control; key: G.Key);
-BEGIN
-  CASE key.code OF
-    G.kLeft:      MoveInLine(c(Editor), FALSE)
-  | G.kRight:     MoveInLine(c(Editor), TRUE)
-  | G.kUp:        MoveByLine(c(Editor), FALSE, FALSE)
-  | G.kDown:      MoveByLine(c(Editor), TRUE, FALSE)
-  | G.kHome:      MoveToLineEdge(c(Editor), FALSE)
-  | G.kEnd:       MoveToLineEdge(c(Editor), TRUE)
-  | G.kPgUp:      MoveByPage(c(Editor), FALSE)
-  | G.kPgDn:      MoveByPage(c(Editor), TRUE)
-  | G.kBackspace: HandleBackspace(c(Editor))
-  | G.kDel:       HandleDelete(c(Editor))
-  | G.kEnter, G.kEnterPad: HandleEnter(c(Editor))
-  | G.kTab:       HandleTab(c(Editor), key.mod * G.mShift # {})
-  ELSE
-  END
-END EditorKeyDown;
-
-PROCEDURE EditorGetFocus*(c: OV.Control);
-BEGIN
-  IF OV.windowMethod.getFocus # NIL THEN OV.windowMethod.getFocus(c) END;
-  G.StartTextInput; T.ShowCursor(TRUE)
-END EditorGetFocus;
-
-PROCEDURE EditorRefresh(c: OV.Control);
-BEGIN OV.WindowRefresh(c)
-END EditorRefresh;
-
-PROCEDURE InitEditorMethod*(m: OV.ControlMethod);
-BEGIN OV.InitWindowMethod(m);
-  m.getFocus := EditorGetFocus;
-  m.refresh := EditorRefresh;
-  m.draw := EditorDraw;
-  m.mouseDown := EditorMouseDown;
-  m.mouseUp := EditorMouseUp;
-  m.mouseMove := EditorMouseMove;
-  m.keyDown := EditorKeyDown;
-  m.textInput := EditorTextInput
-END InitEditorMethod;
-
-BEGIN
-  NEW(editorMethod); InitEditorMethod(editorMethod);
-  NEW(fileDialogMethod); InitFileDialogMethod(fileDialogMethod)
-END Editor.
+MODULE Editor;
+(* Copyright 2017-2021 Arthur Yefimov
+
+This file is part of Free Oberon.
+
+Free Oberon is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Free Oberon is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
+*)
+IMPORT OV, T := Terminal, G := Graph, Text := EditorText,
+  Strings, StrList, Out;
+CONST
+  (* Direction of Selection *)
+  dirLeft  = 0;
+  dirRight = 1;
+  dirUp    = 2;
+  dirDown  = 3;
+
+  (* Character Classes *)
+  charOther       = 0;
+  charAlpha       = 1;
+  charDigit       = 2;
+  charMinusPlus   = 3;
+  charQuote       = 4;
+  charOpenBracket = 5;
+
+  (* Token Classes *)
+  tokenOther   = 0;
+  tokenKeyword = 1;
+  tokenNumber  = 2;
+  tokenString  = 3;
+  tokenComment = 4;
+
+  (* FileDialog type *)
+  open* = 1;
+  save* = 2;
+
+TYPE
+  CHAR = SHORTCHAR;
+
+  FileDialog* = POINTER TO FileDialogDesc;
+  FileDialogDesc* = RECORD(OV.WindowDesc)
+    type*: INTEGER; (* open or save *)
+    edtFilename*: OV.Edit;
+    btnOk*, btnCancel*: OV.Button;
+    cslFiles*: OV.ColumnSelection;
+    onFileOk*: PROCEDURE (c: OV.Control; filename: ARRAY OF CHAR)
+  END;
+
+  Editor* = POINTER TO EditorDesc;
+  EditorDesc* = RECORD(OV.WindowDesc)
+    text*: Text.Text;
+    filename*: ARRAY 1000 OF CHAR
+  END;
+
+VAR
+  clipboard: ARRAY 16000 OF CHAR;
+  editorMethod-: OV.ControlMethod;
+  fileDialogMethod-: OV.ControlMethod;
+
+(* FileDialog *)
+
+PROCEDURE FileDialogOkClick*(c: OV.Control);
+VAR w: FileDialog;
+BEGIN OV.CloseCurWindow(c);
+  w := c.parent(FileDialog);
+  IF w.onFileOk # NIL THEN
+    w.onFileOk(c, w.edtFilename.caption)
+  END
+END FileDialogOkClick;
+
+PROCEDURE InitFileDialog*(c: FileDialog; type: INTEGER);
+BEGIN OV.InitWindow(c); c.do := fileDialogMethod; c.type := type;
+  IF type = open THEN c.caption := 'Open a File'
+  ELSE c.caption := 'Save File As'
+  END;
+  c.modal := TRUE; c.w := 70; c.h := 20; OV.CenterWindow(c);
+
+  (* File Name Edit *)
+  c.edtFilename := OV.NewEdit();
+  c.edtFilename.do.resize(c.edtFilename, 3, 3, c.w - 18, 1);
+  OV.Add(c, c.edtFilename);
+
+  (* Open/Save (Ok) button *)
+  IF type = open THEN c.btnOk := OV.NewButton('&Open')
+  ELSE c.btnOk := OV.NewButton('O&K')
+  END;
+  c.btnOk.default := TRUE;
+  c.btnOk.do.resize(c.btnOk, c.w - 13, 3, 9, 1);
+  c.btnOk.onClick := FileDialogOkClick;
+  OV.Add(c, c.btnOk);
+
+  (* Cancel button *)
+  c.btnCancel := OV.NewButton('Cancel');
+  c.btnCancel.onClick := OV.CloseCurWindow;
+  c.btnCancel.do.resize(c.btnCancel, c.w - 13, 11, 9, 1);
+  OV.Add(c, c.btnCancel);
+
+  (* ColumnSelection *)
+  c.cslFiles := OV.NewColumnSelection();
+  (*c.cslFiles.onClick := FileDialogOkClick;*)
+  c.cslFiles.do.resize(c.cslFiles, 3, 6, c.w - 18, c.h - 8);
+  StrList.Append(c.cslFiles.items, '[..]');
+  StrList.Append(c.cslFiles.items, '[EGABGI]');
+  StrList.Append(c.cslFiles.items, '[Program Files]');
+  StrList.Append(c.cslFiles.items, 'GRAPH3.TPU');
+  StrList.Append(c.cslFiles.items, 'MEMORY.TPU');
+  StrList.Append(c.cslFiles.items, 'MENUS.TPU');
+  StrList.Append(c.cslFiles.items, 'CRT.PAS');
+  StrList.Append(c.cslFiles.items, 'TURBO3.TPU');
+  StrList.Append(c.cslFiles.items, 'COMPUTER.TPU');
+  StrList.Append(c.cslFiles.items, 'TEXTVIEW.TPU');
+  StrList.Append(c.cslFiles.items, 'HISTLIST.TPU');
+  StrList.Append(c.cslFiles.items, 'VALIDATE.TPU');
+  StrList.Append(c.cslFiles.items, 'STRINGS.TPU');
+  StrList.Append(c.cslFiles.items, 'MSGBOX.TPU');
+  StrList.Append(c.cslFiles.items, 'OPENGL.TPU');
+  StrList.Append(c.cslFiles.items, 'SDL2.TPU');
+  OV.Add(c, c.cslFiles)
+END InitFileDialog;
+
+PROCEDURE NewFileDialog*(type: INTEGER): FileDialog;
+VAR c: FileDialog;
+BEGIN NEW(c); InitFileDialog(c, type); RETURN c
+END NewFileDialog;
+
+(* FileDialog Method *)
+
+PROCEDURE FileDialogDraw*(c: OV.Control; x, y: INTEGER);
+VAR f: FileDialog;
+BEGIN f := c(FileDialog);
+  OV.WindowDraw(c, x, y); INC(x, c.x); INC(y, c.y);
+  IF f.type = open THEN OV.PutMarkedString(x + 3, y + 2, '&Name', 15, 14, 7, 0)
+  ELSE OV.PutMarkedString(x + 3, y + 2, '&Save file as', 15, 14, 7, 0)
+  END;
+  OV.PutMarkedString(x + 3, y + 5, '&Files', 0, 14, 7, 0)
+END FileDialogDraw;
+
+PROCEDURE FileDialogGetFocus(c: OV.Control);
+BEGIN OV.WindowGetFocus(c)
+END FileDialogGetFocus;
+
+PROCEDURE FileDialogKeyDown*(c: OV.Control; key: G.Key);
+BEGIN
+  CASE key.code OF
+    G.kEsc: OV.CloseCurWindow(c)
+  | G.kEnter: c(FileDialog).btnOk.do.click(c(FileDialog).btnOk)
+  ELSE
+  END
+END FileDialogKeyDown;
+
+PROCEDURE InitFileDialogMethod*(m: OV.ControlMethod);
+BEGIN OV.InitWindowMethod(m);
+  m.draw := FileDialogDraw;
+  m.getFocus := FileDialogGetFocus;
+  m.keyDown := FileDialogKeyDown
+END InitFileDialogMethod;
+
+(* Editor *)
+
+PROCEDURE IsEmpty*(e: Editor): BOOLEAN;
+BEGIN
+  RETURN (e.filename[0] = 0X) & (e.text.first = e.text.last) &
+         (e.text.first.len = 0)
+END IsEmpty;
+
+PROCEDURE IntToStr*(n: INTEGER; VAR s: ARRAY OF CHAR); (* !TODO move out *)
+(* LEN(s) > 1 *)
+VAR i, j: INTEGER; tmp: CHAR; neg: BOOLEAN;
+BEGIN
+  IF n = 0 THEN
+    s[0] := '0'; i := 1
+  ELSE i := 0; neg := n < 0; 
+    IF neg THEN n := -n END;
+    WHILE (n > 0) & (i < LEN(s) - 1) DO
+      s[i] := CHR(ORD('0') + n MOD 10);
+      n := n DIV 10; INC(i)
+    END;
+    IF neg & (i < LEN(s) - 1) THEN s[i] := '-'; INC(i) END;
+  END;
+  s[i] := 0X; j := 0; DEC(i);
+  WHILE j < i DO
+    tmp := s[j]; s[j] := s[i]; s[i] := tmp;
+    INC(j); DEC(i)
+  END
+END IntToStr;
+
+PROCEDURE StringsFindPrev* (pattern, stringToSearch: ARRAY OF CHAR; startPos: INTEGER;
+                     VAR patternFound: BOOLEAN; VAR posOfPattern: INTEGER); (* !TODO move out *)
+VAR patternPos, stringLength, patternLength: INTEGER;
+BEGIN
+  (* correct `startPos' if it is larger than the possible searching range *)
+  stringLength := Strings.Length (stringToSearch);
+  patternLength := Strings.Length (pattern);
+  IF (startPos > stringLength-patternLength) THEN
+    startPos := stringLength-patternLength
+  END;
+  
+  IF (startPos >= 0) THEN
+    patternPos := 0;
+    LOOP
+      IF (pattern[patternPos] = 0X) THEN     
+        (* reached end of pattern *)
+        patternFound := TRUE;
+        posOfPattern := startPos-patternPos;
+        EXIT
+      ELSIF (stringToSearch[startPos] # pattern[patternPos]) THEN
+        (* characters differ: reset indices and restart *)
+        IF (startPos > patternPos) THEN
+          startPos := startPos-patternPos-1;
+          patternPos := 0
+        ELSE
+          (* reached beginning of `stringToSearch' without finding a match *)
+          patternFound := FALSE;
+          EXIT
+        END
+      ELSE  (* characters identic, compare next one *)
+        INC (startPos); 
+        INC (patternPos)
+      END
+    END
+  ELSE
+    patternFound := FALSE
+  END
+END StringsFindPrev;
+
+PROCEDURE GetCharClass(ch: CHAR): INTEGER;
+VAR class: INTEGER;
+BEGIN
+  CASE ch OF
+    'a'..'z', 'A'..'Z': class := charAlpha
+  | '0'..'9': class := charDigit
+  | '-', '+': class := charMinusPlus
+  | '"', "'": class := charQuote
+  | '(': class := charOpenBracket
+  ELSE class := charOther END;
+  RETURN class
+END GetCharClass;
+
+PROCEDURE IsHexDigit(ch: CHAR): BOOLEAN;
+VAR result: BOOLEAN;
+BEGIN
+  CASE ch OF
+    'A'..'F': result := TRUE
+  ELSE result := FALSE END;
+  RETURN result
+END IsHexDigit;
+
+PROCEDURE IsKeywordInString(s: ARRAY OF CHAR; x, len: INTEGER): BOOLEAN;
+VAR part: ARRAY 32 OF CHAR; result: BOOLEAN;
+  PROCEDURE KW(keyword: ARRAY OF CHAR): BOOLEAN;
+  BEGIN
+    RETURN keyword = part
+  END KW;
+BEGIN
+  IF (s[x] >= 'A') & (s[x] <= 'Z') THEN
+    Strings.Extract(s, x, len, part);
+    result := KW('ABS') OR KW('ASH') OR KW('BOOLEAN') OR KW('CAP') OR
+      KW('CHAR') OR KW('CHR') OR KW('COPY') OR KW('DEC') OR KW('ENTIER') OR
+      KW('EXCL') OR KW('FALSE') OR KW('HALT') OR KW('INC') OR KW('INCL') OR
+      KW('INTEGER') OR KW('LEN') OR KW('LONG') OR KW('LONGINT') OR
+      KW('LONGREAL') OR KW('MAX') OR KW('MIN') OR KW('NEW') OR KW('ODD') OR
+      KW('ORD') OR KW('REAL') OR KW('SET') OR KW('SHORT') OR
+      KW('SHORTINT') OR KW('SIZE') OR KW('TRUE') OR KW('ARRAY') OR
+      KW('BEGIN') OR KW('BY') OR KW('CASE') OR KW('DIV') OR KW('DO') OR
+      KW('ELSIF') OR KW('END') OR KW('EXIT') OR KW('FOR') OR KW('IF') OR
+      KW('IMPORT') OR KW('IN') OR KW('IS') OR KW('LOOP') OR
+      KW('MODULE') OR KW('NIL') OR KW('OR') OR KW('POINTER') OR
+      KW('PROCEDURE') OR KW('RECORD') OR KW('REPEAT') OR KW('RETURN') OR
+      KW('THEN') OR KW('TO') OR KW('TYPE') OR KW('VAR') OR KW('WHILE') OR
+      KW('ELSE') OR KW('OF') OR KW('WITH') OR KW('LONGSET') OR
+      KW('UNTIL') OR KW('CONST') OR KW('MOD') OR KW('FLOOR')
+  ELSE result := FALSE
+  END;
+  RETURN result
+END IsKeywordInString;
+
+PROCEDURE GetToken(s: ARRAY OF CHAR; x: INTEGER;
+  VAR class, len, comLevel: INTEGER);
+
+VAR i, chClass: INTEGER;
+
+  PROCEDURE TryNumber;
+  VAR ok, hex, hexEnding, point, scale,
+      scaleSign, scaleNum, finish: BOOLEAN;
+  BEGIN
+    IF (x = 0) OR
+       (~(GetCharClass(s[x - 1]) IN {charDigit, charAlpha}) &
+        (s[x - 1] # '.')) THEN
+      ok := TRUE; finish := FALSE; point := FALSE;
+      hex := FALSE; hexEnding := FALSE;
+      scale := FALSE; scaleSign := FALSE; scaleNum := FALSE;
+      REPEAT
+        CASE s[i] OF
+          '0'..'9': IF scale THEN scaleNum := TRUE END
+        | 'D', 'E':
+          IF point THEN
+            IF scale THEN ok := FALSE
+            ELSE scale := TRUE END
+          ELSE hex := TRUE END
+        | 'A', 'B', 'C', 'F':
+          IF point THEN ok := FALSE ELSE hex := TRUE END
+        | 'H', 'X':
+          IF point OR hexEnding THEN ok := FALSE
+          ELSE hexEnding := TRUE END
+        | '.': IF point OR hex THEN ok := FALSE ELSE point := TRUE END
+        | '-', '+':
+          IF hexEnding OR ~scale OR scaleNum THEN finish := TRUE
+          ELSE scaleSign := TRUE END
+        ELSE
+          IF point & scale & ~scaleNum THEN ok := FALSE
+          ELSE finish := TRUE END
+        END;
+        INC(i)
+      UNTIL ~ok OR finish;
+      IF ok & (~hex OR hexEnding) THEN
+        IF GetCharClass(s[i - 1]) # charAlpha THEN
+          len := i - x - 1;
+          class := tokenNumber
+        END
+      END
+    END
+  END TryNumber;
+
+  PROCEDURE TryString;
+  VAR quote: CHAR;
+  BEGIN
+    quote := s[x];
+    WHILE (s[i] # 0X) & (s[i] # quote) DO INC(i) END;
+    IF s[i] # 0X THEN
+      len := i - x + 1;
+      class := tokenString
+    END
+  END TryString;
+
+  PROCEDURE TryComment;
+  BEGIN
+    IF (i > 0) & (s[i] = '*') THEN INC(i); INC(comLevel) END;
+    IF comLevel > 0 THEN
+      REPEAT
+        WHILE (s[i] # 0X) & (s[i] # '*') DO INC(i) END;
+        IF s[i] = '*' THEN
+          IF (i > 0) & (s[i - 1] = '(') THEN INC(comLevel)
+          ELSIF s[i + 1] = ')' THEN DEC(comLevel) END;
+          INC(i)
+        END
+      UNTIL (s[i] = 0X) OR (comLevel <= 0);
+      len := i - x + 1;
+      class := tokenComment
+    END
+  END TryComment;
+
+BEGIN
+  class := tokenOther; len := 0;
+  IF s[x] # 0X THEN
+    IF (x = 0) & (comLevel > 0) THEN i := 0; TryComment END;
+    IF (s[x] # 0X) & (class # tokenComment) THEN
+      i := x + 1;
+      chClass := GetCharClass(s[x]);
+      CASE chClass OF
+        charAlpha:
+        IF (x = 0) OR (GetCharClass(s[x - 1]) # charDigit) THEN
+          WHILE GetCharClass(s[i]) = charAlpha DO INC(i) END;
+          len := i - x;
+          IF IsKeywordInString(s, x, len) THEN class := tokenKeyword END
+        END
+      | charDigit: TryNumber
+      | charQuote: TryString
+      | charOpenBracket: TryComment
+      ELSE len := 1 END
+    END
+  END
+END GetToken;
+
+PROCEDURE GetTokenColor(class: INTEGER): INTEGER;
+VAR color: INTEGER;
+BEGIN
+  CASE class OF
+    tokenKeyword: color := 15
+  | tokenNumber:  color := 3
+  | tokenString:  color := 11
+  | tokenComment: color := 7
+  ELSE color := 14 END;
+  RETURN color
+END GetTokenColor;
+
+(* Like T.PutString, but highlighted *)
+PROCEDURE PutStringH*(x, y: INTEGER; s: ARRAY OF CHAR;
+  bg, limit: INTEGER; VAR comLevel: INTEGER);
+VAR i, fg, class, len: INTEGER;
+BEGIN
+  IF limit = 0 THEN limit := T.charsX END; 
+  GetToken(s, 0, class, len, comLevel);
+  fg := GetTokenColor(class); i := 0;
+  WHILE (s[i] # 0X) & (x < limit) DO
+    T.PutChar(x, y, s[i], fg, bg);
+    INC(i); INC(x); DEC(len);
+    IF len <= 0 THEN
+      GetToken(s, i, class, len, comLevel);
+      fg := GetTokenColor(class)
+    END
+  END;
+  DEC(i);
+  WHILE (i >= 0) & (s[i] = ' ') DO
+    DEC(x);
+    T.PutChar(x, y, 0FAX, 3, bg);
+    DEC(i)
+  END
+END PutStringH;
+
+(* Put selected string (highlighted) *)
+PROCEDURE PutSelStringH*(x, y: INTEGER; s: ARRAY OF CHAR;
+  bg, limit: INTEGER; VAR comLevel: INTEGER;
+  fgSel, bgSel: INTEGER; x0, x1: INTEGER);
+VAR i, fg, class, len: INTEGER; sel: BOOLEAN;
+BEGIN
+  sel := FALSE;
+  GetToken(s, 0, class, len, comLevel);
+  fg := GetTokenColor(class); i := 0;
+  WHILE (s[i] # 0X) & (x < limit) DO
+    IF i = x1 + 1 THEN sel := FALSE
+    ELSIF i = x0 THEN sel := TRUE END;
+    IF sel THEN T.PutChar(x, y, s[i], fgSel, bgSel)
+    ELSE T.PutChar(x, y, s[i], fg, bg) END;
+    INC(i); INC(x); DEC(len);
+    IF len <= 0 THEN
+      GetToken(s, i, class, len, comLevel);
+      fg := GetTokenColor(class)
+    END
+  END
+END PutSelStringH;
+
+PROCEDURE TextChanged*(c: Editor): BOOLEAN;
+BEGIN
+  RETURN c.text.changed
+END TextChanged;
+
+PROCEDURE PrintText*(c: Editor);
+VAR L: Text.Line; scrY: INTEGER;
+  textY, x0, x1, minX, maxX, maxY, comLevel: INTEGER;
+  cursorHandled: BOOLEAN;
+BEGIN T.CharFill(c.x + 1, c.y + 1, c.w - 2, c.h - 2, ' ', 15, c.bg);
+  L := c.text.scrFirst; scrY := c.y + 1; textY := c.text.scrY;
+  minX := c.x + 1; maxX := c.x + c.w - 1;
+  maxY := c.y + c.h - 1; comLevel := L.comLevel;
+  cursorHandled := FALSE;
+  WHILE (L # NIL) & (scrY < maxY) DO
+    IF c.text.selected & (c.text.selT <= textY) & (textY <= c.text.selB) THEN
+      IF textY = c.text.selT THEN x0 := c.text.selL ELSE x0 := 0 END;
+      IF textY = c.text.selB THEN x1 := c.text.selR - 1 ELSE x1 := L.len END;
+      PutSelStringH(minX, scrY, L.s, c.bg, maxX, comLevel, 1, 7, x0, x1)
+    ELSE PutStringH(minX, scrY, L.s, c.bg, maxX, comLevel)
+    END;
+    IF (L = c.text.cur) & (c.text.x + 1 < T.charsX) &
+       ~OV.HasModalWindow(c.app) THEN
+      x0 := minX + c.text.x;
+      IF c.focused & (x0 <= maxX) THEN
+        T.ShowCursor(TRUE); T.GoToXY(x0, scrY);
+        cursorHandled := TRUE
+      END
+    END;
+    L := L.next; INC(scrY); INC(textY);
+    IF L # NIL THEN L.comLevel := comLevel END
+  END;
+  IF c.focused & ~cursorHandled THEN T.ShowCursor(FALSE) END
+END PrintText;
+
+PROCEDURE StartSelection(c: Editor);
+BEGIN c.text.selected := TRUE;
+  c.text.selL := c.text.x; c.text.selR := c.text.x;
+  c.text.selT := c.text.y; c.text.selB := c.text.y
+END StartSelection;
+
+PROCEDURE CheckSelBorders(c: Editor; VAR onLeft, onRight: BOOLEAN);
+BEGIN
+  onLeft := (c.text.x = c.text.selL) & (c.text.y = c.text.selT);
+  onRight := (c.text.x = c.text.selR) & (c.text.y = c.text.selB)
+END CheckSelBorders;
+
+PROCEDURE Swap(VAR a, b: INTEGER);
+VAR tmp: INTEGER;
+BEGIN
+  tmp := a; a := b; b := tmp
+END Swap;
+
+PROCEDURE Order(VAR a, b: INTEGER);
+VAR tmp: INTEGER;
+BEGIN
+  IF a > b THEN tmp := a; a := b; b := tmp END
+END Order;
+
+(* Starts, removes, upgrades selection if Shift held. *)
+(* dir is one of Direction of Selection constants. *)
+PROCEDURE HandleSelection(c: Editor; dir: INTEGER; viaHoriz: BOOLEAN);
+VAR mod: SET; onLeft, onRight: BOOLEAN;
+BEGIN
+  IF ~G.ShiftPressed() THEN c.text.selected := FALSE
+  ELSE CheckSelBorders(c, onLeft, onRight);
+    IF ~c.text.selected OR (~onLeft & ~onRight) THEN
+      StartSelection(c); (* Reset/new selection *)
+      CheckSelBorders(c, onLeft, onRight)
+    END;
+    CASE dir OF
+      dirRight:
+      IF onRight THEN INC(c.text.selR)
+      ELSIF onLeft THEN INC(c.text.selL) END
+    | dirLeft:
+      IF onLeft THEN DEC(c.text.selL)
+      ELSIF onRight THEN DEC(c.text.selR) END
+    | dirUp:
+      IF onLeft THEN
+        DEC(c.text.selT);
+        IF viaHoriz THEN c.text.selL := c.text.cur.prev.len
+        ELSIF c.text.selL > c.text.cur.prev.len
+        THEN c.text.selL := c.text.cur.prev.len END
+      ELSIF viaHoriz THEN
+        DEC(c.text.selB); c.text.selR := c.text.cur.prev.len
+      ELSIF (c.text.selB > c.text.selT + 1) OR
+         ((c.text.selB = c.text.selT + 1) &
+          (c.text.selR >= c.text.selL)) THEN
+        DEC(c.text.selB);
+        IF c.text.selR > c.text.cur.prev.len
+        THEN c.text.selR := c.text.cur.prev.len END
+      ELSIF c.text.selT = c.text.selB THEN
+        DEC(c.text.selT); Order(c.text.selR, c.text.selL);
+        IF c.text.selL > c.text.cur.prev.len
+        THEN c.text.selL := c.text.cur.prev.len END
+      ELSE
+        DEC(c.text.selB); Order(c.text.selL, c.text.selR);
+        IF c.text.selR > c.text.cur.prev.len
+        THEN c.text.selR := c.text.cur.prev.len END
+      END
+    | dirDown:
+      IF onRight THEN
+        INC(c.text.selB);
+        IF viaHoriz THEN c.text.selR := 0
+        ELSIF c.text.selR > c.text.cur.next.len
+        THEN c.text.selR := c.text.cur.next.len END
+      ELSIF viaHoriz THEN
+        INC(c.text.selT); c.text.selL := 0
+      ELSIF (c.text.selB > c.text.selT + 1) OR
+         ((c.text.selB = c.text.selT + 1) &
+          (c.text.selR >= c.text.selL)) THEN
+        INC(c.text.selT);
+        IF c.text.selL > c.text.cur.next.len
+        THEN c.text.selL := c.text.cur.next.len END
+      ELSIF c.text.selT = c.text.selB THEN
+        INC(c.text.selB); Swap(c.text.selL, c.text.selR);
+        IF c.text.selR > c.text.cur.next.len
+        THEN c.text.selR := c.text.cur.next.len END
+      ELSE
+        INC(c.text.selT); Swap(c.text.selL, c.text.selR);
+        IF c.text.selL > c.text.cur.next.len
+        THEN c.text.selL := c.text.cur.next.len END
+      END
+    ELSE
+    END;
+    IF ((c.text.selT >= c.text.selB) & (c.text.selL >= c.text.selR)) OR
+       (c.text.selT > c.text.selB) THEN
+      c.text.selected := FALSE
+    END
+  END
+END HandleSelection;
+
+PROCEDURE MoveScreenByLine(c: Editor; down: BOOLEAN);
+BEGIN
+  IF down THEN
+    ASSERT(c.text.scrFirst.next # NIL, 98);
+    c.text.scrFirst := c.text.scrFirst.next; INC(c.text.scrY)
+  ELSE (* Up *)
+    ASSERT(c.text.scrFirst.prev # NIL, 98); ASSERT(c.text.scrY > 0, 99);
+    c.text.scrFirst := c.text.scrFirst.prev; DEC(c.text.scrY)
+  END;
+  T.ResetCursorBlink
+END MoveScreenByLine;
+
+PROCEDURE MoveScreen(c: Editor; delta: INTEGER);
+BEGIN
+  IF delta > 0 THEN
+    WHILE (delta > 0) & (c.text.scrFirst.next # NIL) DO
+      MoveScreenByLine(c, TRUE); DEC(delta)
+    END
+  ELSE (* Up *)
+    WHILE (delta < 0) & (c.text.scrFirst.prev # NIL) DO
+      MoveScreenByLine(c, FALSE); INC(delta)
+    END
+  END
+END MoveScreen;
+
+(* Moves input cursor up and down *)
+PROCEDURE MoveByLine(c: Editor; down, viaHoriz: BOOLEAN);
+VAR moved: BOOLEAN;
+BEGIN
+  moved := FALSE;
+  IF down THEN
+    IF c.text.cur.next # NIL THEN
+      HandleSelection(c, dirDown, viaHoriz);
+      moved := TRUE;
+      c.text.cur := c.text.cur.next; INC(c.text.y);
+      IF c.text.y >= c.text.scrY + c.h - 2 THEN MoveScreenByLine(c, TRUE) END
+    END
+  ELSE (* Up *)
+    IF c.text.cur.prev # NIL THEN
+      HandleSelection(c, dirUp, viaHoriz);
+      moved := TRUE;
+      IF c.text.scrFirst = c.text.cur THEN MoveScreenByLine(c, FALSE) END;
+      c.text.cur := c.text.cur.prev; DEC(c.text.y)
+    END
+  END;
+  IF moved THEN
+    IF c.text.x > c.text.cur.len THEN c.text.x := c.text.cur.len END;
+    PrintText(c)
+  END;
+  T.ResetCursorBlink
+END MoveByLine;
+
+(* Moves input cursor up and down by page *)
+PROCEDURE MoveByPage(c: Editor; down: BOOLEAN);
+VAR i: INTEGER; moved: BOOLEAN;
+BEGIN
+  i := 1;
+  IF down THEN
+    moved := c.text.cur.next # NIL;
+    WHILE (i < c.h - 2) & (c.text.cur.next # NIL) DO
+      c.text.scrFirst := c.text.scrFirst.next; INC(c.text.scrY);
+      MoveByLine(c, TRUE, FALSE);
+      INC(i)
+    END
+  ELSE (* Up *)
+    moved := c.text.cur.prev # NIL;
+    WHILE (i < c.h - 2) & (c.text.cur.prev # NIL) DO
+      IF c.text.scrY > 0 THEN c.text.scrFirst := c.text.scrFirst.prev; DEC(c.text.scrY) END;
+      MoveByLine(c, FALSE, FALSE);
+      INC(i)
+    END
+  END;
+  T.ResetCursorBlink;
+  IF moved THEN PrintText(c) END
+END MoveByPage;
+
+(* Moves input cursor left and right by one char *)
+PROCEDURE MoveInLine(c: Editor; right: BOOLEAN);
+BEGIN
+  IF right THEN
+    IF c.text.x < c.text.cur.len THEN
+      HandleSelection(c, dirRight, FALSE); INC(c.text.x)
+    ELSIF c.text.cur.next # NIL THEN
+      MoveByLine(c, TRUE, TRUE);
+      c.text.x := 0;
+    END
+  ELSE (* Left *)
+    IF c.text.x > 0 THEN
+      HandleSelection(c, dirLeft, FALSE); DEC(c.text.x)
+    ELSIF c.text.cur.prev # NIL THEN
+      MoveByLine(c, FALSE, TRUE);
+      c.text.x := c.text.cur.len;
+    END
+  END;
+  T.ResetCursorBlink;
+  PrintText(c)
+END MoveInLine;
+
+(* Moves input cursor to start and to end *)
+PROCEDURE MoveToLineEdge(c: Editor; end: BOOLEAN);
+VAR onLeft, onRight: BOOLEAN;
+BEGIN
+  IF G.ShiftPressed() THEN
+    IF ~c.text.selected THEN StartSelection(c)
+    ELSE
+      CheckSelBorders(c, onLeft, onRight);
+      IF ~onLeft & ~onRight THEN StartSelection(c) END
+    END;
+    CheckSelBorders(c, onLeft, onRight);
+    IF end THEN
+      IF onRight THEN c.text.selR := c.text.cur.len
+      ELSE c.text.selL := c.text.cur.len END
+    ELSE (* Home *)
+      IF onRight THEN c.text.selR := 0
+      ELSE c.text.selL := 0 END
+    END;
+    IF c.text.selT = c.text.selB THEN Order(c.text.selL, c.text.selR) END
+  END;
+  IF end THEN
+    IF c.text.x < c.text.cur.len THEN c.text.x := c.text.cur.len END
+  ELSE (* Home *)
+    IF c.text.x > 0 THEN c.text.x := 0 END
+  END;
+  T.ResetCursorBlink;
+  PrintText(c)
+END MoveToLineEdge;
+
+PROCEDURE HandleBackspace(c: Editor);
+BEGIN
+  c.text.HandleBackspace;
+  PrintText(c)
+END HandleBackspace;
+
+PROCEDURE HandleDelete(c: Editor);
+BEGIN
+  c.text.HandleDelete;
+  PrintText(c)
+END HandleDelete;
+
+PROCEDURE HandleTab(e: Editor; shift: BOOLEAN);
+BEGIN
+  IF shift THEN e.text.RemoveIndent
+  ELSIF e.text.WholeLineSelected() THEN
+    IF shift THEN e.text.RemoveIndent
+    ELSE e.text.AddIndent
+    END
+  ELSE
+    IF e.text.selected THEN e.text.DeleteSelection END;
+    e.text.InsertChar(' ');
+    IF ODD(e.text.x MOD 2) THEN e.text.InsertChar(' ') END
+  END;
+  PrintText(e)
+END HandleTab;
+
+PROCEDURE HandleEnter(c: Editor);
+BEGIN
+  c.text.HandleEnter(TRUE);
+  IF c.text.y >= c.text.scrY + c.h - 2 THEN MoveScreenByLine(c, TRUE) END;
+  PrintText(c)
+END HandleEnter;
+
+PROCEDURE InitEditor*(c: Editor);
+BEGIN OV.InitWindow(c);
+  c.filename[0] := 0X;
+  c.bg := 1; c.resizable := TRUE; c.caption := 'NONAME';
+  c.text := Text.NewText();
+  c.do := editorMethod
+END InitEditor;
+
+PROCEDURE NewEditor*(): Editor;
+VAR w: Editor;
+BEGIN NEW(w); InitEditor(w); RETURN w
+END NewEditor;
+
+(* Standard Menu Hanlders *)
+
+PROCEDURE EditCut*(c: OV.Control);
+BEGIN
+  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
+    c.app.windows(Editor).text.CopySelection(clipboard);
+    c.app.windows(Editor).text.DeleteSelection
+  END
+END EditCut;
+
+PROCEDURE EditCopy*(c: OV.Control);
+BEGIN
+  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
+    c.app.windows(Editor).text.CopySelection(clipboard);
+    PrintText(c.app.windows(Editor))
+  END
+END EditCopy;
+
+PROCEDURE EditClear*(c: OV.Control);
+VAR e: OV.Control;
+BEGIN e := c.app.windows;
+  IF (e # NIL) & (e IS Editor) THEN
+    e(Editor).text.DeleteSelection;
+    PrintText(e(Editor))
+  END
+END EditClear;
+
+PROCEDURE EditPaste*(c: OV.Control);
+VAR e: Editor;
+BEGIN
+  IF (c.app.windows # NIL) & (c.app.windows IS Editor) &
+      (clipboard[0] # 0X) THEN
+    e := c.app.windows(Editor);
+    e.text.DeleteSelection;
+    e.text.Insert(clipboard, FALSE);
+    IF e.text.y >= e.text.scrY + e.h - 2 THEN
+      MoveScreen(e, e.text.y - e.h + 3 - e.text.scrY)
+    END;
+    PrintText(e)
+  END
+END EditPaste;
+
+PROCEDURE EditSelectAll*(c: OV.Control);
+BEGIN
+  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
+    c.app.windows(Editor).text.SelectAll;
+    PrintText(c.app.windows(Editor))
+  END
+END EditSelectAll;
+
+PROCEDURE EditUnselect*(c: OV.Control);
+BEGIN
+  IF (c.app.windows # NIL) & (c.app.windows IS Editor) THEN
+    c.app.windows(Editor).text.selected := FALSE;
+    PrintText(c.app.cur(Editor))
+  END
+END EditUnselect;
+
+(* EditorMethod *)
+
+PROCEDURE EditorDraw*(c: OV.Control; x, y: INTEGER);
+BEGIN OV.WindowDraw(c, x, y);
+  PrintText(c(Editor))
+END EditorDraw;
+
+PROCEDURE EditorMouseDown*(c: OV.Control; x, y, button: INTEGER);
+VAR t: Text.Text; L: Text.Line; i: INTEGER;
+BEGIN OV.WindowMouseDown(c, x, y, button);
+  IF (x > 0) & (x < c.w - 1) & (y > 0) & (y < c.h - 1) THEN
+    DEC(x); DEC(y); t := c(Editor).text;
+    t.selected := FALSE; L := t.scrFirst; i := y;
+    WHILE (i > 0) & (L # NIL) DO DEC(i); L := L.next END;
+    IF L # NIL THEN
+      t.cur := L; t.y := y + t.scrY;
+      IF x > L.len THEN x := L.len END;
+      t.x := x;
+      T.GoToXY(c.x + x + 1, c.y + y + 1)
+    END;
+    PrintText(c(Editor))
+  END
+END EditorMouseDown;
+
+PROCEDURE EditorMouseUp*(c: OV.Control; x, y, button: INTEGER);
+BEGIN
+  OV.WindowMouseUp(c, x, y, button)
+END EditorMouseUp;
+
+PROCEDURE EditorMouseMove*(c: OV.Control; x, y: INTEGER; buttons: SET);
+BEGIN
+  OV.WindowMouseMove(c, x, y, buttons)
+END EditorMouseMove;
+
+PROCEDURE EditorTextInput(c: OV.Control; s: ARRAY OF CHAR; sym: INTEGER);
+BEGIN
+  IF sym # 0 THEN c(Editor).text.InsertChar(CHR(sym)); T.ResetCursorBlink;
+    c(Editor).text.selected := FALSE; PrintText(c(Editor))
+  END
+END EditorTextInput;
+
+PROCEDURE EditorKeyDown*(c: OV.Control; key: G.Key);
+BEGIN
+  CASE key.code OF
+    G.kLeft:      MoveInLine(c(Editor), FALSE)
+  | G.kRight:     MoveInLine(c(Editor), TRUE)
+  | G.kUp:        MoveByLine(c(Editor), FALSE, FALSE)
+  | G.kDown:      MoveByLine(c(Editor), TRUE, FALSE)
+  | G.kHome:      MoveToLineEdge(c(Editor), FALSE)
+  | G.kEnd:       MoveToLineEdge(c(Editor), TRUE)
+  | G.kPgUp:      MoveByPage(c(Editor), FALSE)
+  | G.kPgDn:      MoveByPage(c(Editor), TRUE)
+  | G.kBackspace: HandleBackspace(c(Editor))
+  | G.kDel:       HandleDelete(c(Editor))
+  | G.kEnter, G.kEnterPad: HandleEnter(c(Editor))
+  | G.kTab:       HandleTab(c(Editor), key.mod * G.mShift # {})
+  ELSE
+  END
+END EditorKeyDown;
+
+PROCEDURE EditorGetFocus*(c: OV.Control);
+BEGIN
+  IF OV.windowMethod.getFocus # NIL THEN OV.windowMethod.getFocus(c) END;
+  G.StartTextInput; T.ShowCursor(TRUE)
+END EditorGetFocus;
+
+PROCEDURE EditorRefresh(c: OV.Control);
+BEGIN OV.WindowRefresh(c)
+END EditorRefresh;
+
+PROCEDURE InitEditorMethod*(m: OV.ControlMethod);
+BEGIN OV.InitWindowMethod(m);
+  m.getFocus := EditorGetFocus;
+  m.refresh := EditorRefresh;
+  m.draw := EditorDraw;
+  m.mouseDown := EditorMouseDown;
+  m.mouseUp := EditorMouseUp;
+  m.mouseMove := EditorMouseMove;
+  m.keyDown := EditorKeyDown;
+  m.textInput := EditorTextInput
+END InitEditorMethod;
+
+BEGIN
+  NEW(editorMethod); InitEditorMethod(editorMethod);
+  NEW(fileDialogMethod); InitFileDialogMethod(fileDialogMethod)
+END Editor.

+ 162 - 12
src/OV.Mod

@@ -16,9 +16,9 @@ GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 *)
-IMPORT G := Graph, T := Terminal, Strings, Out, SYSTEM;
+IMPORT G := Graph, T := Terminal, Strings, StrList, Out, SYSTEM;
 CONST
-  (* Control Statuses *)
+  (* ControlDesc.status possible values *)
   normal*   = 0;
   disabled* = 1;
   selected* = 2;
@@ -573,6 +573,7 @@ TYPE
     parent*, children*: Control;
     prev*, next*: Control;
     x*, y*, w*, h*: INTEGER;
+    default*: BOOLEAN;
     focused*: BOOLEAN;
     status*: INTEGER;
     caption*: ARRAY 256 OF CHAR;
@@ -619,6 +620,14 @@ TYPE
     pos*: INTEGER
   END;
 
+  ColumnSelection* = POINTER TO ColumnSelectionDesc;
+  ColumnSelectionDesc* = EXTENSIBLE RECORD(ControlDesc)
+    cols*: INTEGER; (* Column count *)
+    items*: StrList.List;
+    begin*: INTEGER; (* What index list begins with visually *)
+    cur*: INTEGER
+  END;
+
   Window* = POINTER TO WindowDesc;
   WindowDesc* = EXTENSIBLE RECORD(ControlDesc)
     cur*: Control;
@@ -628,7 +637,7 @@ TYPE
     closeBtn*, zoomBtn*: WinBtn;
     bg*: INTEGER;
     minW*, minH*: INTEGER;
-    mx*, my*, mw*, mh*: INTEGER (* To save x, y, w, h when maximized *)
+    mx*, my*, mw*, mh*: INTEGER (* To store x, y, w, h when maximized *)
   END;
 
   AppDesc* = EXTENSIBLE RECORD
@@ -648,6 +657,7 @@ VAR
   buttonMethod-: ControlMethod;
   winBtnMethod-: ControlMethod;
   editMethod-: ControlMethod;
+  columnSelectionMethod-: ControlMethod;
   windowMethod-: ControlMethod;
   menuMethod-: ControlMethod;
   quickBtnMethod-: ControlMethod;
@@ -740,6 +750,24 @@ END SetApp;
 
 (* Common *)
 
+(** fg2 is a color of marked letters, that are escaped with '&'. *)
+PROCEDURE PutMarkedString*(x, y: INTEGER; s: ARRAY OF CHAR; fg, fg2, bg, limit: INTEGER);
+VAR i, c: INTEGER;
+  mark: BOOLEAN;
+BEGIN
+  IF limit = 0 THEN limit := T.charsX END; 
+  i := 0; mark := FALSE;
+  WHILE (i < LEN(s)) & (s[i] # 0X) & (x <= limit) DO
+    IF s[i] = '&' THEN mark := TRUE
+    ELSE
+      IF mark THEN c := fg2; mark := FALSE ELSE c := fg END;
+      T.PutChar(x, y, s[i], c, bg);
+      INC(x)
+    END;
+    INC(i)
+  END
+END PutMarkedString;
+
 PROCEDURE DrawWindowBorder*(x, y, w, h, fg, bg: INTEGER;
   title: ARRAY OF CHAR; resizable, moving, inactive: BOOLEAN);
 VAR i, x2, y2, len: INTEGER; ch: CHAR;
@@ -811,8 +839,8 @@ PROCEDURE InitControl*(c: Control);
 BEGIN
   c.x := 0; c.y := 0; c.w := 0; c.h := 0;
   c.parent := NIL; c.children := NIL;
-  c.prev := NIL; c.next := NIL;
-  c.focused := FALSE; c.status := 0; c.caption[0] := 0X;
+  c.prev := NIL; c.next := NIL; c.default := FALSE; c.focused := FALSE;
+  c.status := 0; c.caption[0] := 0X;
   c.do := controlMethod
 END InitControl;
 
@@ -1313,7 +1341,7 @@ END NewButton;
 (* Button Method *)
 
 PROCEDURE ButtonDraw*(c: Control; x, y: INTEGER);
-VAR i, bg: INTEGER;
+VAR i, fg, bg: INTEGER;
 BEGIN INC(x, c.x); INC(y, c.y);
   IF c.status = selected THEN INC(x) END;
   IF (c.parent # NIL) & (c.parent IS Window) THEN bg := c.parent(Window).bg
@@ -1322,8 +1350,9 @@ BEGIN INC(x, c.x); INC(y, c.y);
   T.CharFill(x, y, c.w, c.h, ' ', 0, 2);
   i := 0; WHILE c.caption[i] # 0X DO INC(i) END;
   IF i > c.w THEN i := c.w END;
-  T.PutString(x + (c.w - i) DIV 2, y + c.h DIV 2,
-    c.caption, 0, 2, x + c.w - 1);
+  IF c.default THEN fg := 11 ELSE fg := 0 END;
+  PutMarkedString(x + (c.w - i) DIV 2, y + c.h DIV 2,
+    c.caption, fg, 14, 2, x + c.w - 1);
   IF c.status # selected THEN
     T.PutChar(x + c.w, y, 0DCX, 0, bg);
     T.CharFill(x + c.w, y + 1, 1, c.h - 1, 0DBX, 0, bg);
@@ -1461,13 +1490,15 @@ VAR i, j: INTEGER; e: Edit;
 BEGIN e := c(Edit); INC(x, e.x); INC(y, e.y);
   T.PutChar(x, y, ' ', 0, 1);
   i := 0; j := x + 1;
-  WHILE (e.caption[i] # 0X) & (i < e.w) DO
+  WHILE (e.caption[i] # 0X) & (i < e.w - 1) DO
     T.PutChar(j, y, e.caption[i], 15, 1); INC(i); INC(j)
   END;
-  WHILE i < e.w DO
+  WHILE i < e.w - 1 DO
     T.PutChar(j, y, ' ', 15, 1); INC(i); INC(j)
   END;
-  IF c.app.cur = c THEN T.GoToXY(x + e.pos + 1, y) END
+
+  i := e.pos + 1; IF i >= e.w THEN i := e.w - 1 END;
+  IF c.app.cur = c THEN T.GoToXY(x + i, y) END
 END EditDraw;
 
 PROCEDURE EditMouseDown*(c: Control; x, y, edit: INTEGER);
@@ -1513,6 +1544,124 @@ BEGIN InitControlMethod(m);
   m.textInput := EditTextInput
 END InitEditMethod;
 
+(* ColumnSelection *)
+
+PROCEDURE InitColumnSelection*(c: ColumnSelection);
+BEGIN InitControl(c); c.cols := 2; c.items := StrList.New();
+  c.w := 30; c.h := 6; c.caption[0] := 0X; c.cur := 0;
+  c.do := columnSelectionMethod
+END InitColumnSelection;
+
+PROCEDURE NewColumnSelection*(): ColumnSelection;
+VAR c: ColumnSelection;
+BEGIN NEW(c); InitColumnSelection(c); RETURN c
+END NewColumnSelection;
+
+(* ColumnSelection Method *)
+
+PROCEDURE ColumnSelectionDraw*(c: Control; x, y: INTEGER);
+VAR i, x2, y2, colw, fg, bg: INTEGER;
+  C: ColumnSelection;
+  s: ARRAY 256 OF CHAR;
+BEGIN C := c(ColumnSelection); INC(x, C.x); INC(y, C.y);
+  bg := 3; x2 := x + 1; y2 := y;
+  colw := (C.w - C.cols + 1) DIV C.cols;
+  T.CharFill(x, y, C.w, C.h - 1, ' ', 0, bg);
+
+  (* Scroll bars *)
+  T.CharFill(x + 1, y + C.h - 1, C.w - 1, 1, 0B1X, bg, 1);
+  T.PutChar(x, y + C.h - 1, 11X, bg, 1);
+  T.PutChar(x + C.w - 1, y + C.h - 1, 10X, bg, 1);
+  T.PutChar(x + 1, y + C.h - 1, 0FEX, bg, 1);
+
+  (* Column separators *)
+  i := x + colw;
+  WHILE i < x + C.w - 2 DO
+    T.CharFill(i, y, 1, C.h - 1, 0B3X, 1, bg); INC(i, colw + 1)
+  END;
+
+  StrList.First(C.items, s);
+  i := 0;
+  WHILE ~C.items.eol DO fg := 0; bg := 3;
+    IF i = C.cur THEN
+      IF C.status = selected THEN fg := 15; bg := 2;
+        T.CharFill(x2 - 1, y2, colw, 1, ' ', fg, bg)
+      ELSE fg := 14
+      END
+    END;
+    T.PutString(x2, y2, s, fg, bg, x2 + colw - 1);
+    IF y2 < y + C.h - 2 THEN INC(y2) ELSE y2 := y; INC(x2, colw + 1) END;
+    StrList.Next(C.items, s); INC(i)
+  END
+END ColumnSelectionDraw;
+
+PROCEDURE ColumnSelectionGetFocus*(c: Control);
+BEGIN ControlGetFocus(c); G.StartTextInput
+END ColumnSelectionGetFocus;
+
+PROCEDURE ColumnSelectionUpdateCur*(C: ColumnSelection; x, y: INTEGER);
+VAR oldCur, col, colw, count: INTEGER;
+BEGIN
+  IF (x < 0) OR (x >= C.w) THEN
+    (*!TODO scroll*)
+    (*IF ... THEN NeedRedraw(C.app) END*)
+  ELSIF y # C.h - 1 THEN oldCur := C.cur;
+    IF y < 0 THEN (* Scroll to top within column *)
+      C.cur := C.cur DIV (C.h - 1) * (C.h - 1)
+    ELSIF y >= C.h THEN (* Scroll to bottom within column *)
+      C.cur := C.cur DIV (C.h - 1) * (C.h - 1) + C.h - 2
+    ELSE
+      colw := (C.w - C.cols + 1) DIV C.cols;
+      col := x DIV (colw + 1);
+      C.cur := col * (C.h - 1) + y
+    END;
+    count := StrList.Count(C.items);
+    IF C.cur >= count THEN C.cur := count - 1 END;
+    IF C.cur # oldCur THEN NeedRedraw(C.app) END
+  END
+END ColumnSelectionUpdateCur;
+
+PROCEDURE ColumnSelectionMouseDown*(c: Control; x, y, button: INTEGER);
+VAR C: ColumnSelection;
+  col, colw: INTEGER;
+BEGIN C := c(ColumnSelection);
+  IF (c.parent # NIL) & (c.parent IS Window) THEN
+    c.status := selected;
+    c.app.dragged := c;
+    c.app.dragX := c.x + c.parent.x;
+    c.app.dragY := c.y + c.parent.y;
+    ColumnSelectionUpdateCur(C, x, y);
+    NeedRedraw(c.app)
+  END
+END ColumnSelectionMouseDown;
+
+PROCEDURE ColumnSelectionMouseUp*(c: Control; x, y, button: INTEGER);
+BEGIN
+  IF (c.status = selected) & (c.do.click # NIL) THEN c.do.click(c) END
+END ColumnSelectionMouseUp;
+
+PROCEDURE ColumnSelectionMouseMove*(c: Control; x, y: INTEGER; buttons: SET);
+VAR C: ColumnSelection;
+  oldCur, col, colw: INTEGER;
+BEGIN C := c(ColumnSelection);
+  IF buttons = {G.btnLeft} THEN ColumnSelectionUpdateCur(C, x, y) END
+END ColumnSelectionMouseMove;
+
+PROCEDURE ColumnSelectionClick*(c: Control);
+BEGIN
+  IF c.onClick # NIL THEN c.onClick(c) END
+END ColumnSelectionClick;
+
+PROCEDURE InitColumnSelectionMethod*(m: ControlMethod);
+BEGIN InitControlMethod(m);
+  m.draw := ColumnSelectionDraw;
+  m.getFocus := ColumnSelectionGetFocus;
+  m.mouseDown := ColumnSelectionMouseDown;
+  m.mouseUp := ColumnSelectionMouseUp;
+  m.mouseMove := ColumnSelectionMouseMove;
+  m.click := ColumnSelectionClick
+END InitColumnSelectionMethod;
+
 (* Standard Click Handlers *)
 
 PROCEDURE QuitApp*(c: Control);
@@ -1607,7 +1756,7 @@ BEGIN
 END HasModalWindow;
 
 PROCEDURE CenterWindow*(c: Window);
-BEGIN c.x := (T.charsX - c.w) DIV 2; c.y := (T.charsY - c.h) DIV 2 - 1;
+BEGIN c.x := (T.charsX - c.w) DIV 2; c.y := (T.charsY - c.h - 1) DIV 2;
   IF c.x < 0 THEN c.x := 0 END;
   IF c.y < 1 THEN c.y := 1 END
 END CenterWindow;
@@ -1993,6 +2142,7 @@ BEGIN
   NEW(buttonMethod); InitButtonMethod(buttonMethod);
   NEW(winBtnMethod); InitWinBtnMethod(winBtnMethod);
   NEW(editMethod); InitEditMethod(editMethod);
+  NEW(columnSelectionMethod); InitColumnSelectionMethod(columnSelectionMethod);
   NEW(windowMethod); InitWindowMethod(windowMethod);
   NEW(menuMethod); InitMenuMethod(menuMethod);
   NEW(quickBtnMethod); InitQuickBtnMethod(quickBtnMethod)

+ 63 - 0
src/StrList.Mod

@@ -0,0 +1,63 @@
+MODULE StrList;
+
+TYPE
+  Item = POINTER TO ItemDesc;
+  ItemDesc = RECORD
+    s: ARRAY 256 OF CHAR;
+    next: Item
+  END;
+
+  List* = POINTER TO ListDesc;
+  ListDesc* = RECORD
+    eol*: BOOLEAN;
+    first, last, cur: Item;
+    count: INTEGER
+  END;
+
+PROCEDURE Clear*(L: List);
+BEGIN L.first := NIL; L.last := NIL; L.cur := NIL;
+  L.count := 0; L.eol := FALSE
+END Clear;
+
+PROCEDURE New*(): List;
+VAR L: List;
+BEGIN NEW(L); Clear(L)
+RETURN L END New;
+
+PROCEDURE Append*(L: List; s: ARRAY OF CHAR);
+VAR p: Item;
+BEGIN NEW(p); p.s := s; p.next := NIL; INC(L.count);
+  IF L.count = 1 THEN L.first := p; L.last := p
+  ELSE L.last.next := p; L.last := p
+  END
+END Append;
+
+PROCEDURE Count*(L: List): INTEGER;
+RETURN L.count END Count;
+
+PROCEDURE GetCur(L: List; VAR s: ARRAY OF CHAR);
+VAR i: INTEGER; p: Item;
+BEGIN
+  IF L.cur # NIL THEN L.eol := FALSE; p := L.cur; i := 0;
+    WHILE (i < LEN(s) - 1) & (p.s[i] # 0X) DO s[i] := p.s[i]; INC(i) END;
+    s[i] := 0X
+  ELSE s[0] := 0X; L.eol := TRUE
+  END
+END GetCur;
+
+PROCEDURE First*(L: List; VAR s: ARRAY OF CHAR);
+BEGIN L.cur := L.first; GetCur(L, s)
+END First;
+
+PROCEDURE Next*(L: List; VAR s: ARRAY OF CHAR);
+BEGIN L.cur := L.cur.next; GetCur(L, s)
+END Next;
+
+PROCEDURE SetPos*(L: List; pos: INTEGER);
+VAR p: Item;
+BEGIN p := L.first;
+  WHILE (pos > 0) & (p # NIL) DO p := p.next; DEC(pos) END;
+  L.cur := p
+END SetPos;
+
+END StrList.

+ 1 - 3
src/Terminal.Mod

@@ -229,10 +229,9 @@ END PutChar;
 PROCEDURE PutString*(x, y: INTEGER; s: ARRAY OF CHAR; fg, bg, limit: INTEGER);
 VAR i: INTEGER;
 BEGIN
-  needRedraw := TRUE;
   IF limit = 0 THEN limit := charsX END; 
   i := 0;
-  WHILE (i < SHORT(LEN(s))) & (s[i] # 0X) & (x <= limit) DO
+  WHILE (i < LEN(s)) & (s[i] # 0X) & (x <= limit) DO
     PutChar(x, y, s[i], fg, bg);
     INC(i); INC(x)
   END
@@ -241,7 +240,6 @@ END PutString;
 PROCEDURE CharFill*(x, y, w, h: INTEGER; ch: CHAR; fg, bg: INTEGER);
 VAR X, Y: INTEGER;
 BEGIN
-  needRedraw := TRUE;
   FOR Y := y TO y + h - 1 DO
     FOR X := x TO x + w - 1 DO
       PutChar(X, Y, ch, fg, bg)

+ 24 - 14
src/make.bat

@@ -5,31 +5,41 @@ SET GCCDIR=C:\prg\mingw-w64\mingw64\bin
 SET PATH=%GCCDIR%;%OFRDIR%;%PATH%
 REM SET SDL2Opts=-w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2
 SET SDL2Opts=-lmingw32 -lSDL2main -lSDL2
-SET OFR=ofront+ -88 -C -s
+SET OFR=ofront+ -s -88
 SET OBERON=.;%OFRDIR%\Lib\Sym
 SET CC=gcc
 SET AR=ar
 SET CCFULL=%CC% -g3 -O0 -fno-exceptions -I %OFRDIR%\..\..\Mod\Lib -I %OFRDIR%\Lib\Obj
 
 ECHO ON
-%OFR% Config_win32.Mod
-%OFR% Int.Mod
-%OFR% -i SDL2.Mod
-%OFR% Graph.Mod
-%OFR% Terminal.Mod
-%OFR% Term.Mod
-%OFR% OV.Mod
-%OFR% EditorText.Mod
-%OFR% Editor.Mod
-%OFR% -m FreeOberon.Mod
+%OFR% -C Config_win32.Mod
+%OFR% -C Int.Mod
+%OFR% -7 StrList.Mod
+%OFR% -7 Dir.Mod
+%OFR% -C -i SDL2.Mod
+%OFR% -C Graph.Mod
+%OFR% -C Terminal.Mod
+%OFR% -C Term.Mod
+%OFR% -C OV.Mod
+%OFR% -C EditorText.Mod
+%OFR% -C Editor.Mod
+%OFR% -C -m FreeOberon.Mod
 
 windres resources.rc resources.o
 
-REM                        -O0 change to -Os (?)   add -s (?)
+@REM                        -O0 change to -Os (?)   add -s (?)
 
 %CCFULL% -c Int.c
+%CCFULL% -c StrList.c
+%CCFULL% -c Dir.c
 %CCFULL% -c SDL2.c
 %CCFULL% -c Graph.c
-%AR% -crs ..\data\bin\libFreeOberon.a Int.o SDL2.o Graph.o
+%AR% -crs ..\data\bin\libFreeOberon.a Int.o StrList.o Dir.o SDL2.o Graph.o
 
-%CCFULL% Config.c term\term_win32.c Int.o SDL2.o Graph.o Term.c Terminal.c OV.c EditorText.c Editor.c %PROG%.c resources.o -o ..\%PROG%.exe %OFRDIR%\Lib\Ofront.a %SDL2Opts% -lSDL2_image
+%CCFULL% Config.c term\term_win32.c ^
+  Int.o StrList.o Dir.o SDL2.o Graph.o ^
+  Term.c Terminal.c OV.c EditorText.c Editor.c ^
+  %PROG%.c -o ..\%PROG%.exe ^
+  resources.o ^
+  %OFRDIR%\Lib\Ofront.a ^
+  %SDL2Opts% -lSDL2_image