; _______________________________________________________________________________________
;|                                                                                       |
;| ..::FreshLib::..  Free, open source. Licensed under "BSD 2-clause" license."          |
;|_______________________________________________________________________________________|
;
;  Description: TMenu object class
;
;  Target OS: Any
;
;  Dependencies:
;
;  Notes: Represents main menu or popup menu objects.
;_________________________________________________________________________________________
module "TMenu library"


mitSeparator = 0
mitAction    = 1
mitSubmenu   = 2
mitString    = 3        ; contains simple text string can't have accelerator and icons.


struct TMenuItem
  .type   dd ?
  .item   dd ?    ; different for the different types of items: NULL, pointer to TAction, pointer to TMenu, handle of string.
  .height dd ?
ends



object TMenu, TWindow
  ._p_items   dd ?
  ._selected  dd ?
  .__caller   dd ?

  .__icon_size dd ?

  param .Selected, ._selected, .SetSelected
  param .Items, ._p_items, .SetItems

  method .SetSelected, .value
  method .SetItems, .menu_template
  method .SetVisible, .value

  method .AddItem, .Type, .Item

  method .Paint

  method .Create, .Parent
  method .Destroy

  method .Show, .parent, .x, .y

  method .EventButtonPress, .button, .kbdState, .x, .y
  method .EventMouseMove, .x, .y, .kbdState

endobj



method TMenu.Create
begin
        stdcall CreateArray, sizeof.TMenuItem
        push    eax

        mov     eax, [.self]
        pop     [eax+TMenu._p_items]

        mov     [eax+TMenu.__want_focus], 0
        mov     [eax+TMenu._selected], -1

        inherited 0

        set     [.self], TWindow:border, borderNone
        return
endp




method TMenu.Destroy
begin
        mov     eax, [.self]
        stdcall FreeMem, [eax+TMenu._p_items]
        return
endp



method TMenu.SetItems
begin
        pushad




        popad
        return
endp


method TMenu.AddItem
begin
        pushad

        mov     esi, [.self]
        mov     edx, [esi+TMenu._p_items]

        stdcall AddArrayItems, edx, 1
        mov     [esi+TMenu._p_items], edx

        pushd   [.Type]
        pushd   [.Item]
        popd    [eax+TMenuItem.item]
        popd    [eax+TMenuItem.type]

        mov     [eax+TMenuItem.height], 10


        popad
        return
endp




method TMenu.SetVisible ;

.maxW dd ?
.sumH dd ?
.str  dd ?

begin
        pushad

        mov     esi, [.self]

        cmp     [.value], FALSE
        je      .switch_off

        get     edi, esi, TMenu:Items

        mov     ecx, [edi+TArray.count]
        test    ecx, ecx
        jnz     .compute_width

        stdcall GetTextBounds, "Empty menu", 10, [GUI.DefaultFont]
        add     eax, 20
        jmp     .set_width


.compute_width:

        lea     edi, [edi+TArray.array-sizeof.TMenuItem]        ; edi points to the current TMenuItem

        xor     eax, eax
        mov     [.maxW], eax                    ; the width of the menu window
        mov     [.sumH], eax                    ; the total height of the menu window
        mov     [esi+TMenu.__icon_size], 0

.loop:
        add     edi, sizeof.TMenuItem
        dec     ecx
        js      .end_width

        cmp     [edi+TMenuItem.type], mitSeparator
        je      .separator

        cmp     [edi+TMenuItem.type], mitAction
        je      .action

        cmp     [edi+TMenuItem.type], mitSubmenu
        je      .submenu

; it is string.

        stdcall StrDup, [edi+TMenuItem.item]
        push    eax
        jmp     .str_width

.separator:

        mov     edx, [GUI.menuSeparatorHeight]
        jmp     .set_height

.submenu:
        get     eax, [edi+TMenuItem.item], TMenu:Caption
        push    eax

        stdcall StrCat, eax, txt "  >"
        jmp     .str_width


.action:
        get     eax, [edi+TMenuItem.item], TAction:Icon
        test    eax, eax
        jz      .icon_ok

        mov     eax, [eax+TImage.height]

        cmp     eax, [esi+TMenu.__icon_size]
        jbe     .icon_ok
        mov     [esi+TMenu.__icon_size], eax

