If you have comments or questions concerning this source file, discuss them in the forum.
/*
Copyright (c) 2002 Nicolai Haehnle

See the license.txt for details. If that file was not included in the
source distributions, please email <prefect@rtts.org>
*/
// gui_panel.h

#include "library.h"
#include "hal.h"

#include "gui_panel.h"

/*
GPanel is the base class for all visible GUI elements

m_pFocus is the currently focussed child. Key events are always relayed
to the focussed child. Oblivious children cannot hold the focus. Modal
panels are always focussed.
*/


/*
==============================================================================

GPanel IMPLEMENTATION

==============================================================================
*/

GPanel *GPanel::s_pModal = 0;
GPanel *GPanel::s_pGrabMouse = 0;
std::list<GPanel*> GPanel::s_TopLevel;

/*
==============
GPanel::GPanel

Initialize member variables, link into parent's list
==============
*/
GPanel::GPanel(GPanel *pParent, int x, int y, int w, int h, bool bTopLevel)
    : LObject(), m_rc(x, y, w, h)
{
    m_pFocus = 0;
    m_bFocussed = 0;
    m_pMouseIn = 0;
    m_bHasMouse = false;
    m_bGrabbedMouse = false;

    m_bModal = false;

    m_bDormant = false;
    m_bOblivious = false;
    m_bCanFocus = false;
    m_bVisible = true;
    m_bTopLevel = false;

    m_bActive = false;

    m_color[0] = m_color[1] = m_color[2] = m_color[3] = 255;

    m_pParent = pParent;
    if (pParent) {
        pParent->m_Children.push_front(this);
        pParent->intAddChild(this);
    } else
        bTopLevel = true;

    if (bTopLevel)
        SetTopLevel(true);
}

/*
==============
GPanel::~GPanel

Unlink the panel from parent and children
==============
*/
GPanel::~GPanel()
{
    lassert(m_bModal == false);

    while(!m_Children.empty())
        delete *m_Children.begin();

    if (m_pParent) {
        m_pParent->intRemoveChild(this);
        m_pParent->m_Children.remove(this);
    }

    if (m_bTopLevel)
        s_TopLevel.remove(this);
}

/*
==============
GPanel::intAddChild

Called by the child's constructor, as a mere notification.
Linking is done by the child itself.
==============
*/
void GPanel::intAddChild(GPanel *pChild)
{
}

/*
==============
GPanel::intRemoveChild

This is only a notification. Unlinking is done by the child
itself.
==============
*/
void GPanel::intRemoveChild(GPanel *pChild)
{
    if (pChild == m_pFocus)
        SetFocus(0);
}

/*
==============
GPanel::IsVisible

Parents' invisibility overrides children's visibility.
==============
*/
bool GPanel::IsVisible()
{
    if (!m_bVisible)
        return false;

    if (m_bTopLevel || !m_pParent)
        return true;
    return m_pParent->IsVisible();
}

/*
===============
GPanel::SetTopLevel

(Un-)makes the panel a top-level panel
===============
*/
void GPanel::SetTopLevel(bool toplevel)
{
    if (!m_pParent && !toplevel)
        return;
    if (toplevel == m_bTopLevel)
        return;

    m_bTopLevel = toplevel;
    if (m_bTopLevel) {
        s_TopLevel.push_front(this);
    } else {
        s_TopLevel.remove(this);
    }
}

/*
==============
GPanel::SetOblivious

Oblivious panels can't receive the focus
==============
*/
void GPanel::SetOblivious(bool bOblivious)
{
    if (bOblivious) {
        lassert(!m_bModal);

        if (m_pParent && m_pParent->m_pFocus == this)
            m_pParent->SetFocus(0);
    }

    m_bOblivious = bOblivious;
}

/*
==============
GPanel::SetCanFocus

Sets the panel's can-focus state
==============
*/
void GPanel::SetCanFocus(bool bCanFocus)
{
    // Drop the focus if necessary
    if (!bCanFocus) {
        lassert(!m_bModal);

        if (m_pParent && m_pParent->m_pFocus == this)
            m_pParent->SetFocus(0);
    }

    m_bCanFocus = bCanFocus;
}

