2
0
Эх сурвалжийг харах

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

Arthur Yefimov 4 жил өмнө
parent
commit
cbf1c64d22

+ 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 CURDIR=%~dp0
 SET OFRDIR=%CURDIR%OfrontPlus\Target\Win64
 SET OFRDIR=%CURDIR%OfrontPlus\Target\Win64
 SET PATH=%OFRDIR%;%CURDIR%mingw32\bin;%PATH%
 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
 ECHO ON
 %OFR% %2 ..\Programs\%1
 %OFR% %2 ..\Programs\%1

+ 2 - 2
data/bin/compile.sh

@@ -8,8 +8,8 @@ cd bin
 
 
 OFRDIR="../data/bin/OfrontPlus/Target/Linux_amd64"
 OFRDIR="../data/bin/OfrontPlus/Target/Linux_amd64"
 PATH="$OFRDIR:$PATH"
 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
 $OFR $2 ../Programs/$1

+ 16 - 9
data/bin/link_console.bat

@@ -1,12 +1,15 @@
 @ECHO OFF
 @ECHO OFF
 REM This script is run by Free Oberon on Windows. Current directory of the
 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 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
 CD bin >nul 2>&1
 @DEL /s %~n1.exe >nul 2>&1
 @DEL /s %~n1.exe >nul 2>&1
 SET CURDIR=%~dp0
 SET CURDIR=%~dp0
 SET ONAME=%~n1
 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.
 REM Put all arguments starting from 2nd to ARGS.
 SHIFT
 SHIFT
@@ -18,13 +21,17 @@ SET ARGS=%ARGS% %1
 SHIFT
 SHIFT
 GOTO START
 GOTO START
 :FINISH
 :FINISH
-REM END Put all... ARGS.
-
+REM END Put all ARGS.
 ECHO ON
 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%
 @SET RETCODE=%ERRORLEVEL%
+
 @EXIT /b %RETCODE%
 @EXIT /b %RETCODE%

+ 1 - 0
data/bin/link_graph.bat

@@ -27,6 +27,7 @@ REM END Put all ARGS.
 ECHO ON
 ECHO ON
 
 
 %CC% -g3 -O0 -fno-exceptions ^
 %CC% -g3 -O0 -fno-exceptions ^
+  -I %CURDIR%..\..\src ^
   -I %OFRDIR%\..\..\Mod\Lib ^
   -I %OFRDIR%\..\..\Mod\Lib ^
   -I %OFRDIR%\Lib\Obj ^
   -I %OFRDIR%\Lib\Obj ^
   %ONAME%.c -o %ONAME%.exe ^
   %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
 You should have received a copy of the GNU General Public License
 along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 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
 CONST
-  (* Control Statuses *)
+  (* ControlDesc.status possible values *)
   normal*   = 0;
   normal*   = 0;
   disabled* = 1;
   disabled* = 1;
   selected* = 2;
   selected* = 2;
@@ -573,6 +573,7 @@ TYPE
     parent*, children*: Control;
     parent*, children*: Control;
     prev*, next*: Control;
     prev*, next*: Control;
     x*, y*, w*, h*: INTEGER;
     x*, y*, w*, h*: INTEGER;
+    default*: BOOLEAN;
     focused*: BOOLEAN;
     focused*: BOOLEAN;
     status*: INTEGER;
     status*: INTEGER;
     caption*: ARRAY 256 OF CHAR;
     caption*: ARRAY 256 OF CHAR;
@@ -619,6 +620,14 @@ TYPE
     pos*: INTEGER
     pos*: INTEGER
   END;
   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;
   Window* = POINTER TO WindowDesc;
   WindowDesc* = EXTENSIBLE RECORD(ControlDesc)
   WindowDesc* = EXTENSIBLE RECORD(ControlDesc)
     cur*: Control;
     cur*: Control;
@@ -628,7 +637,7 @@ TYPE
     closeBtn*, zoomBtn*: WinBtn;
     closeBtn*, zoomBtn*: WinBtn;
     bg*: INTEGER;
     bg*: INTEGER;
     minW*, minH*: 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;
   END;
 
 
   AppDesc* = EXTENSIBLE RECORD
   AppDesc* = EXTENSIBLE RECORD
@@ -648,6 +657,7 @@ VAR
   buttonMethod-: ControlMethod;
   buttonMethod-: ControlMethod;
   winBtnMethod-: ControlMethod;
   winBtnMethod-: ControlMethod;
   editMethod-: ControlMethod;
   editMethod-: ControlMethod;
+  columnSelectionMethod-: ControlMethod;
   windowMethod-: ControlMethod;
   windowMethod-: ControlMethod;
   menuMethod-: ControlMethod;
   menuMethod-: ControlMethod;
   quickBtnMethod-: ControlMethod;
   quickBtnMethod-: ControlMethod;
@@ -740,6 +750,24 @@ END SetApp;
 
 
 (* Common *)
 (* 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;
 PROCEDURE DrawWindowBorder*(x, y, w, h, fg, bg: INTEGER;
   title: ARRAY OF CHAR; resizable, moving, inactive: BOOLEAN);
   title: ARRAY OF CHAR; resizable, moving, inactive: BOOLEAN);
 VAR i, x2, y2, len: INTEGER; ch: CHAR;
 VAR i, x2, y2, len: INTEGER; ch: CHAR;
@@ -811,8 +839,8 @@ PROCEDURE InitControl*(c: Control);
 BEGIN
 BEGIN
   c.x := 0; c.y := 0; c.w := 0; c.h := 0;
   c.x := 0; c.y := 0; c.w := 0; c.h := 0;
   c.parent := NIL; c.children := NIL;
   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
   c.do := controlMethod
 END InitControl;
 END InitControl;
 
 