.icon_ok:

        get     eax, [edi+TMenuItem.item], TAction:AccelStr
        push    eax

        get     edx, [edi+TMenuItem.item], TAction:Caption
        stdcall StrCat, eax, edx


.str_width:
        stdcall StrPtr, eax
        stdcall GetTextBounds, eax, [eax+string.len], [GUI.DefaultFont]

        stdcall StrDel ; from the stack

        cmp     eax, [.maxW]
        jbe     .comp_height

        mov     [.maxW], eax

.comp_height:

        mov     eax, [esi+TMenu.__icon_size]
        add     eax, [GUI.menuIconMargin]
        add     edx, [GUI.menuIconMargin]       ; edx = height

        cmp     edx, eax
        cmovb   edx, eax

.set_height:
        mov     [edi+TMenuItem.height], edx
        add     [.sumH], edx

        jmp     .loop

.end_width:

; here compute the size of the menu and set .width and .height fields.

        mov     edx, [GUI.iconMenuChecked]
        mov     eax, [edx+TImage.width]
        add     eax, [esi+TMenu.__icon_size]

        add     eax, [.maxW]
        add     eax, [GUI.menuMinTextDist]

        mov     edx, [GUI.menuIconMargin]
        lea     edx, [3*edx]
        add     eax, edx
        mov     edx, [GUI.boxBorderWidth]
        lea     eax, [eax+2*edx]


.set_width:
        set     esi, TWindow:width, eax

        mov     edx, [.sumH]
        mov     eax, [GUI.boxBorderWidth]
        lea     edx, [edx+2*eax]

        mov     eax, [GUI.menuIconMargin]
        lea     edx, [edx+2*eax]

        set     esi, TWindow:height, edx

.finish:
        popad

        inherited [.value]
        return


.switch_off:
        xor     ecx, ecx

        mov     eax, [esi+TMenu.__caller]
        test    eax, eax
        jz      .set_active

        istype  eax, TMenu
        cmove   ecx, eax
        je      .set_active

; remove capture.
        stdcall SetMouseCapture, ecx    ; ecx is 0 here!!!

.set_active:
        mov     [__ActiveMenu], ecx
        jmp     .finish

endp



method TMenu.Paint

.bounds TBounds

.y    dd ?
.ytxt dd ?
.xTxt dd ?
.clTxt dd ?

.txtline dd ?
.ascender dd ?

.count dd ?

.canvas dd ?

; item parameters, depend on the item type.

.text    dd ?
.accel   dd ?
.icon    dd ?
.check   dd ?
.enabled dd ?
.subicon dd ?

begin
        pushad
        mov     esi, [.self]

        exec    esi, TWindow:__UpdateCanvas

        mov     ebx, [esi+TWindow._canvas]
        mov     [.canvas], ebx
        test    ebx, ebx
        jz      .end_loop

        xor     eax, eax
        mov     [.bounds.x], eax
        mov     [.bounds.y], eax

        mov     eax, [esi+TWindow._width]
        mov     ecx, [esi+TWindow._height]
        mov     [.bounds.width], eax
        mov     [.bounds.height], ecx

        lea     eax, [.bounds]
        stdcall [DrawBox], [.canvas], eax, [GUI.clMenuBack], [GUI.borderMenu], [GUI.boxBorderWidth]

        get     edi, esi, TMenu:Items
        test    edi, edi
        jz      .end_loop


; compute the text base line.

        stdcall GetFontMetrics, [GUI.DefaultFont]
        mov     [.txtline], eax
        lea     ebx, [2*ebx]
        mov     [.ascender], ebx

        mov     ecx, [GUI.boxBorderWidth]
        add     ecx, [GUI.menuIconMargin]
        mov     [.y], ecx

        mov     ecx, [GUI.menuIconMargin]
        lea     ecx, [3*ecx]
        mov     eax, [esi+TMenu.__icon_size]

        add     ecx, eax
        add     ecx, [GUI.boxBorderWidth]

        mov     eax, [GUI.iconMenuChecked]
        add     ecx, [eax+TImage.width]

        mov     [.xTxt], ecx

        mov     ecx, [edi+TArray.count]
        lea     edi, [edi+TArray.array]

        mov     [.count], ecx

        xor     ecx, ecx
        dec     ecx

