z80:Menu Routines

From Learn @ Cemetech
Jump to: navigation, search

Basics

A useful method of having a program interacting with the user is through a menu. However, menu's fall under a broad category. So what is a menu? A menu must have:

  • List(s) of possible actions that the user can perform
  • A method of selecting items from the list(s)

Here's a chunk of code that will create a simple menu with a header(Test Menu), 4 items (A,B,C, and Quit), and allow the user to select an item. All the other optional stuff will be added later.


   ;Hard-coded menu routine
   ;
   ;inputs: none
   ;
   ;outputs: menu
   ;
   ;destroyed: all
   ;
   
   menuStart:		;Start of menu routine
   
    bcall(_ClrLCDFull)		;Display the Header
    ld hl,txtHeader
    call dispHeader
   
    ld hl,txtItem1			;Display Items
    call dispItem
    ld hl,txtItem2
    call dispItem
    ld hl,txtItem3
    call dispItem
    ld hl,txtItemQuit
    call dispItem
    
   menuLoop:		;Scan key loop to get user input
   
    bcall(_GetKey)
   
    cp k1			;If user pressed 1, do action 1
    jr z,item1
   
    cp k2			;If user pressed 2, do action 2
    jr z,item2
   
    cp k3			;If user pressed 3, do action 3
    jr z,item3
   
    cp k4			;If user pressed 4, quit
    jr z,quit
   
    jr menuLoop		;Else, invalid input. Wait for user to input new key
   
   item1:			;Action 1
   
    bcall(_ClrLCDFull)
   
    ld bc,0
    ld (curRow),bc
   
    ld hl,txtSelect1
    bcall(_PutS)
   
    bcall(_GetKey)
   
    jr menuStart
   
   item2:			;Action 2
   
    bcall(_ClrLCDFull)
   
    ld bc,0
    ld (curRow),bc
   
    ld hl,txtSelect2
   
    bcall(_PutS)
    bcall(_GetKey)
   
    jr menuStart
   
   
   item3:			;Action 3
   
    bcall(_ClrLCDFull)
   
    ld bc,0
    ld (curRow),bc
   
    ld hl,txtSelect3
    bcall(_PutS)
   
    bcall(_GetKey)
   
    jr menuStart
   
   quit:			;quit the program
   
    bcall(_ClrLCDFull)
   
    ret
    
   ;dispHeader
   ;
   ;displays the header centered and at the top
   ;
   ;Inputs: HL points to null-terminating string
   ;
   ;Output: text displayed to string, top center right and inverse text 
   ;
   ;Destroyed: bc,hl
   ;
   ;Note: text string must be large text and take up the full line
   ;(use blank spaces to fill in gaps)
   ;
   
   dispHeader:				;displays the header centered and at the top
   
    ld bc,$0
    ld (curRow),bc
   
    set textInverse,(IY+TextFlags)
    bcall(_PutS)
    res textInverse,(IY+TextFlags)
   
    ld hl,0
    ld (curRow),hl
   
    ret
    
   ;dispItem
   ;
   ;displays menu items at the start of the next line
   ;
   ;Inputs: HL points to null-terminating string
   ;
   ;Output: text displayed
   ;
   ;Destroyed: all
   ;
   
   dispItem:				;displays menu items at the start of the next line
   
    push hl
    bcall(_NewLine)
    pop hl
   
    bcall(_PutS)
   
    ret
    
   ;========================================
   ;data
   ;========================================
    
   txtHeader:
    .db "   Test  Menu   ",0
    
   txtItem1:
    .db "1:A",0
    
   txtItem2:
    .db "2:B",0
    
   txtItem3:
    .db "3:C",0
    
   txtSelect1:
    .db "You have selected A",0
    
   txtSelect2:
    .db "You have selected B",0
    
   txtSelect3:
    .db "You have selected C",0
   
   txtItemQuit:
    .db "4:Quit",0


Hopefully from the code you'll be able to understand the general flow. First, the program runs menuStart, which displays the menu header and items to the display with the sub-routines dispHeader and dispItem. Once the menu has been displayed, wait for a user input. If the user presses "1", do action 1 (display "You have selected A") If the user presses "2", do action 2 If the user presses "3", do action 3 If the user presses "4", quit

For a simple menu, this isn't too bad. However, it's bland, and void of features. So, let's add some features.

Cursor

To give the user some convenience, we'll add a cursor. The cursor allows the user to input up or down and use enter to select an item besides just pressing the corresponding number.