/*
==============
GPanel::SetFocus

Set the focus to the given panel. It must be a direct child of us.
The panel must not be oblivious.
If self isn't focussed it tries to gain focus.
==============
*/
void GPanel::SetFocus(GPanel *pPanel)
{
    lassert(!pPanel || pPanel->m_pParent == this);
    lassert(!pPanel || pPanel->CanFocus());

    // if it's currently focussed, nothing happens unless we have
    // to grab the focus from our parent
    if (m_pFocus == pPanel) {
        if (pPanel && !m_bFocussed) {
            lassert(m_pParent);
            m_pParent->SetFocus(this);
        }
        return;
    }

    // notify the current focus that it looses
    if (m_pFocus) {
        if (m_bFocussed)
            m_pFocus->intGainFocus(false);
        m_pFocus = 0;
    }

    // notify the new focus that it wins
    if (pPanel) {
        m_pFocus = pPanel;
        if (m_bFocussed)
            m_pFocus->intGainFocus(true);
        else {
            lassert(m_pParent);
            m_pParent->SetFocus(this); // calls child's gainfocus indirectly
        }
    }
}

/*
==============
GPanel::FocusNext

Select the next focussable child.
Returns true if a focussable child has been found.
==============
*/
bool GPanel::FocusNext()
{
    GPanel_lit it, start;

    if (m_Children.empty())
        return false;

    if (m_pFocus)
        start = std::find(m_Children.begin(), m_Children.end(), m_pFocus);
    else
        start = m_Children.end();

    it = start;
    do {
        if (it == m_Children.begin())
            it = m_Children.end();
        else
            it--;

        if (it != m_Children.end() && (*it)->CanFocus()) {
            SetFocus(*it);
            return true;
        }
    } while(it != start);

    return false;
}

/*
==============
GPanel::FocusLast

Select the previous (actually m_logical.next) focussable child
Returns true if a focussable child has been found.
==============
*/
bool GPanel::FocusLast()
{
    GPanel_lit it, start;

    if (m_Children.empty())
        return false;

    if (m_pFocus)
        start = std::find(m_Children.begin(), m_Children.end(), m_pFocus);
    else
        start = m_Children.begin();

    it = start;
    do {
        if (it == m_Children.end())
            it = m_Children.begin();
        else
            it++;

        if (it != m_Children.end() && (*it)->CanFocus()) {
            SetFocus(*it);
            return true;
        }
    } while(it != start);

    return false;
}

/*
==============
GPanel::PanelToScreen
GPanel::ScreenToPanel

Convert coordinates relative to the panel to coordinates relative
to the screen and vice versa
==============
*/
void GPanel::PanelToScreen(int *x, int *y)
{
    m_rc.RectToWorld(x, y);
    if (!m_bTopLevel)
        m_pParent->PanelToScreen(x, y);
}

void GPanel::ScreenToPanel(int *x, int *y)
{
    if (!m_bTopLevel)
        m_pParent->ScreenToPanel(x, y);
    m_rc.WorldToRect(x, y);
}

/*
==============
GPanel::GrabMouse

Toggles mouse grabbing on or off.
If a panel grabs the mouse, it'll be the only one to receive mouse
events until the mouse in un-grabbed.
Only one panel can grab the mouse at a time. This means that the mouse
should only be grabbed as a reaction to a mouse event.
==============
*/
void GPanel::GrabMouse(bool bGrab)
{
    if (!bGrab) {
        s_pGrabMouse = 0;
        m_bGrabbedMouse = false;
    } else {
        s_pGrabMouse = this;
        m_bGrabbedMouse = true;
    }
}

