/*------------------------------------------------------------*/
/* filename -       tmnuview.cpp                              */
/*                                                            */
/* function(s)                                                */
/*                  TMenuView member functions                */
/*------------------------------------------------------------*/
/*
 *      Turbo Vision - Version 2.0
 *
 *      Copyright (c) 1994 by Borland International
 *      All Rights Reserved.
 *
 */

#define Uses_TMenuItem
#define Uses_TMenu
#define Uses_TMenuView
#define Uses_TKeys
#define Uses_TRect
#define Uses_TEvent
#define Uses_TGroup
#define Uses_TMenuBox
#define Uses_opstream
#define Uses_ipstream
#include <tvision/tv.h>

#if !defined( __ASSERT_H )
#include <assert.h>
#endif  // __ASSERT_H

#if !defined( __CTYPE_H )
#include <ctype.h>
#endif  // __CTYPE_H

#if !defined( __STRING_H )
#include <string.h>
#endif  // __STRING_H

#define cpMenuView "\x02\x03\x04\x05\x06\x07"

TMenuItem::TMenuItem(   TStringView aName,
                        ushort aCommand,
                        TKey aKey,
                        ushort aHelpCtx,
                        TStringView p,
                        TMenuItem *aNext
                    ) noexcept
{
    name = newStr( aName );
    command = aCommand;
    disabled = Boolean(!TView::commandEnabled(command));
    keyCode = aKey;
    helpCtx = aHelpCtx;
    if( p.empty() )
        param = 0;
    else
        param = newStr( p );
    next = aNext;
}

TMenuItem::TMenuItem( TStringView aName,
                      TKey aKey,
                      TMenu *aSubMenu,
                      ushort aHelpCtx,
                      TMenuItem *aNext
                    ) noexcept
{
    name = newStr( aName );
    command = 0;
    disabled = Boolean(!TView::commandEnabled(command));
    keyCode = aKey;
    helpCtx = aHelpCtx;
    subMenu = aSubMenu;
    next = aNext;
}

TMenuItem::~TMenuItem()
{
    delete[] (char *)name;
    if( command == 0 )
        delete subMenu;
    else
        delete[] (char *)param;
}

TMenu::~TMenu()
{
    while( items != 0 )
        {
        TMenuItem *temp = items;
        items = items->next;
        delete temp;
        }
}

void TMenuView::trackMouse( TEvent& e, Boolean& mouseActive )
{
    TPoint mouse = makeLocal( e.mouse.where );
    for( current = menu->items; current != 0; current = current->next )
        {
        TRect r = getItemRect( current );
        if( r.contains(mouse) )
            {
            mouseActive = True;
            return;
            }
        }
}

void TMenuView::nextItem()
{
    if( (current = current->next) == 0 )
        current = menu->items;
}

void TMenuView::prevItem()
{
    TMenuItem *p;

    if( (p = current) == menu->items)
        p = 0;

    do  {
        nextItem();
        } while( current->next != p );
}

void TMenuView::trackKey( Boolean findNext )
{
    if( current == 0 )
        {
        current = menu->items;
        if( !findNext )
            prevItem();
        if( current->name != 0 )
            return;
        }

    do  {
        if( findNext )
            nextItem();
        else
            prevItem();
        } while( current->name == 0 );
}

Boolean TMenuView::mouseInOwner( TEvent& e )
{
    if( parentMenu == 0 )
        return False;
    else
        {
        TPoint mouse = parentMenu->makeLocal( e.mouse.where );
        TRect r = parentMenu->getItemRect( parentMenu->current );
        return r.contains( mouse );
        }
}

Boolean TMenuView::mouseInMenus( TEvent& e )
{
    TMenuView *p =  parentMenu;
    while( p != 0 && !p->mouseInView(e.mouse.where) )
        p = p->parentMenu;

    return Boolean( p != 0 );
}

TMenuView *TMenuView::topMenu()
{
    TMenuView *p = this;
    while( p->parentMenu != 0 )
        p = p->parentMenu;
    return p;
}

enum menuAction { doNothing, doSelect, doReturn };

