Kaynağa Gözat

Add menu bar and drop-down menus to tvision-m2

New menu API: MenuBarInit / MenuAdd / MenuItemAdd / MenuSep / MenuBar.
The menu bar is drawn over the desktop and the selected item's tag is
returned by HandleEvent.

Interaction:
- click a title to open/close its drop-down;
- hovering another title while open switches menus;
- Up/Down move the highlight (separators skipped), Left/Right switch menus,
  Enter/Space or a click selects, Esc closes, a click outside closes;
- F10 opens/cycles menus from the keyboard.

The demo adds File (New, Open..., --, Quit), Edit (Cut, Copy, Paste) and
Help (About) and handles the resulting command tags. Docs updated.
Eric Streit 3 hafta önce
ebeveyn
işleme
e3f29ed2fc
7 değiştirilmiş dosya ile 305 ekleme ve 7 silme
  1. 6 1
      tvision-m2/README.md
  2. 8 2
      tvision-m2/SUMMARY.md
  3. BIN
      tvision-m2/demo
  4. 34 2
      tvision-m2/demo.mod
  5. 11 0
      tvision-m2/tv.def
  6. 246 2
      tvision-m2/tv.mod
  7. BIN
      tvision-m2/tv.o

+ 6 - 1
tvision-m2/README.md

@@ -37,6 +37,7 @@ or simply `make`.  Run it from a real terminal; `Esc` exits.
   one you click comes to the front.
 - Windows: double-line frame, title, ` X ` close box, draggable by the title
   bar with the mouse.
+- Menu bar + drop-down menus (mouse and keyboard).
 - Widgets: static text, single/double frame, input line (insert, backspace,
   delete, Home/End/arrows), push button, check box, radio buttons, list box
   with selection and wheel scrolling, vertical scroll bar.
@@ -48,11 +49,15 @@ or simply `make`.  Run it from a real terminal; `Esc` exits.
   click list rows, wheel-scroll lists.
 - Diffed redraw (only changed cells are written).
 - Modal dialog helper (`Message`), used by the demo's OK button.
+- Menu bar with drop-down menus: click a title (or press `F10`), navigate
+  with Left/Right/Up/Down, select with Enter/Space or a mouse click;
+  item separators (`MenuSep`) are supported.  The selected item's tag is
+  returned by `HandleEvent`.
 
 ## Roadmap ideas
 
-- Drop-down menus and a menu-bar widget.
 - Modal `Dialog` with arbitrary controls; form validation.
+- Menu item hotkeys (Alt+letter) and check/radio menu items.
 - Nested clipping groups and a desktop with many windows / tiling.
 - More widgets: multi-line editor, tabs, combo box, progress bar.
 - `SIGWINCH`-driven relayout (today the app polls the size).

+ 8 - 2
tvision-m2/SUMMARY.md

@@ -41,6 +41,9 @@ from the captured terminal stream).
 - Mouse: xterm SGR reporting (mode 1002/1006) - click to focus/press, drag
   titles, click list rows, wheel scroll.
 - Modal `Message` dialog with its own event loop.
+- Menu bar with drop-down menus: click or `F10` to open, Left/Right switch
+  menus, Up/Down move, Enter/Space or click selects (returns the item tag);
+  separators supported.
 
 ## Verified
 
@@ -49,12 +52,15 @@ Driving the demo through a pty and reconstructing the screen:
   status bar) renders correctly;
 - clicking `OK` with the mouse opens the modal Info box;
 - dragging the title moves the window in every direction and clamps at the
-  top-left so the title stays visible.
+  top-left so the title stays visible;
+- clicking the `File` title opens the drop-down (New/Open/Sep/Quit), `F10`
+  + Enter selects the first item (status shows the command tag), and
+  clicking `Quit` exits the program.
 
 ## Known limitations / next steps
 
-- No drop-down menus / menu-bar yet.
 - `Message` is the only dialog helper; no generic form dialog.
+- Menu items have no hotkeys; no check/radio menu items.
 - Windows are not resizable; no hover highlight.
 - Terminal size is polled (no `SIGWINCH`-driven relayout).
 - Nested clipping groups beyond windows are untested.

