TUTORIAL.md 10 KB

tvision-m2 - a complete tutorial

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.


1. Building the library

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).


2. The program skeleton

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.


3. Views and the view tree

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);
  • Windows draw a double-line frame and a title, clip their children and can be dragged by the title row and resized by the ■ handle in the bottom-right corner. X closes them (HandleEvent returns cmClose).
  • Overlapping windows have a z-order: clicking one brings it to the front (BringToFront).
  • Children are drawn after their parent; a window clips them (PushClipRect internally).

Useful calls: BringToFront(v), MoveViewBy(v,dx,dy), DelView(v), WindowResize(w, ww, hh).


4. The widget set

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.


5. Focus and commands

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.


6. Mouse

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;

7. Menus

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).


8. Dialogs and message boxes

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.


9. The editor

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:

  • insert/overwrite (Ins), Enter, Backspace/Delete with line join, arrows/Home/End/PgUp/PgDn, Tab (indent to a 4-column stop);
  • mouse: click to place the cursor, wheel to scroll;
  • a Ln/Col/Insert/Modified status row at the bottom;
  • syntax highlighting for Modula-2, Pascal and Oberon (keywords, strings, numbers, (* *) / { } / // comments);
  • keyword up-casing for Modula-2 and Oberon (begin -> BEGIN);
  • auto-indentation: Enter copies the leading spaces and adds a level after 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.


10. Running in a graphics window

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.


11. Walk-through: examples/edit.mod

A minimal but complete editor:

  1. tv.Init, then a menu bar (File, Window, Syntax).
  2. A full-screen VWindow filling the desktop minus the menu and status rows.
  3. A VEditor child sized to the window interior, flagged vfExpand, with an EditorData loaded from tvision-edit.txt (or a sample).
  4. A VText status bar on the last row.
  5. The event loop dispatches keys/mouse; menu commands call EditorSave, EditorInit, WindowResize or switch the language; the window redraws with tv.Finish.

Run it from a terminal:

cd examples && make && ./edit

12. GNU Modula-2 pitfalls

  • Pointer fields need ^: v^.rect, not v.rect.
  • Value-returning no-argument procedures need () in a .def (PROCEDURE Cols() : INTEGER;).
  • INTEGER compared with HIGH(array) needs VAL(INTEGER, HIGH(a)).
  • A BEGIN ... END block cannot be a CASE branch - use a helper.
  • EXIT is only allowed inside a LOOP, not WHILE/REPEAT.
  • Cast an address to a view/buffer pointer with 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.