/* menu.c - proper 3D menus
   Copyright (C) 1996-2000 Paul Sheer

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307, USA.
 */


#include <config.h>
#include <stdio.h>
#include <my_string.h>
#include <stdlib.h>
#include <stdarg.h>

#include <X11/Intrinsic.h>
#include "lkeysym.h"

#include "stringtools.h"
#include "app_glob.c"
#include "coolwidget.h"
#include "coollocal.h"

#include "mad.h"

extern struct look *look;

CWidget *current_pulled_button = 0;

int menu_grabbed = 0;

int eh_menu (CWidget * w, XEvent * xevent, CEvent * cwevent);
void CMenuSelectionDialog (CWidget * button);

static void render_menu_button (CWidget * wdt);

void destroy_menu (CWidget * w)
{
    int i;
    if (!w)
	return;
    if (!w->menu)
	return;
    for (i = 0; i < w->numlines; i++)
	if (w->menu[i].text)
	    free (w->menu[i].text);
    free (w->menu);
}

/* 
   Gets y1 and y2 of the entry of a menu (i.e. a menu item). x1, y1,
   x2, y2 would describe the rectangle that encloses that entry,
   relative to the top left corner of the pulled-down menu window.
 */
void get_menu_item_extents (int n, int j, struct menu_item m[], int *border, int *relief, int *y1, int *y2)
{
    (*look->get_menu_item_extents) (n, j, m, border, relief, y1, y2);
}

/*
   Returns 0-(n-1) where n is the number of entries in that menu. Returns
   which menu item the pointer is in if (x,y) is the pointer pos. Returns
   -1 if out of bounds or between menu items.
 */
int whereis_pointer (int x, int y, int w, int n, struct menu_item m[])
{
    int i, y1, y2, border, relief;
    for (i = 0; i < n; i++) {
	if (!m[i].text[2])
	    continue;
	get_menu_item_extents (n, i, m, &border, &relief, &y1, &y2);
	if (y >= y1) {
	    if (y < y2 && x >= border && x < w - border)
		return i;
	} else
	    break;
    }
    return -1;
}

int find_menu_hotkey (struct menu_item m[], int this, int num)
{
    unsigned char used_keys[256];
    int n = 0, j;
    if (!num)
	return 0;
    for (j = 0; j < num; j++)
	if (m[j].hot_key && j != this)
	    used_keys[n++] = my_lower_case (m[j].hot_key);
    return find_letter_at_word_start ((unsigned char *) m[this].text + 1, used_keys, n);
}

void menu_draw (Window win, int w, int h, struct menu_item m[], int n, int light)
{
    (*look->menu_draw) (win, w, h, m, n, light);
}

void render_menu (CWidget * w)
{
    int n, border, relief, y1, y2, i;
    unsigned new_width, new_height;
    if (!w)
	return;
    n = w->numlines;
    get_menu_item_extents (n, n - 1, w->menu, &border, &relief, &y1, &y2);
    new_height = y2 + border;
    new_width = 0;
    for (i = 0; i < n; i++) {
	int t;
	t = CImageStringWidth (w->menu[i].text);
	t += CImageStringWidth ("W");
	if (new_width < t)
	    new_width = t;
    }
    new_width += (border + relief) * 2;
    if (w->width != new_width || w->height != new_height) {
	w->width = new_width;
	w->height = new_height;
	XResizeWindow (CDisplay, w->winid, w->width, w->height);
    }
#if 0
    if (w->y + w->height > HeightOfScreen (DefaultScreenOfDisplay (CDisplay))) {
	int y;
	y = HeightOfScreen (DefaultScreenOfDisplay (CDisplay)) - w->height;
	if (y < 0)
	    y = 0;
	XMoveWindow (CDisplay, w->winid, w->x, y);
    }
#endif
#define BOUND_LIMITS 50
    get_menu_item_extents (n, w->current, w->menu, &border, &relief, &y1, &y2);
    if (w->current >= 0) {
	if (y2 + w->y + BOUND_LIMITS >= HeightOfScreen (DefaultScreenOfDisplay (CDisplay)))
	    CSetWidgetPosition (w->ident, w->x, HeightOfScreen (DefaultScreenOfDisplay (CDisplay)) - y2 - BOUND_LIMITS);
	if (y1 + w->y < BOUND_LIMITS)
	    CSetWidgetPosition (w->ident, w->x, BOUND_LIMITS - y1);
    }
    w->droppedmenu->current = w->current;
    menu_draw (w->winid, w->width, w->height, w->menu, w->numlines, w->current);
}

/* gets a windows position relative to the origin if some ancestor windows,
   window can be of a widget or not */