BIN
tvision-m2/demo


+ 34 - 2
tvision-m2/demo.mod

@@ -12,6 +12,14 @@ FROM InOut IMPORT WriteString, WriteLn;
 FROM SYSTEM IMPORT ADR, CAST;
 IMPORT tv;
 
+CONST
+    mNew   = 100;
+    mQuit  = 101;
+    mAbout = 102;
+    mCut   = 103;
+    mCopy  = 104;
+    mPaste = 105;
+
 VAR
     desk, w1, w2, status : tv.ViewPtr;
     input, check, r1, r2, listv, okb, closeb : tv.ViewPtr;
@@ -21,6 +29,7 @@ VAR
     quit : BOOLEAN;
     cmd : INTEGER;
     sender : tv.ViewPtr;
+    tmp, msgbuf : ARRAY [0..63] OF CHAR;
 
 BEGIN
     IF NOT tv.Init() THEN
@@ -29,9 +38,23 @@ BEGIN
     END;
     desk := tv.Desk();
 
+    (* menu bar *)
+    tv.MenuBarInit;
+    tv.MenuAdd("File");
+    tv.MenuItemAdd("New", mNew);
+    tv.MenuItemAdd("Open...", mNew);
+    tv.MenuSep;
+    tv.MenuItemAdd("Quit", mQuit);
+    tv.MenuAdd("Edit");
+    tv.MenuItemAdd("Cut", mCut);
+    tv.MenuItemAdd("Copy", mCopy);
+    tv.MenuItemAdd("Paste", mPaste);
+    tv.MenuAdd("Help");
+    tv.MenuItemAdd("About", mAbout);
+
     (* status bar (bottom line) *)
     status := tv.NewView(tv.VText, 0, tv.Rows() - 1, tv.Cols(), 1);
-    tv.SetText(status, " Tab: next   arrows: move   Enter: press   mouse: click/drag   Esc: quit");
+    tv.SetText(status, " F10/click: menu   Tab: next   Enter: press   mouse: drag titles   Esc: quit");
     tv.AddView(desk, status);
 
     (* ---- Editor window ---- *)
@@ -94,7 +117,16 @@ BEGIN
             ELSE
                 cmd := tv.HandleEvent(e);
                 sender := tv.Sender();
-                IF cmd = tv.cmClose THEN
+                IF cmd = mQuit THEN
+                    quit := TRUE
+                ELSIF cmd = mAbout THEN
+                    tv.Message("About", "tvision-m2 menus - GNU Modula-2")
+                ELSIF cmd >= 100 THEN
+                    tv.CopyText(msgbuf, "Menu command: ");
+                    tv.IntToText(tmp, cmd, 0);
+                    tv.AppendText(msgbuf, tmp);
+                    tv.SetText(status, msgbuf)
+                ELSIF cmd = tv.cmClose THEN
                     IF sender # NIL THEN tv.DelView(sender) END
                 ELSIF cmd = tv.cmOK THEN
                     tv.Message("Info", "You pressed OK.")

+ 11 - 0
tvision-m2/tv.def

@@ -143,6 +143,17 @@ PROCEDURE FocusView (v : ViewPtr);
 PROCEDURE FocusNext (backwards : BOOLEAN);
 PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
 
+(* Menu bar + drop-down menus.  Typical use:
+     MenuBarInit;
+     MenuAdd("File"); MenuItemAdd("New", cNew); MenuItemAdd("Quit", cQuit);
+     MenuAdd("Help"); MenuItemAdd("About", cAbout);
+   Selecting an item makes HandleEvent return its tag. *)
+PROCEDURE MenuBarInit;
+PROCEDURE MenuAdd (title : ARRAY OF CHAR);
+PROCEDURE MenuItemAdd (text : ARRAY OF CHAR; tag : INTEGER);
+PROCEDURE MenuSep;
+PROCEDURE MenuBar (show : BOOLEAN);
+
 (* Modal helper: shows a small centered box with the text and an OK button. *)
 PROCEDURE Message (title : ARRAY OF CHAR; text : ARRAY OF CHAR);
 

+ 246 - 2
tvision-m2/tv.mod

