This is a step-by-step guide to writing applications with tvision-m2, a
Turbo-Vision / TVision style character-cell UI toolkit written in ISO GNU
Modula-2 (gm2 -fiso).
You can run the same program in a terminal (ANSI backend) or in a
graphics window (tigr backend); the toolkit code is identical, only the
Init/ReadEvent/Finish calls change.
The full reference is in API.md; this document teaches by example.
From the tvision-m2/ directory:
gm2 -fiso -c tvtty.def
gm2 -fiso -I. -c tv.mod
gm2 -fiso -I. -c tvfont.mod # only needed for the graphics backend
gm2 -fiso -I. -I../modula2 -c tvg.mod # idem
or simply make. To build a program:
gm2 -fiso -I. tv.o myapp.mod -o myapp # terminal
gm2 -fiso -I. tv.o tvg.o tvfont.o ../modula2/tigr.o \
../modula2/helper.o myapp.mod -o myapp -lGL -lX11 # graphics
Run it from a real terminal (or a graphical session for the tigr backend).
Every application has the same shape: initialise, build the views, then loop reading events and redrawing.
MODULE hello;
IMPORT tv;
VAR e : tv.Event; quit : BOOLEAN; st : tv.ViewPtr;
BEGIN
IF NOT tv.Init() THEN HALT END; (* raw mode + alternate screen *)
st := tv.NewView(tv.VText, 0, tv.Rows()-1, tv.Cols(), 1);
tv.SetText(st, " Hello - press Esc");
tv.AddView(tv.Desk(), st);
tv.Finish; (* draw + flush *)
quit := FALSE;
WHILE NOT quit DO
IF tv.Resized() THEN tv.Finish END; (* SIGWINCH / size change *)
IF tv.ReadEvent(e) THEN (* waits ~0.1 s *)
IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN
quit := TRUE
ELSE
IF tv.HandleEvent(e) = tv.cmClose THEN quit := TRUE END
END;
IF NOT quit THEN tv.Finish END
END
END;
tv.Done (* restore the terminal *)
END hello.
tv.HandleEvent(e) dispatches the event to the menu bar, the modal dialog
(if any) or the desktop, and returns a command (cmNone, cmClose, or
the tag of the control that fired). tv.Sender() tells you which view
produced it.
Everything on screen is a view. NewView(kind, x, y, w, h) creates one
at absolute screen coordinates; AddView(parent, child) attaches it. The
desktop (tv.Desk()) is the root; put your top-level windows in it.
VAR w, t, b : tv.ViewPtr;
w := tv.NewView(tv.VWindow, 3, 2, 50, 12);
tv.SetText(w, "My window");
tv.AddView(tv.Desk(), w);
t := tv.NewView(tv.VText, 5, 4, 20, 1);
tv.SetText(t, "Name:"); tv.AddView(w, t);
b := tv.NewView(tv.VButton, 5, 9, 10, 1);
tv.SetText(b, "OK"); tv.SetTag(b, tv.cmOK); tv.AddView(w, b);
■ handle in
the bottom-right corner. X closes them (HandleEvent returns
cmClose).BringToFront).PushClipRect internally).Useful calls: BringToFront(v), MoveViewBy(v,dx,dy), DelView(v),
WindowResize(w, ww, hh).
| kind | what it is | key fields |
|---|---|---|
VText |
static text | text |
VFrame |
single/double box | rect |
VInput |
one-line edit field | text, value |
VButton |
push button | text, tag |
VCheck |
check box | text, flags (vfSelected) |
VRadio |
radio button (self-exclusive) | text, flags |
VList |
list box | list, value |
VScroll |
vertical scroll bar | value, minv, maxv |
VEditor |
multi-line editor | ed |
Field access uses ^ because views are pointers: v^.text, v^.flags,
v^.value. Flags are bits: vfDisabled, vfSelected, vfFramed,
vfExpand.
Radio buttons are automatically exclusive among their siblings, so you do not manage the group yourself.
Tab / Shift-Tab (and up/down) move the keyboard focus through the
focusable widgets (input, button, check, radio, list, scroll, editor).
FocusView(v) sets it programmatically. The focused input gets the
hardware cursor.
Every control can carry a command tag (SetTag). When it is activated,
HandleEvent returns that tag:
cmd := tv.HandleEvent(e);
IF cmd = tv.cmClose THEN
IF tv.Sender() # NIL THEN tv.DelView(tv.Sender()) END
ELSIF cmd = tv.cmOK THEN
tv.Message("Info", "OK pressed")
END;
Use the built-ins cmOK, cmCancel, cmYes, cmNo, cmClose, or any
value >= 100 for your own commands.
Mouse handling is automatic: click a control to focus/press it, drag a
window title to move it, drag the ■ corner to resize, click list rows and
use the wheel to scroll. You only call HandleEvent.
To make a child grow/shrink with its window, set vfExpand:
edView^.flags := edView^.flags + tv.vfExpand;
CONST mQuit = 101; mAbout = 102;
tv.MenuBarInit;
tv.MenuAdd("File");
tv.MenuItemAdd("Open", 100);
tv.MenuSep;
tv.MenuItemAdd("Quit", mQuit);
tv.MenuAdd("Help");
tv.MenuItemAdd("About", mAbout);
The bar appears on row 0. Click a title (or press F10) to open the
drop-down; Left/Right switch menus, Up/Down move, Enter/Space or a click
selects, Esc or a click outside closes. Selecting an item makes
HandleEvent return its tag (Sender is NIL).
MessageBox(title, text, kind) with mbOK, mbOKCancel or mbYesNo
returns cmOK/cmCancel/cmYes/cmNo. Message(t, x) is the OK
shorthand.
For a form use the dialog builder:
VAR iName, cOn, cmd : INTEGER; s : ARRAY [0..127] OF CHAR;
...
tv.Dialog("Options", 44, 12);
iName := tv.DlgInput("Name:", "Modula-2"); (* -> field index *)
cOn := tv.DlgCheck("Enabled", TRUE);
tv.DlgRadio("Fast", TRUE); (* radios are exclusive *)
tv.DlgRadio("Safe", FALSE);
tv.DlgButton("OK", tv.cmOK);
tv.DlgButton("Cancel", tv.cmCancel);
cmd := tv.DlgRun; (* blocks *)
IF cmd = tv.cmOK THEN
tv.DlgTextAt(iName, s); (* read an input *)
IF tv.DlgValue(cOn) = 1 THEN ... END (* read a check/radio *)
END;
DlgRun returns the pressed button's tag, cmCancel on Esc, cmClose
from the close box.
Declare one EditorData buffer (a few tens of KB) and attach it to a
VEditor view:
VAR ed : tv.EditorData; edp : tv.EditorDataPtr; ev : tv.ViewPtr;
edp := CAST(tv.EditorDataPtr, ADR(ed));
IF NOT tv.EditorLoad(edp, "notes.txt") THEN
tv.EditorInit(edp);
tv.EditorAddLine(edp, "MODULE hello;")
END;
tv.EditorSetLanguage(edp, tv.langModula2); (* or langPascal / langOberon / langNone *)
tv.EditorSetIndent(edp, 2); (* auto-indent step, 0 = off *)
ev := tv.NewView(tv.VEditor, 2, 2, 60, 20);
ev^.flags := ev^.flags + tv.vfExpand;
tv.EditorAttach(ev, edp);
tv.AddView(win, ev);
tv.FocusView(ev);
...
IF tv.EditorSave(edp, "notes.txt") THEN ... END;
IF tv.EditorDirty(edp) THEN ... END; (* modified since load/save *)
The editor supports:
Ins), Enter, Backspace/Delete with line join,
arrows/Home/End/PgUp/PgDn, Tab (indent to a 4-column stop);(* *) / { } / // comments);begin -> BEGIN);BEGIN/THEN/DO/...; a line starting with END/UNTIL/ELSE
is dedented.See examples/edit.mod for a full-screen editor with File (New/Save/Quit),
Window (Size/Maximize) and Syntax menus.
Replace the terminal loop with the tigr backend; the rest of the program is unchanged.
IMPORT tv, tvg;
...
IF NOT tvg.InitGfx("my app", 90, 30) THEN HALT END; (* title, cols, rows *)
... build views on tv.Desk() exactly as before ...
tvg.Finish;
quit := FALSE;
WHILE NOT quit DO
IF tvg.ReadEvent(e) THEN
IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE
ELSE
cmd := tv.HandleEvent(e);
...
END;
IF NOT quit THEN tvg.Finish END
END;
IF tvg.Closed() THEN quit := TRUE END
END;
tvg.Done
tvg renders the cell buffer with a bundled 8x16 monospaced VGA font and
maps tigr keyboard/mouse to the same tv.Event, so menus, dialogs, the
editor and mouse dragging all work unchanged. Build it as shown in §1; see
examples/gfx.mod.
examples/edit.modA minimal but complete editor:
tv.Init, then a menu bar (File, Window, Syntax).VWindow filling the desktop minus the menu and status
rows.VEditor child sized to the window interior, flagged vfExpand, with
an EditorData loaded from tvision-edit.txt (or a sample).VText status bar on the last row.EditorSave,
EditorInit, WindowResize or switch the language; the window redraws
with tv.Finish.Run it from a terminal:
cd examples && make && ./edit
^: v^.rect, not v.rect.() in a .def
(PROCEDURE Cols() : INTEGER;).INTEGER compared with HIGH(array) needs VAL(INTEGER, HIGH(a)).BEGIN ... END block cannot be a CASE branch - use a helper.EXIT is only allowed inside a LOOP, not WHILE/REPEAT.CAST(tv.EditorDataPtr, ADR(x)).That is everything you need to build a Turbo-Vision style application.
Have a look at examples/ for complete programs and API.md for the full
reference.