.loop:
        inc     ecx
        cmp     ecx, [.count]
        jae     .end_loop

        mov     eax, [edi+TMenuItem.type]

        cmp     eax, mitSeparator
        je      .draw_separator

        cmp     eax, mitAction
        je      .action

        cmp     eax, mitSubmenu
        je      .submenu

        cmp     eax, mitString
        jne     .next_item

.string:
        stdcall StrDup, [edi+TMenuItem.item]
        mov     [.text], eax

        xor     eax, eax
        mov     [.accel], eax
        mov     [.icon], eax
        mov     [.check], eax
        mov     [.subicon], eax
        inc     eax
        mov     [.enabled], eax
        jmp     .draw_background

.submenu:
        get     eax, [edi+TMenuItem.item], TMenu:Caption
        mov     [.text], eax

        get     eax, [edi+TMenuItem.item], TMenu:Enabled
        mov     [.enabled], eax

        mov     ebx, [GUI.menuSubIconGray]
        test    eax, eax
        jz      @f

        mov     ebx, [GUI.menuSubIcon]

        cmp     ecx, [esi+TMenu._selected]
        jne     @f
        mov     ebx, [GUI.menuSubIconSel]

@@:
        mov     [.subicon], ebx

        xor     eax, eax
        mov     [.icon], eax
        mov     [.check], eax
        mov     [.accel], eax
        jmp     .draw_background


.action:
        mov     [.subicon], 0

        get     eax, [edi+TMenuItem.item], TAction:Caption
        mov     [.text], eax

        get     eax, [edi+TMenuItem.item], TAction:AccelStr
        mov     [.accel], eax

        get     eax, [edi+TMenuItem.item], TAction:Enabled
        mov     [.enabled], eax

        get     eax, [edi+TMenuItem.item], TAction:Icon
        mov     [.icon], eax

        get     eax, [edi+TMenuItem.item], TAction:Checked
        mov     [.check], eax
        jmp     .draw_background


.draw_separator:
        mov     eax, [.bounds.x]
        mov     ebx, [.bounds.width]
        mov     edx, [GUI.menuIconMargin]
        add     eax, edx
        sub     ebx, edx
        sub     ebx, edx

        mov     edx, [edi+TMenuItem.height]
        add     edx, [.y]
        add     edx, [.y]
        sar     edx, 1

        stdcall DrawSolidRect, [.canvas], eax, edx, ebx, 1, [GUI.clBorderDark]
        inc     edx
        stdcall DrawSolidRect, [.canvas], eax, edx, ebx, 1, [GUI.clBorderLight]
        jmp     .next_item


; draw the background
.draw_background:
        cmp     ecx, [esi+TMenu._selected]
        jne     .selected_ok

        cmp     [.enabled], 0
        je      .selected_ok

        stdcall DrawSolidRect, [.canvas], [.bounds.x], [.y], [.bounds.width], [edi+TMenuItem.height], [GUI.clMenuSelection]

.selected_ok:

; draw the icon

        cmp     [.icon], 0
        je      .icon_ok

        mov     ebx, [GUI.iconMenuChecked]
        mov     eax, [ebx+TImage.width]

        add     eax, [GUI.menuIconMargin]
        add     eax, [GUI.menuIconMargin]
        add     eax, [GUI.boxBorderWidth]       ; x-coordinate

        mov     ebx, [.icon]
        test    ebx, ebx
        jz      .icon_ok

        mov     edx, [edi+TMenuItem.height]
        sub     edx, [ebx+TImage.height]
        add     edx, [.y]
        add     edx, [.y]
        sar     edx, 1

        stdcall BlendImage, [.canvas], eax, edx, ebx, 0, 0, [ebx+TImage.width], [ebx+TImage.height]

.icon_ok:

; draw the checkbox

        cmp     [.check], 0
        je      .draw_text

        mov     eax, [GUI.menuIconMargin]
        add     eax, [GUI.boxBorderWidth]

        mov     ebx, [GUI.iconMenuChecked]

        cmp     [.enabled], 0
        jne     .check_ok

        mov     ebx, [GUI.iconMenuCheckedGray]

.check_ok:
        mov     edx, [edi+TMenuItem.height]
        sub     edx, [ebx+TImage.height]
        add     edx, [.y]
        add     edx, [.y]
        sar     edx, 1

        stdcall BlendImage, [.canvas], eax, edx, ebx, 0, 0, [ebx+TImage.width], [ebx+TImage.height]