@@ -26,10 +26,24 @@ CONST
 
     vfPressed = 8;   (* internal, transient *)
 
+    MAXMENUS = 8;
+    MAXMITEMS = 16;
+
 TYPE
     Cell = RECORD cp : CARDINAL; attr : CARDINAL; END;
     BytePtr = POINTER TO BYTE;
 
+    MenuItem = RECORD
+                   text : ARRAY [0..47] OF CHAR;
+                   tag  : INTEGER;
+                   sep  : BOOLEAN;
+               END;
+    MenuDef = RECORD
+                  title : ARRAY [0..31] OF CHAR;
+                  items : ARRAY [0..MAXMITEMS - 1] OF MenuItem;
+                  count : INTEGER;
+              END;
+
 VAR
     cols, rows       : INTEGER;
     cells            : ARRAY [0..MAXCELLS - 1] OF Cell;
@@ -58,6 +72,12 @@ VAR
     focusList  : ARRAY [0..MAXVIEWS - 1] OF ViewPtr;
     focusCount : INTEGER;
 
+    menus     : ARRAY [0..MAXMENUS - 1] OF MenuDef;
+    menuCount : INTEGER;
+    menuOpen  : INTEGER;
+    menuSel   : INTEGER;
+    menuShown : BOOLEAN;
+
 (*========================================================================*)
 (*  libc struct helpers                                                    *)
 (*========================================================================*)
@@ -201,6 +221,7 @@ BEGIN
     cursorX := 0; cursorY := 0; cursorShown := FALSE;
     clipN := 0;
     modal := NIL; dragWin := NIL; lastSender := NIL;
+    menuCount := 0; menuOpen := -1; menuSel := 0; menuShown := FALSE;
     (* reset view pool *)
     FOR i := 0 TO MAXVIEWS - 1 DO used[i] := FALSE END;
     desk := NIL;
@@ -1034,12 +1055,231 @@ BEGIN
     END
 END DrawView;
 