void CGetWindowPosition (Window win, Window ancestor, int *x_return, int *y_return)
{
    CWidget *w = (CWidget *) 1;
    int x = 0, y = 0;
    Window root, parent, *children;
    unsigned int nchildren, width, height, bd, depth;
    *x_return = *y_return = 0;
    if (win == ancestor)
	return;
    for (;;) {
	if (w)
	    w = CWidgetOfWindow (win);
	if (w)
	    if (w->parentid == CRoot)
		w = 0;
	if (w) {
	    parent = w->parentid;
	    x = w->x;
	    y = w->y;
	} else {
	    if (!XQueryTree (CDisplay, win, &root, &parent, &children, &nchildren))
		return;
	    if (children)
		XFree ((char *) children);
	    XGetGeometry (CDisplay, win, &root, &x, &y, &width, &height, &bd, &depth);
	}
	*x_return += x;
	*y_return += y;
	if (parent == ancestor || parent == CRoot)
	    break;
	win = parent;
    }
}

void focus_stack_remove_window (Window w);

void pull_up (CWidget * button)
{
    if (!button)
	return;
    if (button->kind != C_MENU_BUTTON_WIDGET)
	return;
    if (button->droppedmenu) {
	current_pulled_button = 0;
	CDestroyWidget (button->droppedmenu->ident);
	button->droppedmenu = 0;
    }
    focus_stack_remove_window (button->winid);
    render_menu_button (button);
}

static CWidget *last_menu = 0;

void CSetLastMenu (CWidget *button)
{
    last_menu = button;
}

CWidget *CGetLastMenu (void)
{
    return last_menu;
}

void menu_hand_cursor (Window win);

static CWidget *pull_down (CWidget * button) /* must create a new widget */
{
    CWidget *menu;
    CWidget *sib;
    int width, height, n;
    int x, y;

    if(button->droppedmenu)
	return 0;

    sib = CGetLastMenu ();
    if (sib)
	if (strcmp (button->ident, sib->ident))
	    pull_up (sib);			/* pull up last menu if different */

    sib = button;
    while((sib = CNextFocus (sib)) != button)	/* pull up any other sibling menus */
	pull_up (sib);

    CSetLastMenu (button);

    n = button->numlines;

    CGetWindowPosition (button->winid, CRoot, &x, &y);

    height = 2;	/* don't know what these are yet */
    width = 2;

    x += button->firstcolumn;

    menu = CSetupWidget (catstrs (button->ident, ".pull", 0), CRoot, x,
	   y + button->height, width, height, C_MENU_WIDGET, INPUT_KEY, COLOR_FLAT, 0);
    menu->options |= (button->options & MENU_AUTO_PULL_UP);

    menu_hand_cursor (menu->winid);

/* no destroy 'cos gets ->menu gets destroyed by other menu-button-widget */
    menu->numlines = n;
    menu->menu = button->menu;
    menu->eh = eh_menu;
    menu->droppedmenu = button;
    button->droppedmenu = menu;
    current_pulled_button = button;
    render_menu_button (button);
    return menu;
}

void CPullDown (CWidget * button)
{
    pull_down (button);
}

CWidget *get_pulled_menu (void)
{
    return current_pulled_button;
}

void CPullUp (CWidget * button)
{
    pull_up (button);
}

int execute_item (CWidget * menu, int item)
{
    int r = 0;
    char ident[33];
    strcpy (ident, menu->ident);
    menu->droppedmenu->current = item;
    XUngrabPointer (CDisplay, CurrentTime);
    XUnmapWindow (CDisplay, menu->winid);
    if (item >= 0 && item < menu->numlines)
	if (menu->menu[item].call_back) {
	    menu->droppedmenu->current = item;
	    (*(menu->menu[item].call_back)) (menu->menu[item].data);
	    r = 1;
	}
    menu = CIdent (ident);	/* menu may not exists anymore */
    if (menu)
	pull_up (menu->droppedmenu);
    CFocusLast ();
    return r;
}

static void render_menu_button (CWidget * wdt)
{
    (*look->render_menu_button) (wdt);
}


int is_focus_change_key (KeySym k, int command);
int is_focus_prev_key (KeySym k, int command, unsigned int state);