ushort TMenuView::execute()
{
    Boolean    autoSelect = False;
    Boolean    firstEvent = True;
    menuAction action;
    char   ch;
    ushort result = 0;
    TMenuItem *itemShown = 0;
    TMenuItem *p;
    TMenuView *target;
    TMenuItem *lastTargetItem = 0;
    TRect  r;
    TEvent e;
    Boolean mouseActive;

    current = menu->deflt;

    mouseActive = False;
    do  {
        action = doNothing;
        getEvent(e);
        switch (e.what)
            {
            case  evMouseDown:
                if( mouseInView(e.mouse.where) || mouseInOwner(e) )
                    {
                    trackMouse(e, mouseActive);
                    // autoSelect makes it possible to open the selected submenu directly
                    // on a MouseDown event. This should be avoided, however, when said
                    // submenu was just closed by clicking on its name, or when this is
                    // not a menu bar.
                    if( size.y == 1 )
                        autoSelect = (Boolean) (!current || lastTargetItem != current);
                    // A submenu will close if the MouseDown event takes place on the
                    // parent menu, except when this submenu has just been opened.
                    else if( !firstEvent && mouseInOwner(e) )
                        action = doReturn;
                    }
                else
                    {
                    // Menu gets closed by a click outside its bounds.
                    // Let the event reach the view recovering focus.
                    if( putClickEventOnExit )
                        putEvent(e);
                    action =  doReturn;
                    }
                break;
            case  evMouseUp:
                trackMouse(e, mouseActive);
                if( mouseInOwner(e) )
                    current = menu->deflt;
                else if( current != 0 )
                    {
                    if( current->name != 0 )
                        {
                        if ( current != lastTargetItem )
                            action = doSelect;
                        else if ( size.y == 1 )
                        // If a menu bar entry was closed, exit and stop listening
                        // for events.
                            action = doReturn;
                        else
                        // MouseUp won't open up a submenu that was just closed by clicking
                        // on its name.
                            {
                            action = doNothing;
                            // But the next one will.
                            lastTargetItem = 0;
                            }
                        }
                    }
                else if ( mouseActive && !mouseInView(e.mouse.where) )
                    action = doReturn;
                else if ( size.y != 1 )
                // When MouseUp happens inside the Box but not on a highlightable entry
                // (e.g. on a margin, or a separator), either the default or the first
                // entry will be automatically highlighted. This was added in Turbo Vision 2.0.
                // But this doesn't make sense in a menu bar, which was the original behaviour.
                    {
                    current = menu->deflt;
                    if (current == 0)
                        current = menu->items;
                    action = doNothing;
                    }
                break;
            case  evMouseMove:
                if( e.mouse.buttons != 0 )
                    {
                    trackMouse(e, mouseActive);
                    if( !(mouseInView(e.mouse.where) || mouseInOwner(e)) &&
                        mouseInMenus(e) )
                        action = doReturn;
                    // A menu bar entry closed by clicking on its name stays highlighted
                    // until MouseUp. If mouse drag is then performed and a different
                    // entry is selected, it will open up automatically.
                    else if ( size.y == 1 && mouseActive && current != lastTargetItem )
                        autoSelect = True;
                    }
                break;
            case  evKeyDown:
                switch( e.keyDown.keyCode )
                    {
                    case  kbUp:
                    case  kbDown:
                        if( size.y != 1 )
                            trackKey(Boolean(e.keyDown.keyCode == kbDown));
                        else if( e.keyDown.keyCode == kbDown )
                            autoSelect =  True;
                        break;
                    case  kbLeft:
                    case  kbRight:
                        if( size.y == 1 )
                            trackKey(Boolean(e.keyDown.keyCode == kbRight));
                        else if( parentMenu != 0 )
                            action = doReturn;
                        break;
                    case  kbHome:
                    case  kbEnd:
                        if( size.y != 1 )
                            {
                            current = menu->items;
                            if( e.keyDown.keyCode == kbEnd )
                                trackKey(False);
                            }
                        break;
                    case  kbEnter:
                        if( size.y == 1 )
                            autoSelect =  True;
                        action = doSelect;
                        break;
                    case  kbEsc:
                        action = doReturn;
                        if( parentMenu == 0 || parentMenu->size.y != 1 )
                            clearEvent(e);
                        break;
                    default:
                        target = this;
                        ch = getAltChar(e.keyDown.keyCode);
                        if( ch == 0 )
                            ch = e.keyDown.charScan.charCode;
                        else
                            target = topMenu();
                        p = target->findItem(ch);
                        if( p == 0 )
                            {
                            p = topMenu()->hotKey(e.keyDown);
                            if( p != 0 && commandEnabled(p->command) )
                                {
                                result = p->command;
                                action = doReturn;
                                }
                            }
                        else if( target == this )
                            {
                            if( size.y == 1 )
                                autoSelect = True;
                            action = doSelect;
                            current = p;
                            }
                        else if( parentMenu != target ||
                                 parentMenu->current != p )
                                action = doReturn;
                    }
                break;
            case  evCommand:
                if( e.message.command == cmMenu )
                    {
                    autoSelect = False;
                    lastTargetItem = 0;
                    if (parentMenu != 0 )
                        action = doReturn;
                    }
                else
                    action = doReturn;
                break;
            }

        // If a submenu was closed by clicking on its name, and the mouse is dragged
        // to another menu entry, then the submenu will be opened the next time it
        // is hovered over.
        if( lastTargetItem != current )
            lastTargetItem = 0;

        if( itemShown != current )
            {
            itemShown =  current;
            drawView();
            }

        if( (action == doSelect || (action == doNothing && autoSelect)) &&
            current != 0 &&
            current->name != 0 )
                {
                if( current->command == 0 && !current->disabled )
                    {
                    if( (e.what & (evMouseDown | evMouseMove)) != 0 )
                        putEvent(e);
                    r = getItemRect( current );
                    r.a.x = r.a.x + origin.x;
                    r.a.y = r.b.y + origin.y;
                    r.b = owner->size;
                    if( size.y == 1 )
                        r.a.x--;
                    target = topMenu()->newSubView(r, current->subMenu, this);
                    result = owner->execView(target);
                    destroy( target );
                    lastTargetItem = current;
                    menu->deflt = current;
                    }
                else if( action == doSelect )
                    result = current->command;
                }

        if( result != 0 && commandEnabled(result) )
            {
            action =  doReturn;
            clearEvent(e);
            }
        else
            result = 0;

        firstEvent = False;
        } while( action != doReturn );

    if( e.what != evNothing &&
        (parentMenu != 0 || e.what == evCommand))
            putEvent(e);
    if( current != 0 )
        {
        menu->deflt = current;
        current = 0;
        drawView();
        }
    return result;
}