/*
==============
GPanel::Run

Executes the panel modally
==============
*/
int GPanel::Run()
{
    GPanel *pOldModal;
    bool bOldFocussed;
    bool bOldVisible;

    lassert(!m_bModal);
    lassert(CanFocus());

    pOldModal = s_pModal;

    s_pModal = this;
    m_bModal = true;

    m_iEndModal = -1;

    // Modal panels are always focussed and visible
    bOldFocussed = m_bFocussed;
    if (!bOldFocussed)
        intGainFocus(true);

    bOldVisible = IsVisible();
    if (!bOldVisible)
        SetVisible(true);

    try
    {
        static const hal_inputcb_t panel_inputcb = {
            GPanel::cbKey,
            GPanel::cbMouseButton,
            GPanel::cbMouseMove,
            GPanel::cbQuitRequest
        };

        Begin();

        // run the gameloop
        while(m_bModal) {
            hal->StartFrame();

            hal->Ortho(0, 0, 640, 480);

            hal->Input(&panel_inputcb);

            if (!m_bDormant)
                intLogic();

            for(GPanel_rlit tlp = s_TopLevel.rbegin(); tlp != s_TopLevel.rend(); tlp++)
            {
//              for(int i = 0; i < 10; i++)
                    if ((*tlp)->IsVisible())
                        (*tlp)->intDraw();
            }

            intDrawMouse();

            hal->EndFrame();
        }
    }
    catch(...)
    {
        m_bModal = false;
        s_pModal = pOldModal;

        if (!bOldFocussed)
            intGainFocus(false);
        if (!bOldVisible)
            SetVisible(false);

        End();

        throw;
    }

    // cleanup: reset the modal pointer and mouse state
    m_bModal = false;
    s_pModal = pOldModal;

    if (!bOldFocussed)
        intGainFocus(false);
    if (!bOldVisible)
        SetVisible(false);

    End();

    return m_iEndModal;
}


/*
==============
GPanel::intDrawMouse

Draw the mouse if necessary. Called from Run()
==============
*/
void GPanel::intDrawMouse()
{
    GPanel *panel;
    const char *szMouse;
    sprite_t *spr;
    int x, y;

    // Get the mouse cursor name
    panel = this;
    while(panel->m_pMouseIn)
        panel = panel->m_pMouseIn;
    szMouse = panel->MouseCursor();

    if (!szMouse)
        return;

    hal->SetDrawRect(0, 0, 640, 480, 0, 0);

    spr = SPR_Get(szMouse);
    hal->GetMouseState(&x, &y);
    SPR_Draw(spr, x, y, -1, hal->framestart);
    SPR_Free(spr);
}


/*
==============
GPanel::EndModal

Ends the mainloop
==============
*/
void GPanel::EndModal(int iCode)
{
    lassert(m_bModal);

    m_bModal = false;
    m_iEndModal = iCode;
}


/*
==============
GPanel::intLogic

Call our own logic, then children's logic function if desired
==============
*/
void GPanel::intLogic()
{
    lassert(!m_bDormant);

    Logic();

    for(GPanel_lit child = m_Children.begin(); child != m_Children.end(); child++)
        if (!(*child)->m_bDormant)
            (*child)->intLogic();
}

/*
==============
GPanel::MouseCursor

The mouse cursor to be used when this panel becomes modal
==============
*/
const char *GPanel::MouseCursor()
{
    return "mouse";
}

/*
==============
GPanel::Begin
GPanel::End

Called just before and just after the modal loop in Run
==============
*/
void GPanel::Begin()
{
}

void GPanel::End()
{
}

/*
==============
GPanel::Logic

Called every frame to update game logic etc..
Only called if m_bThink is set and m_bDormant is unset.
==============
*/
void GPanel::Logic()
{
}