.draw_text:

        mov     eax, [GUI.clMenuTextGray]

        cmp     [.enabled], 0
        je      .txt_color_ok

        mov     eax, [GUI.clMenuText]

        cmp     ecx, [esi+TMenu._selected]
        jne     .txt_color_ok

        mov     eax, [GUI.clEditSelTxt]

.txt_color_ok:
        mov     [.clTxt], eax

        push    eax
        push    [GUI.DefaultFont]

        mov     eax, [edi+TMenuItem.height]
        sub     eax, [.txtline]
        add     eax, [.ascender]
        sar     eax, 1
        add     eax, [.y]
        mov     [.ytxt], eax

        push    eax
        pushd   [.xTxt]

        stdcall StrLen, [.text]
        push    eax

        stdcall StrPtr, [.text]

        stdcall DrawString, [.canvas], eax    ; remaining from the stack

        stdcall StrDel, [.text]

; draw the submenu arrow
        mov     ebx, [.subicon]
        test    ebx, ebx
        jz      .draw_accelerator

        mov     eax, [.bounds.width]
        sub     eax, [ebx+TImage.width]
        sub     eax, [GUI.menuIconMargin]

        mov     edx, [edi+TMenuItem.height]
        sub     edx, [ebx+TImage.height]
        add     edx, [.y]
        add     edx, [.y]
        sar     edx, 1

        stdcall BlendImage, [.canvas], eax, edx, ebx, 0, 0, [ebx+TImage.width], [ebx+TImage.height]
        jmp     .next_item

; draw the accelerator
.draw_accelerator:
        cmp     [.accel], 0
        je      .next_item

        stdcall StrPtr, [.accel]
        push    eax

        stdcall GetTextBounds, eax, [eax+string.len], [GUI.DefaultFont]

        mov     edx, [.bounds.width]
        sub     edx, [GUI.boxBorderWidth]
        sub     edx, [GUI.menuIconMargin]
        sub     edx, eax

        pop     eax
        stdcall DrawString, [.canvas], eax, [eax+string.len], edx, [.ytxt], [GUI.DefaultFont], [.clTxt]

        stdcall StrDel, [.accel]


.next_item:
        mov     eax, [edi+TMenuItem.height]
        add     [.y], eax

        add     edi, sizeof.TMenuItem
        jmp     .loop


.end_loop:

        popad
        return

.empty_menu:
        stdcall DrawString, [.canvas], "Empty menu", 10, 10, 14, [GUI.DefaultFont], [GUI.clMenuText]
        stdcall FilterDisabled, [.canvas], FALSE
        jmp     .end_loop

endp




method TMenu.Show
begin
        pushad

        mov     esi, [.self]
        mov     edi, [.parent]

        istype edi, TMenu
        je     .capture_ok

        stdcall SetMouseCapture, edi

.capture_ok:

        exec    edi, TWindow:ClientToScreenXY, [.x], [.y]

        mov     [esi+TWindow._x], ecx
        mov     [esi+TWindow._y], edx

        mov     [esi+TMenu._selected], 0

        mov     [esi+TMenu.__caller], edi
        mov     [__ActiveMenu], esi

        set     esi, TMenu:Visible, TRUE

        exec    esi, TMenu:Refresh

        popad
        return
endp




proc AcceleratorToStr, .pFastKey
begin
        push    ecx edx

        mov     edx, [.pFastKey]
        test    edx, edx
        jz      .empty

        mov     eax, [edx+TFastKey.key]         ; scan code
        xor     ecx, ecx                        ; utf8

        test    eax, eax
        js      @f
        xchg    ecx, eax
@@:
        and     eax, $7fffffff
        stdcall CreateKeyName, ecx, eax, [edx+TFastKey.flags]
        jnc     .finish

.empty:
        stdcall StrNew
        stc

.finish:
        pop     edx ecx
        return
endp



method TMenu.SetSelected
begin
        pushad

        mov     esi, [.self]
        mov     ecx, [.value]
        cmp     ecx, [esi+TMenu._selected]
        je      .finish

        get     edi, esi, TMenu:Items
        test    edi, edi
        jz      .finish

        cmp     ecx, [edi+TArray.count]
        jae     .finish

        mov     [esi+TMenu._selected], ecx
        exec    esi, TWindow:Refresh

; compute Y coordinate

        lea     edi, [edi+TArray.array]
        xor     ebx, ebx