TMenuItem *TMenuView::findItem( char ch )
{
    ch = toupper((uchar) ch);
    TMenuItem *p = menu->items;
    while( p != 0 )
        {
        if( p->name != 0 && !p->disabled )
            {
            char *loc = strchr( (char *) p->name, '~' );
            if( loc != 0 && ch == (char) toupper((uchar) loc[1]) )
                return p;
            }
        p =  p->next;
        }
    return 0;
}

TRect TMenuView::getItemRect( TMenuItem * )
{
    return TRect( 0, 0, 0, 0 );
}

ushort TMenuView::getHelpCtx()
{
    TMenuView *c = this;

    while( c != 0 &&
                (c->current == 0 ||
                 c->current->helpCtx == hcNoContext ||
                 c->current->name == 0 )
         )
        c = c->parentMenu;

    if( c != 0 )
        return c->current->helpCtx;
    else
        return helpCtx;
}

TPalette& TMenuView::getPalette() const
{
    static TPalette palette( cpMenuView, sizeof( cpMenuView )-1 );
    return palette;
}

Boolean TMenuView::updateMenu( TMenu *menu )
{
    Boolean res = False;
    if( menu != 0 )
        {
        for( TMenuItem *p = menu->items; p != 0; p = p->next )
            {
            if( p->name != 0 )
                {
                if( p->command == 0 )
                    {
                    if( updateMenu(p->subMenu) == True )
                        res = True;
                    }
                else
                    {
                    Boolean commandState = commandEnabled(p->command);
                    if( p->disabled == commandState )
                        {
                        p->disabled = Boolean(!commandState);
                        res = True;
                        }
                    }
                }
            }
        }
    return res;
}

