# 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. ```modula2 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. ```modula2 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: ```modula2 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`: ```modula2 edView^.flags := edView^.flags + tv.vfExpand; ``` --- ## 7. Menus ```modula2 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: ```modula2 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: ```modula2 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. ```modula2 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.