To do so, we'll need some code that will draw the cursor:


   ;curDraw
   ;
   ;Draws the cursor
   ;
   ;Inputs: C holds the highlighted item
   ;
   ;Outputs: Cursor displayed
   ;
   ;Destroyed: A
   ;
   
   curDraw:
    set textInverse,(IY+TextFlags)	;set inverse text
    
    xor a
    ld (curCol),a
    ld a,c
    ld (curRow),a
    
    add a,$30				;Character offset
    
    bcall(_PutC)
    
    ld a,':'
    bcall(_PutC)
   
    res textInverse,(IY+TextFlags)
    ret


And, we'll also need some code to erase the cursor:


   ;curErase
   ;
   ;Erase the cursor
   ;
   ;Inputs: C highlighted item
   ;
   ;Ouputs: Cursor erased
   ;
   ;Destroyed: A
   ;
   
   curErase:
    xor a
    ld (curCol),a
    ld a,c
    ld (curRow),a
    
    add a,$30				;Character offset
    
    bcall(_PutC)
    
    ld a,':'
    bcall(_PutC)
   
    ret


What happens if the user presses up/down/enter? We'll need to add code to deal with the new key presses.


   ;Updated menuloop
   ;Stuff with asterisks are new
   
   *ld c,1*			;add this to menuStart to set initial cursor location
   
   menuLoop:		;Scan key loop to get user input
   
   *call curDraw*
   *push bc*			;save C for later
   
    bcall(_GetKey)
   
   *pop bc*			;we'll need this for some routines with the new keypresses
   
   *cp kup*
   *jr z, mUp*
   
   *cp kdown*
   *jr z, mDown*
   
   *cp kenter*
   *jr z, selection*
   
    cp k1			;If user pressed 1, do action 1
    jr z,item1
   
    cp k2			;If user pressed 2, do action 2
    jr z,item2
   
    cp k3			;If user pressed 3, do action 3
    jr z,item3
   
    cp k4			;If user pressed 4, quit
    jr z,quit
   
    jr menuLoop		;Else, invalid input. Wait for user to input new key


Move the cursor up:


   ;mUp
   ;
   ;moves cursor up
   ;
   ;inputs: C highlighted item
   ;
   ;Ouputs: updated cursor place stored in C
   ;
   ;Destroyed: A
   ;
   ;Notes: still need to call curDraw to re-draw the cursor
   ;
   
   mUp:
   
    call curErase			;erase the cursor
   
    ld a,c				;check if the cursor is out of bounds
    cp 1
    jr z,menuLoop
   
    dec c				;if not, decrease and return
    jr menuLoop


...And, down:


   ;mDown
   ;
   ;moves cursor down
   ;
   ;Inputs: C highlighted item
   ;
   ;Outputs: updated cursor place sctored in C
   ;
   ;Destroyed:
   ;
   ;Notes: still need to call curDraw to re-draw the cursor
   ;
   
   mDown:
   
    call curErase			;erase the cursor
   
    ld a,c				;check if the cursor is out of bounds
    cp 4
    jr z,menuLoop
   
    inc c				;if not, increase and return
    jr menuLoop


This bit of code will be called when the user presses enter:


   selection:
   
    ld a,c
   
    cp 1
    jr z,item1
   
    cp 2
    jr z,item2
   
    cp 3
    jr z,item3
   
    jr quit


"2-D" Menus

Instead of only having the choice up and down, why not categorize items and then display the categories left to right? An example of this is the OS's math menu.

2dmenu.jpg

The code for 2 dimensional menus is much more complicated than the standard 1 dimensional menu, but is still manageable for the calculator. Not only do you have to keep track of which item is currently highlighted, you also need to keep track of which group that item is part of.

Since there are enough differences between the code for a 1 dimensional and 2 dimensional menu, I'll post the code in it's entirety. Be aware that it's a lot longer.