/*
==============
GPanel::intDraw

Draw self, then all children in Z-order
==============
*/
void GPanel::intDraw()
{
    GPanel *pPanel;
    LRect rc(m_rc.pos[0], m_rc.pos[1], m_rc.size[0], m_rc.size[1]);
    int x, y;

    lassert(m_bVisible);

    x = y = 0;
    pPanel = this;
    while(!pPanel->m_bTopLevel) {
        pPanel = pPanel->m_pParent;
        pPanel->m_rc.ClipLocalOffset(&rc, &x, &y);
        pPanel->m_rc.RectToWorld(&rc.pos[0], &rc.pos[1]);
    }

    if (rc.size[0] <= 0 || rc.size[1] <= 0)
        return;

    hal->SetDrawRect(rc.pos[0], rc.pos[1], rc.size[0], rc.size[1], x, y);

    Draw();

    for(GPanel_rlit pnl = m_Children.rbegin(); pnl != m_Children.rend(); pnl++) {
        if ((*pnl)->m_bTopLevel)
            continue;
        if ((*pnl)->m_bVisible)
            (*pnl)->intDraw();
    }
}

/*
==============
GPanel::Draw

Redraw the panel. Only called if m_bVisible is true.
==============
*/
void GPanel::Draw()
{
}

/*
==============
GPanel::OkayToQuit

Called when a quit event is received from the OS.
Return true if you want to quit.
==============
*/
bool GPanel::OkayToQuit()
{
    return true;
}

/*
==============
GPanel::Key
GPanel::MouseEnter
GPanel::MouseButton
GPanel::MouseMove

Event handling functions.
Return true if the event was used, or false if ignored.
Mouse coordinates are relative to the panel.

The default key handler supports arrow keys and [shift-]tab
for focus cycling.
==============
*/
void GPanel::GainFocus(bool bGain)
{
}

bool GPanel::Key(int code, char c, bool down)
{
    if (!down)
        return false;

    switch(code) {
    case KEY_LEFT:
    case KEY_UP: return FocusLast();
    case KEY_RIGHT:
    case KEY_DOWN: return FocusNext();
    case KEY_TAB:
        {
            byte *keystate = hal->GetKeyState();
            if (keystate[KEY_LSHIFT] || keystate[KEY_RSHIFT])
                return FocusLast();
            else
                return FocusNext();
        }
    }

    return false;
}

void GPanel::MouseEnter(bool bEnter)
{
}

bool GPanel::MouseButton(int btn, int x, int y, bool down)
{
    if (btn == 0) {
        if (!IsFocussed() && CanFocus()) {
            lassert(m_pParent);
            if (down)
                m_pParent->SetFocus(this);
            return true;
        }
    }

    return false;
}

void GPanel::MouseMove(int x, int y)
{
}

/*
==============
GPanel::intOkayToQuit

Query our own and all children's okay-to-quit status. Return true
if everyone's okay with it.
==============
*/
bool GPanel::intOkayToQuit()
{
    for(GPanel_lit child = m_Children.begin(); child != m_Children.end(); child++) {
        if ((*child)->m_bOblivious)
            continue;

        if (!(*child)->intOkayToQuit())
            return false;
    }

    if (!OkayToQuit())
        return false;

    return true;
}

/*
==============
GPanel::intGainFocus

Set the focussed state and propagate the event to the focussed child
==============
*/
void GPanel::intGainFocus(bool bGain)
{
    lassert(m_bFocussed != bGain);

    if (m_pFocus)
        m_pFocus->intGainFocus(bGain);

    m_bFocussed = bGain;

    GainFocus(bGain);
}

/*
==============
GPanel::intKey

Simply dispatch the key to the currently focussed child.
If that doesn't handle the key, call our own handler.
==============
*/
bool GPanel::intKey(int code, char c, bool down)
{
    if (m_pFocus) {
        if (m_pFocus->intKey(code, c, down))
            return true;
    }

    return Key(code, c, down);
}

/*
==============
GPanel::intTrackMouse

Tracks the mouse coordinates down to child panels and sends
MouseEnter/MouseLeave events accordingly.
Returns the child panel which is currently under the mouse (if any).
==============
*/
GPanel *GPanel::intTrackMouse(int x, int y)
{
    GPanel *pChild = 0;

    //L_Printf("%p, trackmouse: %i, %i\n", this, x, y);

    for(GPanel_lit child = m_Children.begin(); child != m_Children.end(); child++) {
        if ((*child)->m_bTopLevel)
            continue;
        if ((*child)->m_bOblivious || !(*child)->m_bVisible)
            continue;
        if (!(*child)->m_rc.Contains(x, y))
            continue;

        pChild = *child;
        break;
    }

    if (m_pMouseIn != pChild) {
        if (m_pMouseIn)
            m_pMouseIn->intMouseEnter(false);

        if (pChild)
            pChild->intMouseEnter(true);

        m_pMouseIn = pChild;
    }

    return pChild;
}