.loop:
        dec     ecx
        js      .height_ok

        add     ebx, [edi+TMenuItem.height]
        add     edi, sizeof.TMenuItem
        jmp     .loop

.height_ok:

        cmp     [edi+TMenuItem.type], mitSubmenu
        jne     .hide_old

        get     eax, [edi+TMenuItem.item], TMenu:Enabled
        test    eax, eax
        jz      .finish

        mov     eax, [esi+TMenu._width]
        sub     eax, [GUI.menuIconMargin]

        exec    [edi+TMenuItem.item], TMenu:Show, esi, eax, ebx

        jmp     .finish

.hide_old:
        cmp     [__ActiveMenu], esi
        je      .finish

        set     [__ActiveMenu], TWindow:Visible, FALSE
        mov     [__ActiveMenu], esi

.finish:
        popad
        return
endp



method TMenu.EventMouseMove
begin
        pushad

        mov     esi, [.self]

        mov     ecx, [.x]
        mov     edx, [.y]
        sub     ecx, [esi+TMenu._x]
        js      .check_caller
        sub     edx, [esi+TMenu._y]
        js      .check_caller

        cmp     ecx, [esi+TMenu._width]
        jge     .check_caller

        cmp     edx, [esi+TMenu._height]
        jge      .check_caller

        sub     edx, [GUI.boxBorderWidth]
        js      .exit

        sub     edx, [GUI.menuIconMargin]
        js      .exit

; search for item.

        get     edi, esi, TMenu:Items

        mov     ecx, [edi+TArray.count]
        lea     edi, [edi+TArray.array]

        xor     ebx, ebx        ; index
        xor     eax, eax        ; Y coordinate

.loop:
        dec     ecx
        js      .exit

        add     eax, [edi+TMenuItem.height]
        cmp     edx, eax
        jl      .found

        inc     ebx
        add     edi, sizeof.TMenuItem
        jmp     .loop

.found:
        set     esi, TMenu:Selected, ebx

.exit:
        popad
        return

.check_caller:
        mov     esi, [esi+TMenu.__caller]
        test    esi, esi
        jz      .exit

        istype  esi, TMenu
        jne     .exit

        exec    esi, TMenu:EventMouseMove, [.x], [.y], [.kbdState]
        jmp     .exit
endp





method TMenu.EventButtonPress
begin
        pushad
        mov     esi, [.self]

        mov     ecx, [.x]
        mov     edx, [.y]

        sub     ecx, [esi+TMenu._x]
        js      .check_caller
        sub     edx, [esi+TMenu._y]
        js      .check_caller
        cmp     ecx, [esi+TMenu._width]
        jge     .check_caller
        cmp     edx, [esi+TMenu._height]
        jge     .check_caller

        mov     ecx, [esi+TMenu._selected]
        mov     edx, [esi+TMenu._p_items]

        cmp     ecx, [edx+TArray.count]
        jae     .finish

        mov     eax, sizeof.TMenuItem
        imul    ecx, eax

        lea     edi, [edx+TArray.array+ecx]

        cmp     [edi+TMenuItem.type], mitSeparator
        je      .finish

        cmp     [edi+TMenuItem.type], mitString
        je      .finish                                 ; temporary, until I decide what to do with the string items.

        cmp     [edi+TMenuItem.type], mitSubmenu
        je      .finish

        cmp     [edi+TMenuItem.type], mitAction
        jne     .finish

        get     eax, [edi+TMenuItem.item], TAction:Enabled
        test    eax, eax
        jz      .finish

        call    .hideall
        exec    [edi+TMenuItem.item], TAction:Execute, [.self]   ; execute the action.

.finish:
        popad
        return


.check_caller:
        set     esi, TMenu:Visible, FALSE       ; hide the menu

        mov     esi, [esi+TMenu.__caller]
        test    esi, esi
        jz      .finish

        istype  esi, TMenu
        jne     .finish

        mov     [esi+TMenu._selected], -1
        exec    esi, TMenu:EventMouseMove, [.x], [.y], [.kbdState]
        exec    esi, TMenu:EventButtonPress, [.button], [.kbdState], [.x], [.y]
        jmp     .finish


.hideall:
        cmp     [__ActiveMenu], 0
        jne     @f

        retn

@@:
        set     [__ActiveMenu], TMenu:Visible, FALSE       ; hide the menu
        jmp     .hideall




endp




endmodule