void TMenuView::do_a_select( TEvent& event )
{
    putEvent( event );
    event.message.command = owner->execView(this);
    if( event.message.command != 0 && commandEnabled(event.message.command) )
        {
        event.what = evCommand;
        event.message.infoPtr = 0;
        putEvent(event);
        }
    clearEvent(event);
}

void TMenuView::handleEvent( TEvent& event )
{
    if( menu != 0 )
        switch (event.what)
            {
            case  evMouseDown:
                do_a_select(event);
                break;
            case  evKeyDown:
                if( findItem(getAltChar(event.keyDown.keyCode)) != 0 )
                    do_a_select(event);
                else
                    {
                    TMenuItem *p = hotKey(event.keyDown);
                    if( p != 0 && commandEnabled(p->command))
                        {
                        event.what = evCommand;
                        event.message.command = p->command;
                        event.message.infoPtr = 0;
                        putEvent(event);
                        clearEvent(event);
                        }
                    }
                break;
            case  evCommand:
                if( event.message.command == cmMenu )
                    do_a_select(event);
                break;
            case  evBroadcast:
                if( event.message.command == cmCommandSetChanged )
                    {
                    if( updateMenu(menu) )
                        drawView();
                    }
                break;
            }
}


TMenuItem *TMenuView::findHotKey( TMenuItem *p, TKey key )
{

    while( p != 0 )
        {
        if( p->name != 0 )
            {
            if( p->command == 0 )
                {
                TMenuItem *T;
                if( (T = findHotKey( p->subMenu->items, key )) != 0 )
                    return T;
                }
            else if( !p->disabled &&
                     p->keyCode != kbNoKey &&
                     p->keyCode == key
                   )
                return p;
            }
        p =  p->next;
        }
    return 0;
}

TMenuItem *TMenuView::hotKey( TKey key )
{
    return findHotKey( menu->items, key );
}

TMenuView *TMenuView::newSubView( const TRect& bounds,
                                  TMenu *aMenu,
                                  TMenuView *aParentMenu
                               )
{
    return new TMenuBox( bounds, aMenu, aParentMenu );
}

#if !defined(NO_STREAMABLE)

void TMenuView::writeMenu( opstream& os, TMenu *menu )
{
    uchar tok = 0xFF;

    assert( menu != 0 );

    for( TMenuItem *item = menu->items; item != 0; item = item->next )
        {
        os << tok;
        os.writeString( item->name );
        os << item->command << (int)(item->disabled)
           << item->keyCode << item->helpCtx;
        if( item->name != 0 )
            {
            if( item->command == 0 )
                writeMenu( os, item->subMenu );
            else
                os.writeString( item->param );
            }
        }

    tok = 0;
    os << tok;
}

void TMenuView::write( opstream& os )
{
    TView::write( os );
    writeMenu( os, menu );
}

TMenu *TMenuView::readMenu( ipstream& is )
{
    TMenu *menu = new TMenu;
    TMenuItem **last = &(menu->items);
    TMenuItem *item;

    uchar tok;
    is >> tok;

    while( tok != 0 )
        {
        assert( tok == 0xFF );
        item = new TMenuItem( 0, 0, (TMenu *)0 );
        *last = item;
        last = &(item->next);
        item->name = is.readString();
        int temp;
        is >> item->command >> temp
           >> item->keyCode >> item->helpCtx;
        item->disabled = Boolean( temp );
        if( item->name != 0 )
            {
            if( item->command == 0 )
                item->subMenu = readMenu( is );
            else
                item->param = is.readString();
            }
        is >> tok;
        }
    *last = 0;
    menu->deflt = menu->items;
    return menu;
}

void *TMenuView::read( ipstream& is )
{
    TView::read( is );
    menu = readMenu( is );
    parentMenu = 0;
    current = 0;
    return this;
}

TStreamable *TMenuView::build()
{
    return new TMenuView( streamableInit );
}

TMenuView::TMenuView( StreamableInit ) noexcept : TView( streamableInit )
{
}


#endif