/*
==============
GPanel::intMouseEnter

Relays mouse entering info down to m_pMouseIn, if any
==============
*/
void GPanel::intMouseEnter(bool bEnter)
{
    if (!bEnter && m_pMouseIn) {
        m_pMouseIn->intMouseEnter(false);
        m_pMouseIn = 0;
    }

    m_bHasMouse = bEnter;

    MouseEnter(bEnter);
}

/*
==============
GPanel::intMouseButton
GPanel::intMouseMove

Create MouseEnter events as the mouse moves.
Mouse clicks are relayed to the topmost child at the x/y location.
If it ignores the event, we call our own respective handler function.
Mouse moves are propagated to all children under the respective x/y
coordinates.
==============
*/
bool GPanel::intMouseButton(int btn, int x, int y, bool down)
{
    GPanel *pChild;
    int rx, ry;

    pChild = intTrackMouse(x, y);
    if (pChild) {
        rx = x;
        ry = y;
        pChild->m_rc.WorldToRect(&rx, &ry);
        if (pChild->intMouseButton(btn, rx, ry, down))
            return true;
    }

    return MouseButton(btn, x, y, down);
}

void GPanel::intMouseMove(int x, int y)
{
    int rx, ry;

    intTrackMouse(x, y);

    for(GPanel_lit child = m_Children.begin(); child != m_Children.end(); child++) {
        if ((*child)->m_bTopLevel)
            continue;
        if ((*child)->m_bOblivious || !(*child)->m_bVisible)
            continue;
        if (!(*child)->m_rc.Contains(x, y))
            continue;

        rx = x;
        ry = y;
        (*child)->m_rc.WorldToRect(&rx, &ry);
        (*child)->intMouseMove(rx, ry);
    }

    MouseMove(x, y);
}

/*
==============
GPanel::cbKey
GPanel::cbMouseButton
GPanel::cbMouseMove

These callback functions are simple glue functions which call the
currently modal object's dispatch function
==============
*/
void GPanel::cbKey(int code, char c, bool down)
{
    lassert(s_pModal);

    s_pModal->intKey(code, c, down);
}

GPanel *GPanel::cbTrackMouse(int scrx, int scry)
{
    GPanel *pPanel;
    bool bHasMouse;

    if (!s_pGrabMouse)
        pPanel = s_pModal;
    else
        pPanel = s_pGrabMouse;

    pPanel->ScreenToPanel(&scrx, &scry);

    bHasMouse = (scrx >= 0 && scrx < pPanel->m_rc.size[0] &&
                 scry >= 0 && scry < pPanel->m_rc.size[1]);

    if (bHasMouse != pPanel->m_bHasMouse)
        pPanel->intMouseEnter(bHasMouse);

    if (!s_pGrabMouse && !bHasMouse)
        return 0;

    return pPanel;
}

void GPanel::cbMouseButton(int btn, int scrx, int scry, bool down)
{
    GPanel *pPanel;

    pPanel = cbTrackMouse(scrx, scry);

    if (pPanel) {
        pPanel->ScreenToPanel(&scrx, &scry);
        pPanel->intMouseButton(btn, scrx, scry, down);
    }
}

void GPanel::cbMouseMove(int x, int y)
{
    GPanel *pPanel;

    pPanel = cbTrackMouse(x, y);

    if (pPanel) {
        pPanel->ScreenToPanel(&x, &y);
        pPanel->intMouseMove(x, y);
    }
}

void GPanel::cbQuitRequest()
{
    lassert(s_pModal);

    if (s_pModal->intOkayToQuit())
        s_pModal->EndModal(-1);
}