int eh_menubutton (CWidget * w, XEvent * xevent, CEvent * cwevent)
{
    int c;
    CWidget *f;
    switch (xevent->type) {
    case FocusOut:
    case FocusIn:
	render_menu_button (w);
	CExposeWindowArea (w->parentid, 0, w->x - WIDGET_FOCUS_RING, w->y - WIDGET_FOCUS_RING, w->width + WIDGET_FOCUS_RING * 2, w->height + WIDGET_FOCUS_RING * 2);
	break;
    case KeyPress:
	c = CKeySym (xevent);
	if (!w->droppedmenu) {
	    if (c == XK_space || c == XK_Return || c == XK_KP_Enter || cwevent->command == CK_Down) {
		CMenuSelectionDialog (w);
		return 1;
	    }
	}
	if (c == XK_Escape) {
	    pull_up (w);
	    CFocusLast ();
	    return 1;
	}
	if (cwevent->command == CK_Up && w->droppedmenu) {
	    if (w->droppedmenu->numlines < 1)
		return 1;
	    if (w->droppedmenu->current == -1)
		w->droppedmenu->current = 0;
	    do {
		w->droppedmenu->current = (w->droppedmenu->current + w->droppedmenu->numlines - 1) % w->droppedmenu->numlines;
	    } while (w->droppedmenu->menu[w->droppedmenu->current].text[2] == 0);
	    render_menu (w->droppedmenu);
	    return 1;
	}
	if (cwevent->command == CK_Down && w->droppedmenu) {
	    if (w->droppedmenu->numlines < 1)
		return 1;
	    do {
		w->droppedmenu->current = (w->droppedmenu->current + 1) % w->droppedmenu->numlines;
	    } while (w->droppedmenu->menu[w->droppedmenu->current].text[2] == 0);
	    render_menu (w->droppedmenu);
	    return 1;
	}
	if (is_focus_prev_key (c, cwevent->command, xevent->xkey.state)) {
	    f = CPreviousFocus (w);
	    while (f->kind != C_MENU_BUTTON_WIDGET && (unsigned long) f != (unsigned long) w)
		f = CPreviousFocus (f);
	    if (f) {
		CFocus (f);
		if (w->droppedmenu)
		    CMenuSelectionDialog (f);
	    }
	    return 1;
	}
	if (is_focus_change_key (c, cwevent->command)) {
	    f = CNextFocus (w);
	    while (f->kind != C_MENU_BUTTON_WIDGET && (unsigned long) f != (unsigned long) w)
		f = CNextFocus (f);
	    if (f) {
		CFocus (f);
		if (w->droppedmenu)
		    CMenuSelectionDialog (f);
	    }
	    return 1;
	}
	if (w->droppedmenu && c) {
	    int i;
	    if (c == XK_Return || c == XK_KP_Enter || c == XK_space) {
		return execute_item (w->droppedmenu, w->droppedmenu->current);
	    } else {
		for (i = 0; i < w->droppedmenu->numlines; i++)
		    if (match_hotkey (c, w->droppedmenu->menu[i].hot_key))
			return execute_item (w->droppedmenu, i);
	    }
	}
	if (cwevent->command != CK_Up && cwevent->command != CK_Down)
	    return 0;
    case ButtonPress:
	if (xevent->type == ButtonPress) {
	    w->options &= 0xFFFFFFFFUL - BUTTON_PRESSED - BUTTON_HIGHLIGHT;
	    w->options |= BUTTON_PRESSED;
	}
	render_menu_button (w);
	if (!w->droppedmenu)
	    CMenuSelectionDialog (w);
	return 1;
	break;
    case ButtonRelease:
	w->options &= 0xFFFFFFFFUL - BUTTON_PRESSED - BUTTON_HIGHLIGHT;
	w->options |= BUTTON_HIGHLIGHT;
	render_menu_button (w);
	return 1;
    case MotionNotify:
	if (!w->droppedmenu && menu_grabbed) {
	    pull_down (w);
	    CFocus (w);
	}
	return 1;
    case EnterNotify:
	w->options &= 0xFFFFFFFFUL - BUTTON_PRESSED - BUTTON_HIGHLIGHT;
	w->options |= BUTTON_HIGHLIGHT;
	render_menu_button (w);
	break;
    case Expose:
	if (xevent->xexpose.count)
	    break;
    case LeaveNotify:
	w->options &= 0xFFFFFFFFUL - BUTTON_PRESSED - BUTTON_HIGHLIGHT;
	render_menu_button (w);
	break;
    }
    return 0;
}



