|
|
@@ -1,74 +1,140 @@
|
|
|
-# tvision-m2 - tutorial
|
|
|
+# tvision-m2 - a complete tutorial
|
|
|
|
|
|
-Build the library once:
|
|
|
+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:
|
|
|
|
|
|
- cd tvision-m2
|
|
|
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
|
|
|
|
|
|
-Each program is then:
|
|
|
+Run it from a real terminal (or a graphical session for the tigr backend).
|
|
|
|
|
|
- gm2 -fiso -I. tv.o myapp.mod -o myapp
|
|
|
- ./myapp # run from a real terminal
|
|
|
+---
|
|
|
|
|
|
-## 1. The smallest program
|
|
|
+## 2. The program skeleton
|
|
|
|
|
|
-An empty blue desktop with a status line; `Esc` quits.
|
|
|
+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;
|
|
|
+ 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;
|
|
|
+ tv.Finish; (* draw + flush *)
|
|
|
+
|
|
|
quit := FALSE;
|
|
|
WHILE NOT quit DO
|
|
|
- IF tv.Resized() THEN tv.Finish END;
|
|
|
- IF tv.ReadEvent(e) THEN
|
|
|
- IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE END;
|
|
|
+ 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
|
|
|
+ tv.Done (* restore the terminal *)
|
|
|
END hello.
|
|
|
```
|
|
|
|
|
|
-The shape of every program: `Init`; add views to `Desk()`; loop calling
|
|
|
-`Resized` and `ReadEvent`; redraw with `Finish`; `Done` on exit.
|
|
|
+`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.
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-## 2. A window with widgets
|
|
|
+## 3. Views and the view tree
|
|
|
|
|
|
-Views use absolute screen coordinates. `NewView(kind, x, y, w, h)` creates
|
|
|
-one; `AddView(parent, child)` attaches 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.
|
|
|
|
|
|
```modula2
|
|
|
+VAR w, t, b : tv.ViewPtr;
|
|
|
w := tv.NewView(tv.VWindow, 3, 2, 50, 12);
|
|
|
-tv.SetText(w, "Editor"); (* window title *)
|
|
|
+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);
|
|
|
|
|
|
-input := tv.NewView(tv.VInput, 12, 4, 34, 1);
|
|
|
-tv.SetText(input, "Modula-2"); tv.AddView(w, input);
|
|
|
-
|
|
|
-b := tv.NewView(tv.VButton, 12, 10, 8, 1);
|
|
|
+b := tv.NewView(tv.VButton, 5, 9, 10, 1);
|
|
|
tv.SetText(b, "OK"); tv.SetTag(b, tv.cmOK); tv.AddView(w, b);
|
|
|
-
|
|
|
-tv.AddView(tv.Desk(), w);
|
|
|
-tv.FocusView(input);
|
|
|
```
|
|
|
|
|
|
-A `VWindow` clips its children, can be dragged by its title row and closed
|
|
|
-with the ` X ` box. `Tab`/arrows move focus; inputs are edited with the
|
|
|
-usual keys.
|
|
|
+- **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).
|
|
|
|
|
|
-## 3. Reacting to commands
|
|
|
+Useful calls: `BringToFront(v)`, `MoveViewBy(v,dx,dy)`, `DelView(v)`,
|
|
|
+`WindowResize(w, ww, hh)`.
|
|
|
|
|
|
-`HandleEvent` returns `cmNone`, `cmClose`, or the tag you gave a control:
|
|
|
+---
|
|
|
+
|
|
|
+## 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);
|
|
|
@@ -79,13 +145,29 @@ ELSIF cmd = tv.cmOK THEN
|
|
|
END;
|
|
|
```
|
|
|
|
|
|
-Use custom tags >= 100 to distinguish your own buttons.
|
|
|
+Use the built-ins `cmOK`, `cmCancel`, `cmYes`, `cmNo`, `cmClose`, or any
|
|
|
+value >= 100 for your own commands.
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-## 4. Menus
|
|
|
+## 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
|
|
|
-CONST mQuit = 101; mAbout = 102;
|
|
|
+edView^.flags := edView^.flags + tv.vfExpand;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
+## 7. Menus
|
|
|
+
|
|
|
+```modula2
|
|
|
+CONST mQuit = 101; mAbout = 102;
|
|
|
tv.MenuBarInit;
|
|
|
tv.MenuAdd("File");
|
|
|
tv.MenuItemAdd("Open", 100);
|
|
|
@@ -95,109 +177,149 @@ tv.MenuAdd("Help");
|
|
|
tv.MenuItemAdd("About", mAbout);
|
|
|
```
|
|
|
|
|
|
-The bar appears on row 0. Clicking a title opens the drop-down; `F10`,
|
|
|
-arrows, Enter and Esc work too. Selecting an item makes `HandleEvent`
|
|
|
-return its tag:
|
|
|
+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`).
|
|
|
|
|
|
-```modula2
|
|
|
-IF cmd = mQuit THEN quit := TRUE
|
|
|
-ELSIF cmd = mAbout THEN
|
|
|
- IF tv.MessageBox("About", "my app", tv.mbOK) = tv.cmOK THEN END
|
|
|
-END;
|
|
|
-```
|
|
|
+---
|
|
|
+
|
|
|
+## 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.
|
|
|
|
|
|
-## 5. Dialogs
|
|
|
+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");
|
|
|
+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 until a button / Esc *)
|
|
|
+cmd := tv.DlgRun; (* blocks *)
|
|
|
IF cmd = tv.cmOK THEN
|
|
|
- tv.DlgTextAt(iName, s); (* read the field *)
|
|
|
- IF tv.DlgValue(cOn) = 1 THEN tv.Message("Info", "Enabled") END
|
|
|
+ tv.DlgTextAt(iName, s); (* read an input *)
|
|
|
+ IF tv.DlgValue(cOn) = 1 THEN ... END (* read a check/radio *)
|
|
|
END;
|
|
|
```
|
|
|
|
|
|
-Radios added with `DlgRadio` are automatically exclusive. For simple
|
|
|
-"notice" dialogs use `Message` / `MessageBox`.
|
|
|
+`DlgRun` returns the pressed button's tag, `cmCancel` on Esc, `cmClose`
|
|
|
+from the close box.
|
|
|
|
|
|
-## 6. Lists and mouse
|
|
|
+---
|
|
|
|
|
|
-`VList` shows a caller-owned `ListData`:
|
|
|
-
|
|
|
-```modula2
|
|
|
-VAR data : tv.ListData;
|
|
|
-data.items[0] := "Alpha"; data.items[1] := "Bravo"; data.count := 2;
|
|
|
-list := tv.NewView(tv.VList, 5, 4, 20, 6);
|
|
|
-list^.list := CAST(tv.ListDataPtr, ADR(data));
|
|
|
-tv.AddView(w, list);
|
|
|
-```
|
|
|
+## 9. The editor
|
|
|
|
|
|
-The mouse works automatically: click to focus/press buttons, drag window
|
|
|
-titles, click list rows and scroll with the wheel. You only need
|
|
|
-`HandleEvent`; the toolkit does the hit-testing.
|
|
|
-
|
|
|
-## 7. A text 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, "Type here...")
|
|
|
+ tv.EditorInit(edp);
|
|
|
+ tv.EditorAddLine(edp, "MODULE hello;")
|
|
|
END;
|
|
|
-tv.EditorSetLanguage(edp, tv.langModula2); (* or langPascal / langOberon *)
|
|
|
-tv.EditorSetIndent(edp, 2); (* auto-indent: 2 spaces per level; 0 = off *)
|
|
|
+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 *)
|
|
|
```
|
|
|
|
|
|
-See `examples/edit.mod` for a full-screen editor with a File menu.
|
|
|
+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.
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-## 8. Running in a graphics window (tigr)
|
|
|
+## 10. Running in a graphics window
|
|
|
|
|
|
-`tvg` is a drop-in replacement for the terminal loop:
|
|
|
+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;
|
|
|
-tv.MenuBarInit; ... build views on tv.Desk() ...
|
|
|
+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
|
|
|
- cmd := tv.HandleEvent(e); (* same events, same commands *)
|
|
|
- IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE END;
|
|
|
- tvg.Finish
|
|
|
+ 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
|
|
|
```
|
|
|
|
|
|
-Build it with `make tvg.o` then `make -C examples gfx`; see `examples/gfx.mod`.
|
|
|
-The toolkit code (windows, widgets, menus, dialogs) is identical.
|
|
|
+`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`.
|
|
|
|
|
|
-## 9. More examples
|
|
|
+---
|
|
|
|
|
|
-See `examples/` (`empty`, `window`, `dialog`, `menus`) and build them with
|
|
|
-`make`. See `API.md` for the full reference.
|
|
|
+## 11. Walk-through: `examples/edit.mod`
|
|
|
|
|
|
-## Pitfalls (GNU Modula-2)
|
|
|
+A minimal but complete editor:
|
|
|
|
|
|
-- Pointer fields need `^` (`v^.rect`).
|
|
|
-- Value-returning no-arg procedures need `()` in a `.def`
|
|
|
- (`Cols() : INTEGER`).
|
|
|
+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.
|
|
|
-- `Present` is diffed; if you draw outside the loop (e.g. before `Init`),
|
|
|
- the cells do not exist yet.
|
|
|
+- `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.
|