+(*========================================================================*)
+(*  Menu bar + drop-down menus                                             *)
+(*========================================================================*)
+
+PROCEDURE MenuBarInit;
+BEGIN
+    menuCount := 0; menuOpen := -1; menuSel := 0; menuShown := FALSE
+END MenuBarInit;
+
+PROCEDURE MenuAdd (title : ARRAY OF CHAR);
+BEGIN
+    IF menuCount < MAXMENUS THEN
+        CopyText(menus[menuCount].title, title);
+        menus[menuCount].count := 0;
+        menuCount := menuCount + 1;
+        menuShown := TRUE
+    END
+END MenuAdd;
+
+PROCEDURE MenuItemAdd (text : ARRAY OF CHAR; tag : INTEGER);
+    VAR i : INTEGER;
+BEGIN
+    IF menuCount > 0 THEN
+        i := menus[menuCount - 1].count;
+        IF i < MAXMITEMS THEN
+            CopyText(menus[menuCount - 1].items[i].text, text);
+            menus[menuCount - 1].items[i].tag := tag;
+            menus[menuCount - 1].items[i].sep := FALSE;
+            menus[menuCount - 1].count := i + 1
+        END
+    END
+END MenuItemAdd;
+
+PROCEDURE MenuSep;
+    VAR i : INTEGER;
+BEGIN
+    IF menuCount > 0 THEN
+        i := menus[menuCount - 1].count;
+        IF i < MAXMITEMS THEN
+            menus[menuCount - 1].items[i].text[0] := 0C;
+            menus[menuCount - 1].items[i].tag := 0;
+            menus[menuCount - 1].items[i].sep := TRUE;
+            menus[menuCount - 1].count := i + 1
+        END
+    END
+END MenuSep;
+
+PROCEDURE MenuBar (show : BOOLEAN);
+BEGIN
+    menuShown := show;
+    IF NOT show THEN menuOpen := -1 END
+END MenuBar;
+
+PROCEDURE MenuTitleX (i : INTEGER) : INTEGER;
+    VAR k, x : INTEGER;
+BEGIN
+    x := 1;
+    FOR k := 0 TO i - 1 DO
+        x := x + TextLen(menus[k].title) + 2
+    END;
+    RETURN x
+END MenuTitleX;
+
+PROCEDURE MenuWidth (i : INTEGER) : INTEGER;
+    VAR k, w, l : INTEGER;
+BEGIN
+    w := TextLen(menus[i].title) + 2;
+    FOR k := 0 TO menus[i].count - 1 DO
+        l := TextLen(menus[i].items[k].text) + 4;
+        IF l > w THEN w := l END
+    END;
+    RETURN w
+END MenuWidth;
+
+PROCEDURE MenuTitleAt (x : INTEGER) : INTEGER;
+    VAR i, xx : INTEGER;
+BEGIN
+    FOR i := 0 TO menuCount - 1 DO
+        xx := MenuTitleX(i);
+        IF (x >= xx) AND (x < xx + TextLen(menus[i].title)) THEN RETURN i END
+    END;
+    RETURN -1
+END MenuTitleAt;
+
+PROCEDURE FirstSel (i : INTEGER) : INTEGER;
+    VAR k : INTEGER;
+BEGIN
+    k := 0;
+    WHILE (k < menus[i].count) AND menus[i].items[k].sep DO k := k + 1 END;
+    IF k >= menus[i].count THEN k := 0 END;
+    RETURN k
+END FirstSel;
+
+PROCEDURE MenuStep (i, cur, dir : INTEGER) : INTEGER;
+    VAR k, n : INTEGER; found : BOOLEAN;
+BEGIN
+    n := menus[i].count;
+    IF n = 0 THEN RETURN 0 END;
+    k := cur; found := FALSE;
+    WHILE NOT found DO
+        k := (k + dir + n) MOD n;
+        IF (NOT menus[i].items[k].sep) OR (k = cur) THEN found := TRUE END
+    END;
+    RETURN k
+END MenuStep;
+
+PROCEDURE DrawMenuBar;
+    VAR i, k, x : INTEGER; r : Rect; attr : Attr;
+        space : CARDINAL;
+BEGIN
+    IF (NOT menuShown) OR (menuCount = 0) THEN RETURN END;
+    space := VAL(CARDINAL, ORD(' '));
+    Fill(0, 0, cols, 1, space, A(Black, LightGray, FALSE));
+    FOR i := 0 TO menuCount - 1 DO
+        x := MenuTitleX(i);
+        IF menuOpen = i THEN attr := A(White, Blue, FALSE)
+        ELSE attr := A(Black, LightGray, FALSE) END;
+        DrawText(x, 0, menus[i].title, attr)
+    END;
+    IF menuOpen < 0 THEN RETURN END;
+    i := menuOpen;
+    r.x := MenuTitleX(i); r.y := 1;
+    r.w := MenuWidth(i); r.h := menus[i].count + 2;
+    DrawShadow(r, A(Black, Black, FALSE));
+    Fill(r.x, r.y, r.w, r.h, space, A(Black, LightGray, FALSE));
+    DrawBox(r, SingleFrame, A(Black, LightGray, FALSE));
+    FOR k := 0 TO menus[i].count - 1 DO
+        IF menus[i].items[k].sep THEN
+            DrawHLine(r.x + 1, r.y + 1 + k, r.w - 2, H_SINGLE,
+                      A(DarkGray, LightGray, FALSE))
+        ELSE
+            IF k = menuSel THEN attr := A(White, Blue, FALSE)
+            ELSE attr := A(Black, LightGray, FALSE) END;
+            Fill(r.x + 1, r.y + 1 + k, r.w - 2, 1, space, attr);
+            DrawText(r.x + 2, r.y + 1 + k, menus[i].items[k].text, attr)
+        END
+    END
+END DrawMenuBar;
+
+PROCEDURE MenuHandle (e : Event; VAR handled : BOOLEAN) : INTEGER;
+    VAR i, idx : INTEGER; r : Rect;
+BEGIN
+    handled := FALSE;
+    IF (NOT menuShown) OR (menuCount = 0) THEN RETURN cmNone END;
+
+    IF e.kind = evKey THEN
+        IF e.key = kCtrlC THEN RETURN cmNone END;
+        IF e.key = kF10 THEN
+            IF menuOpen < 0 THEN menuOpen := 0 ELSE menuOpen := (menuOpen + 1) MOD menuCount END;
+            menuSel := FirstSel(menuOpen);
+            handled := TRUE; RETURN cmNone
+        END;
+        IF menuOpen >= 0 THEN
+            handled := TRUE;
+            IF e.key = kEsc THEN menuOpen := -1
+            ELSIF e.key = kLeft THEN
+                menuOpen := (menuOpen + menuCount - 1) MOD menuCount;
+                menuSel := FirstSel(menuOpen)
+            ELSIF e.key = kRight THEN
+                menuOpen := (menuOpen + 1) MOD menuCount;
+                menuSel := FirstSel(menuOpen)
+            ELSIF e.key = kUp THEN menuSel := MenuStep(menuOpen, menuSel, -1)
+            ELSIF e.key = kDown THEN menuSel := MenuStep(menuOpen, menuSel, +1)
+            ELSIF (e.key = kEnter) OR (e.key = kSpace) THEN
+                IF NOT menus[menuOpen].items[menuSel].sep THEN
+                    i := menus[menuOpen].items[menuSel].tag;
+                    menuOpen := -1; lastSender := NIL;
+                    RETURN i
+                END
+            END;
+            RETURN cmNone
+        END;
+        RETURN cmNone
+    END;
+
+    IF e.kind # evMouse THEN RETURN cmNone END;
+
+    (* mouse over a title *)
+    IF e.my = 0 THEN
+        i := MenuTitleAt(e.mx);
+        IF i >= 0 THEN
+            handled := TRUE;
+            IF e.mpressed THEN
+                IF menuOpen = i THEN menuOpen := -1
+                ELSE menuOpen := i; menuSel := FirstSel(i) END
+            ELSIF (e.mbtn > 0) OR (e.mwheel # 0) THEN
+                menuOpen := i; menuSel := FirstSel(i)
+            ELSIF menuOpen >= 0 THEN
+                menuOpen := i; menuSel := FirstSel(i)
+            END;
+            RETURN cmNone
+        END
+    END;
+
+    IF menuOpen >= 0 THEN
+        r.x := MenuTitleX(menuOpen); r.y := 1;
+        r.w := MenuWidth(menuOpen); r.h := menus[menuOpen].count + 2;
+        IF Contains(r, e.mx, e.my) THEN
+            handled := TRUE;
+            idx := e.my - (r.y + 1);
+            IF (idx >= 0) AND (idx < menus[menuOpen].count) AND
+               (NOT menus[menuOpen].items[idx].sep) THEN
+                menuSel := idx;
+                IF e.mpressed THEN
+                    i := menus[menuOpen].items[idx].tag;
+                    menuOpen := -1; lastSender := NIL;
+                    RETURN i
+                END
+            END;
+            RETURN cmNone
+        ELSIF e.mpressed THEN
+            menuOpen := -1;
+            handled := TRUE
+        END
+    END;
+    RETURN cmNone
+END MenuHandle;
+
 PROCEDURE DrawTree;
 BEGIN
     Clear(A(LightGray, Blue, FALSE));
     HideCursor;
     clipN := 0;
-    IF desk # NIL THEN DrawView(desk) END
+    IF desk # NIL THEN DrawView(desk) END;
+    DrawMenuBar
 END DrawTree;
 
 PROCEDURE Finish;
@@ -1286,9 +1526,13 @@ BEGIN
 END DispatchKey;
 
 PROCEDURE HandleEvent (e : Event) : INTEGER;
-    VAR cmd : INTEGER; root : ViewPtr;
+    VAR cmd : INTEGER; root : ViewPtr; handled : BOOLEAN;
 BEGIN
     IF e.kind = evNone THEN RETURN cmNone END;
+    IF modal = NIL THEN
+        cmd := MenuHandle(e, handled);
+        IF handled THEN RETURN cmd END
+    END;
     root := modal;
     IF root = NIL THEN root := desk END;
     IF root = NIL THEN RETURN cmNone END;

BIN
tvision-m2/tv.o