int eh_menu (CWidget * w, XEvent * xevent, CEvent * cwevent)
{
    static Window win = 0;
    static int current = -30000;
    switch (xevent->type) {
    case MotionNotify:
	w->current = whereis_pointer (xevent->xmotion.x, xevent->xmotion.y, w->width, w->numlines, w->menu);
	if (w->current == current && w->winid == win)
	    break;
	current = w->current;
	win = w->winid;
	render_menu (w);
	break;
    case ButtonRelease:
	return execute_item (w, whereis_pointer (xevent->xmotion.x, xevent->xmotion.y, w->width, w->numlines, w->menu));
    case ButtonPress:
	w->current = whereis_pointer (xevent->xmotion.x, xevent->xmotion.y, w->width, w->numlines, w->menu);
	render_menu (w);
	break;
    case Expose:
	if (xevent->xexpose.count)
	    break;
    case LeaveNotify:
	current = w->current = w->droppedmenu->current;
	render_menu (w);
	break;
    }
    return 0;
}

CWidget *CDrawMenuButton (const char *ident, Window parent, Window focus_return,
   int x, int y, int width, int height, int num_items, const char *label,...)
{
    va_list ap;
    CWidget *wdt;
    struct menu_item *m;
    int i;
    int w, h;

    if (width == AUTO_WIDTH || height == AUTO_HEIGHT)
	CTextSize (&w, &h, label);
    if (width == AUTO_WIDTH)
	width = w + 4 + BUTTON_RELIEF * 2;
    if (height == AUTO_HEIGHT)
	height = h + 4 + BUTTON_RELIEF * 2;

    wdt = CSetupWidget (ident, parent, x, y,
			width, height, C_MENU_BUTTON_WIDGET, INPUT_KEY | OwnerGrabButtonMask, COLOR_FLAT, 1);
    wdt->options |= MENU_AUTO_PULL_UP;		/* default */

    set_hint_pos (x + width, y + height + WIDGET_SPACING);
    wdt->label = (char *) strdup (label);
    wdt->hotkey = find_hotkey (wdt);
    wdt->options |= WIDGET_HOTKEY_ACTIVATES;

    m = CMalloc ((num_items ? num_items : 1) * sizeof (struct menu_item));

    va_start (ap, label);
    for (i = 0; i < num_items; i++) {
	char *text;
	text = va_arg (ap, char *);
	text = text ? text : "";
	m[i].text = (char *) strdup (catstrs (" ", text, " ", 0));
	m[i].hot_key = va_arg (ap, int);
	m[i].call_back = va_arg (ap, callfn);
	m[i].data = va_arg (ap, unsigned long);
    }
    va_end (ap);

    wdt->destroy = destroy_menu;
    wdt->numlines = num_items;
    wdt->menu = m;
    wdt->eh = eh_menubutton;

    return wdt;
}

void insert_menu_item (CWidget * w, int i, const char *text, int hot_key, callfn call_back, unsigned long data)
{
    struct menu_item *m;

    m = CMalloc ((w->numlines + 1) * sizeof (struct menu_item));
    memcpy (m, w->menu, i * sizeof (struct menu_item));
    memcpy (m + i + 1, w->menu + i, (w->numlines - i) * sizeof (struct menu_item));
    free (w->menu);
    w->menu = m;
    m[i].text = (char *) strdup (catstrs (" ", text, " ", 0));
    m[i].hot_key = hot_key;
    m[i].call_back = call_back;
    m[i].data = data;

    w->numlines++;

    if (w->droppedmenu != 0) {
	w->droppedmenu->menu = m;
	w->droppedmenu->numlines = w->numlines;
	w->droppedmenu->current = w->current;
	render_menu (w->droppedmenu);
    }
}

void CAddMenuItem (const char *ident, const char *text, int hot_key, callfn call_back, unsigned long data)
{
    CWidget *w;
    w = CIdent (ident);
    if (!w) {
	CErrorDialog (0, 0, 0, _ (" Add Menu Item "), " %s: %s ", _ ("No such menu"), ident);
	return;
    }
    insert_menu_item (w, w->numlines, text, hot_key, call_back, data);
}

void CInsertMenuItem (const char *ident, const char *after, const char *text, int hot_key, callfn call_back, unsigned long data)
{
    int i;
    CWidget *w;
    w = CIdent (ident);
    if (!w) {
	CErrorDialog (0, 0, 0, _ (" Insert Menu Item "), " %s: %s ", _ ("No such menu"), ident);
	return;
    }
    i = CHasMenuItem (ident, after);
    if (i < 0) {
	CErrorDialog (0, 0, 0, _ (" Insert Menu Item "), " %s: %s ", _ ("No such item"), after);
	return;
    }
    insert_menu_item (w, i, text, hot_key, call_back, data);
}