This menu has 4 items in each group, with 3 groups.


   menuStart:        ;Start of menu routine
   
    bcall(_ClrLCDFull)        ;Display the Header
   
    ld b,1		;Which group is being displayed
     
   dispMenu:		;display the menu
    
    ld c,1			;Which item is highlighted
    push bc
    
    ld hl,txtHeader		;display the header
    call dispHeader
   
    ld a,b
    
    cp 1
    jr nz,dispMenu2
    
    ld b,3
    ld hl,txtItem1_1            ;Display Items in GA
    call dispItems
    
    jr dispMenu4
    
   dispMenu2:
   
    cp 2
    jr nz,dispMenu3
   
    ld b,3
    ld hl,txtItem2_1		;Display Items in GB
    call dispItems
    jr dispMenu4
   
   dispMenu3:
   
    ld b,3
    ld hl,txtItem3_1		;Display Items in GC
    call dispItems
   
   dispMenu4:			;Since every group has a quit, display it here
    bcall(_NewLine)
    ld hl,txtItemQuit
    bcall(_PutS)
    
    pop bc
   
   menuLoop:        ;Scan key loop to get user input
   
    call curDraw
    push bc            ;save BC for later
   
    bcall(_GetKey)
   
    pop bc            ;we'll need this for some routines with the new keypresses
   
    cp kleft
    jr z,mLeft
    
    cp kright
    jr z,mRight
   
    cp kup
    jr z, mUp
   
    cp kdown
    jr z, mDown
   
    cp kenter
    jr z, selection
   
    cp k1            ;If user pressed 1, do action 1
    jr z,item1
   
    cp k2            ;If user pressed 2, do action 2
    jr z,item2
   
    cp k3            ;If user pressed 3, do action 3
    jp z,item3
   
    cp k4            ;If user pressed 4, quit
    jp z,quit
   
    jr menuLoop        ;Else, invalid input. Wait for user to input new key
   
   mUp:
   
    call curErase            ;erase the cursor
   
    ld a,c                ;check if the cursor is out of bounds
    cp 1
    jr z,menuLoop
   
    dec c                ;if not, decrease and return
    jr menuLoop
   
   mDown:
   
    call curErase            ;erase the cursor
   
    ld a,c                ;check if the cursor is out of bounds
    cp 4
    jr z,menuLoop
   
    inc c                ;if not, increase and return
    jr menuLoop
   
   mRight:		;change group
   
    ld a,b		;check if already as far right as possible
    cp 3
    jr z,menuLoop
    
    inc b
    
    jp dispMenu
   
   mLeft:		;change group
   
    ld a,b		;check if already as far right as possible
    cp 1
    jr z,menuLoop
    
    dec b
    
    jp dispMenu
   
   selection:
   
    ld a,c
   
    cp 1
    jr z,item1
   
    cp 2
    jr z,item2
   
    cp 3
    jr z,item3
   
    jp quit
   
   item1:            ;Action 1
   
    push bc
    bcall(_ClrLCDFull)
    pop bc
   
    ld de,0
    ld (curRow),de
   
    ld a,b
    cp 1
    jr nz,item1B
   
    ld hl,txtSelect1_1		;action 1 for group A
    
    jr item1Done
   item1B:
    cp 2
    jr nz,item1C
    
    ld hl,txtSelect2_1		;action 1 for group B
    
    jr item1Done
   
   item1C:
   
    ld hl,txtSelect3_1		;action 1 for group C
   
   item1Done:
    bcall(_PutS)			;we'll display the text now
   
    bcall(_GetKey)
   
    jp menuStart
   
   item2:            ;Action 2
   
    push bc
    bcall(_ClrLCDFull)
    pop bc
   
    ld de,0
    ld (curRow),de
   
    ld a,b
    cp 1
    jr nz,item2B
   
    ld hl,txtSelect1_2		;action 2 for group A
    
    jr item2Done
   item2B:
    cp 2
    jr nz,item2C
    
    ld hl,txtSelect2_2		;action 2 for group B
    
    jr item2Done
   
   item2C:
   
    ld hl,txtSelect3_2		;action 2 for group C
   
   item2Done:
    bcall(_PutS)
   
    bcall(_GetKey)
   
    jp menuStart
    
   item3:            ;Action 3
   
    push bc
    bcall(_ClrLCDFull)
    pop bc
   
    ld de,0
    ld (curRow),de
   
    ld a,b
    cp 1
    jr nz,item3B
   
    ld hl,txtSelect1_3		;action 3 for group A
    
    jr item2Done
   item3B:
    cp 2
    jr nz,item3C
    
    ld hl,txtSelect2_3		;action 3 for group B
    
    jr item1Done
   
   item3C:
   
    ld hl,txtSelect3_3		;action 3 for group C
   
   item3Done:
    bcall(_PutS)
   
    bcall(_GetKey)
   
    jp menuStart
   
   quit:            ;quit the program
   
    bcall(_ClrLCDFull)
   
    ret
   
   ;dispHeader
   ;
   ;displays the header centered and at the top
   ;
   ;Inputs: HL points to null-terminating string
   ;
   ;Output: text displayed, with the current group in inverse text 
   ;
   ;Destroyed: a,de,hl
   ;
   ;Note: text string must be large text and take up the full line
   ;(use blank spaces to fill in gaps)
   ;
   
   dispHeader:                ;displays the header centered and at the top
   
    ld de,0
    ld (curRow),de
   
    ld a,b
    cp 1
    jr nz,dispHeader1
    set textInverse,(IY+TextFlags)
   
   dispHeader1:					;group A
    
    bcall(_PutS)
    res textInverse,(IY+TextFlags)
   
    bcall(_PutS)
    
    cp 2
    jr nz,dispHeader2
    set textInverse,(IY+TextFlags)
    
   dispHeader2:					;group B
   
    bcall(_PutS)
    res textInverse,(IY+TextFlags)
   
    bcall(_PutS)
   
    cp 3
    jr nz,dispHeader3
    
    set textInverse,(IY+TextFlags)
    
   dispHeader3:					;group C
   
    bcall(_PutS)
    res textInverse,(IY+TextFlags)
    
    ld hl,0
    ld (curRow),hl
   
    ret
   
   ;dispItems
   ;
   ;displays menu items at the start of the next line
   ;
   ;Inputs: HL points to null-terminating string
   ;
   ;Output: text displayed
   ;
   ;Destroyed: all
   ;
   
   dispItems:                ;displays menu items at the start of the next line
   
    push bc
    push hl
    bcall(_NewLine)
    pop hl
    pop bc
   
    bcall(_PutS)
   
    djnz dispItems
   
    ret
   
   ;curDraw
   ;
   ;Draws the cursor
   ;
   ;Inputs: C holds the highlighted item
   ;
   ;Outputs: Cursor displayed
   ;
   ;Destroyed: A
   ;
   
   curDraw:
    set textInverse,(IY+TextFlags)    ;set inverse text
   
    xor a
    ld (curCol),a
    ld a,c
    ld (curRow),a
   
    add a,$30                ;Character offset
   
    bcall(_PutC)
   
    ld a,':'
    bcall(_PutC)
   
    res textInverse,(IY+TextFlags)
    ret
    
   ;curErase
   ;
   ;Erase the cursor
   ;
   ;Inputs: C highlighted item
   ;
   ;Ouputs: Cursor erased
   ;
   ;Destroyed: A
   ;
   
   curErase:
    xor a
    ld (curCol),a
    ld a,c
    ld (curRow),a
   
    add a,$30                ;Character offset
   
    bcall(_PutC)
   
    ld a,':'
    bcall(_PutC)
   
    ret
   
   ;========================================
   ;data
   ;========================================
   
   txtHeader:
    .db "GA",0
    .db "   ",0
    .db "GB",0
    .db "   ",0
    .db "GC",0
   
   txtItem1_1:
    .db "1:A1",0
   
   txtItem1_2:
    .db "2:A2",0
   
   txtItem1_3:
    .db "3:A3",0
   
   txtItem2_1:
    .db "1:B1",0
   
   txtItem2_2:
    .db "2:B2",0
   
   txtItem2_3:
    .db "3:B3",0
    
   txtItem3_1:
    .db "1:C1",0
   
   txtItem3_2:
    .db "2:C2",0
   
   txtItem3_3:
    .db "3:C3",0
   
   txtSelect1_1:
    .db "You selected A1",0
   
   txtSelect1_2:
    .db "You selected A2",0
   
   txtSelect1_3:
    .db "You selected A3",0
   
   txtSelect2_1:
    .db "You selected B1",0
   
   txtSelect2_2:
    .db "You selected B2",0
    
   txtSelect2_3:
    .db "You selected B3",0
   
   txtSelect3_1:
    .db "You selected C1",0
   
   txtSelect3_2:
    .db "You selected C2",0
    
   txtSelect3_3:
    .db "You selected C3",0
   
   txtItemQuit:
    .db "4:Quit",0


Scrolling menus

What if you have more items than will fit in one screen? A solution to this is to display items on the screen and then when the user scrolls past the limits of the screen, it will "move" the items up and display the other items that wouldn't fit.

Scrollingmenu.gif

Summary

There are so many variations of a menu interface that showing them all would be impractical and impossible. Use your imagination and come up with variations, like displaying pictures in the background, having a custom cursor, or anything else you can think of.