@@ -1313,7 +1341,7 @@ END NewButton;
 (* Button Method *)
 (* Button Method *)
 
 
 PROCEDURE ButtonDraw*(c: Control; x, y: INTEGER);
 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);
 BEGIN INC(x, c.x); INC(y, c.y);
   IF c.status = selected THEN INC(x) END;
   IF c.status = selected THEN INC(x) END;
   IF (c.parent # NIL) & (c.parent IS Window) THEN bg := c.parent(Window).bg
   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);
   T.CharFill(x, y, c.w, c.h, ' ', 0, 2);
   i := 0; WHILE c.caption[i] # 0X DO INC(i) END;
   i := 0; WHILE c.caption[i] # 0X DO INC(i) END;
   IF i > c.w THEN i := c.w 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
   IF c.status # selected THEN
     T.PutChar(x + c.w, y, 0DCX, 0, bg);
     T.PutChar(x + c.w, y, 0DCX, 0, bg);
     T.CharFill(x + c.w, y + 1, 1, c.h - 1, 0DBX, 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);
 BEGIN e := c(Edit); INC(x, e.x); INC(y, e.y);
   T.PutChar(x, y, ' ', 0, 1);
   T.PutChar(x, y, ' ', 0, 1);
   i := 0; j := x + 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)
     T.PutChar(j, y, e.caption[i], 15, 1); INC(i); INC(j)
   END;
   END;
-  WHILE i < e.w DO
+  WHILE i < e.w - 1 DO
     T.PutChar(j, y, ' ', 15, 1); INC(i); INC(j)
     T.PutChar(j, y, ' ', 15, 1); INC(i); INC(j)
   END;
   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;
 END EditDraw;
 
 
 PROCEDURE EditMouseDown*(c: Control; x, y, edit: INTEGER);
 PROCEDURE EditMouseDown*(c: Control; x, y, edit: INTEGER);
@@ -1513,6 +1544,124 @@ BEGIN InitControlMethod(m);
   m.textInput := EditTextInput
   m.textInput := EditTextInput
 END InitEditMethod;
 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 *)
 (* Standard Click Handlers *)
 
 
 PROCEDURE QuitApp*(c: Control);
 PROCEDURE QuitApp*(c: Control);
@@ -1607,7 +1756,7 @@ BEGIN
 END HasModalWindow;
 END HasModalWindow;
 
 
 PROCEDURE CenterWindow*(c: Window);
 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.x < 0 THEN c.x := 0 END;
   IF c.y < 1 THEN c.y := 1 END
   IF c.y < 1 THEN c.y := 1 END
 END CenterWindow;
 END CenterWindow;
@@ -1993,6 +2142,7 @@ BEGIN
   NEW(buttonMethod); InitButtonMethod(buttonMethod);
   NEW(buttonMethod); InitButtonMethod(buttonMethod);
   NEW(winBtnMethod); InitWinBtnMethod(winBtnMethod);
   NEW(winBtnMethod); InitWinBtnMethod(winBtnMethod);
   NEW(editMethod); InitEditMethod(editMethod);
   NEW(editMethod); InitEditMethod(editMethod);
+  NEW(columnSelectionMethod); InitColumnSelectionMethod(columnSelectionMethod);
   NEW(windowMethod); InitWindowMethod(windowMethod);
   NEW(windowMethod); InitWindowMethod(windowMethod);
   NEW(menuMethod); InitMenuMethod(menuMethod);
   NEW(menuMethod); InitMenuMethod(menuMethod);
   NEW(quickBtnMethod); InitQuickBtnMethod(quickBtnMethod)
   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);
 PROCEDURE PutString*(x, y: INTEGER; s: ARRAY OF CHAR; fg, bg, limit: INTEGER);
 VAR i: INTEGER;
 VAR i: INTEGER;
 BEGIN
 BEGIN
-  needRedraw := TRUE;
   IF limit = 0 THEN limit := charsX END; 
   IF limit = 0 THEN limit := charsX END; 
   i := 0;
   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);
     PutChar(x, y, s[i], fg, bg);
     INC(i); INC(x)
     INC(i); INC(x)
   END
   END
@@ -241,7 +240,6 @@ END PutString;
 PROCEDURE CharFill*(x, y, w, h: INTEGER; ch: CHAR; fg, bg: INTEGER);
 PROCEDURE CharFill*(x, y, w, h: INTEGER; ch: CHAR; fg, bg: INTEGER);
 VAR X, Y: INTEGER;
 VAR X, Y: INTEGER;
 BEGIN
 BEGIN
-  needRedraw := TRUE;
   FOR Y := y TO y + h - 1 DO
   FOR Y := y TO y + h - 1 DO
     FOR X := x TO x + w - 1 DO
     FOR X := x TO x + w - 1 DO
       PutChar(X, Y, ch, fg, bg)
       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%
 SET PATH=%GCCDIR%;%OFRDIR%;%PATH%
 REM SET SDL2Opts=-w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2
 REM SET SDL2Opts=-w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2
 SET SDL2Opts=-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 OBERON=.;%OFRDIR%\Lib\Sym
 SET CC=gcc
 SET CC=gcc
 SET AR=ar
 SET AR=ar
 SET CCFULL=%CC% -g3 -O0 -fno-exceptions -I %OFRDIR%\..\..\Mod\Lib -I %OFRDIR%\Lib\Obj
 SET CCFULL=%CC% -g3 -O0 -fno-exceptions -I %OFRDIR%\..\..\Mod\Lib -I %OFRDIR%\Lib\Obj
 
 
 ECHO ON
 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
 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 Int.c
+%CCFULL% -c StrList.c
+%CCFULL% -c Dir.c
 %CCFULL% -c SDL2.c
 %CCFULL% -c SDL2.c
 %CCFULL% -c Graph.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