void CInsertMenuItemAfter (const char *ident, const char *after, const char *text, int hot_key, callfn call_back, unsigned long data)
{
    int i;
    CWidget *w;
    w = CIdent (ident);
    if (!w) {
	CErrorDialog (0, 0, 0, _ (" Insert Menu Item "), " %s: %s ", _ ("No such menu"), ident);
	return;
    }
    i = CHasMenuItem (ident, after);
    if (i < 0) {
	CErrorDialog (0, 0, 0, _ (" Insert Menu Item "), " %s: %s ", _ ("No such item"), after);
	return;
    }
    insert_menu_item (w, i + 1, text, hot_key, call_back, data);
}

static void remove_item (CWidget * w, int i)
{
    if (!w)
	return;
    if (i >= w->numlines || i < 0)
	return;
    if (w->menu[i].text)
	free (w->menu[i].text);
    w->numlines--;
    memmove (&w->menu[i], &w->menu[i + 1], (w->numlines - i) * sizeof (struct menu_item));
    if (w->current == i)
	w->current = -1;
    else if (w->current > i)
	w->current--;
    if (w->droppedmenu != 0) {
	w->droppedmenu->numlines = w->numlines;
	w->droppedmenu->current = w->current;
    }
}

void CRemoveMenuItemNumber (const char *ident, int i)
{
    CWidget *w;
    w = CIdent (ident);
    remove_item (w, i);
}

/*
   Starts from the bottom of the menu and searches for the first
   menu item containing text (strstr != NULL), and returns the
   integer.
 */
int CHasMenuItem (const char *ident, const char *text)
{
    CWidget *w;
    int i;
    w = CIdent (ident);
    if (!w)
	return -1;
    if (w->numlines)
	for (i = w->numlines - 1; i >= 0; i--)
	    if (strstr (w->menu[i].text, text) || !*text)
		return i;
    return -1;
}

/*
   Starts from the bottom of the menu and searches for the first
   menu item containing text (strstr != NULL), and deletes it.
 */
void CRemoveMenuItem (const char *ident, const char *text)
{
    remove_item (CIdent (ident), CHasMenuItem (ident, text));
}

void CReplaceMenuItem (const char *ident, const char *old_text, const char *new_text, int hot_key, callfn call_back, unsigned long data)
{
    struct menu_item *m;
    CWidget *w;
    int i;

    w = CIdent (ident);
    if (!w) {
	CErrorDialog (0, 0, 0, _ (" Replace Menu Item "), " %s: %s ", _ ("No such menu"), ident);
	return;
    }
    i = CHasMenuItem (ident, old_text);
    if (i < 0) {
	CErrorDialog (0, 0, 0, _ (" Replace Menu Item "), " %s: %s ", _ ("No such item"), old_text);
	return;
    }
    m = w->menu;
    free (m[i].text);
    m[i].text = (char *) strdup (catstrs (" ", new_text, " ", 0));
    m[i].hot_key = hot_key;
    m[i].call_back = call_back;
    m[i].data = data;

    if (w->droppedmenu != 0)
	render_menu (w->droppedmenu);
}

void CMenuSelectionDialog (CWidget * button)
{
    CEvent cwevent;
    XEvent xevent;

/* sanity check */
    if (!button)
	return;

/* drop the menu and focus on the button */
    pull_down (button);
    CFocus (button);

/* we are already inside this function */
    if (menu_grabbed)
	return;
    menu_grabbed = 1;

/* grab the pointer - we want events even when pointer is over other apps windows */
    XGrabPointer
	(CDisplay, button->winid, True,
	 ButtonPressMask | ButtonReleaseMask | ButtonMotionMask | PointerMotionMask |
	 EnterWindowMask | LeaveWindowMask | OwnerGrabButtonMask, GrabModeAsync,
	 GrabModeAsync, None, CGetCursorID (CURSOR_MENU), CurrentTime);

/* current_pulled_button could go to zero if some handler does a pull_up of the menu */
    while (current_pulled_button) {
	CNextEvent (&xevent, &cwevent);
/* terminate the loop if we have a button press/release as the user expects */
	if (xevent.type == ButtonRelease || xevent.type == ButtonPress) {
	    CWidget *w;
	    w = CWidgetOfWindow (xevent.xbutton.window);
	    if (!w)
		break;
	    if (w->kind != C_MENU_BUTTON_WIDGET && w->kind != C_MENU_WIDGET)
		break;
	    if (xevent.xbutton.x >= w->width || xevent.xbutton.x < 0 ||
		xevent.xbutton.y >= w->height || xevent.xbutton.y < 0)
		break;
	}
    }

/* check if any menu up */
    if (current_pulled_button) {
	pull_up (current_pulled_button);
	current_pulled_button = 0;
    }

/* ungrab and return focus */
    menu_grabbed = 0;
    XUngrabPointer (CDisplay, CurrentTime);
    CFocusLast ();
}


