shithub: 9ferno

ref: 21336d544ae90e20070fd7c8be67f3f2c9ccbe38
dir: /libtk/menus.c/

View raw version
#include "lib9.h"
#include "draw.h"
#include "keyboard.h"
#include "tk.h"
#include "frame.h"
#include "label.h"

/*
arrow annotation for choicebutton: how do we make sure
the menu items come up the same size?
	- set menu items to same req.width & height as button itself.

autorepeat:
when we get mouse event at the edge of the screen
and the menu overlaps that edge,
start autorepeat timer to slide the menu the opposite direction.

variable setting + command invocation:
is the value of the variable the text or the index?
same for the value appended to the command, text or index?

if it's reimplemented as a custom widget, how does the custom widget
get notified of variable changes?
*/

/* Widget Commands (+ means implemented)
	+activate
	+add
	+cget
	+configure
	+delete
	+entrycget
	+entryconfigure
	+index
	+insert
	+invoke
	+post
	+postcascade
	+type
	+unpost
	+yposition
*/

#define	O(t, e)		((long)(&((t*)0)->e))

/* Layout constants */
enum {
	Sepheight	= 6,	/* Height of menu separator */
};

#define NOCHOICE "-----"

enum {
	Startspeed = TKI2F(1),
};

static
TkOption mbopts[] =
{
	"text",		OPTtext,	O(TkLabel, text),		nil,
	"anchor",	OPTflag,	O(TkLabel, anchor),	tkanchor,
	"underline",	OPTdist,	O(TkLabel, ul),		nil,
	"justify",	OPTstab,	O(TkLabel, justify),	tkjustify,
	"menu",		OPTtext,	O(TkLabel, menu),		nil,
	"bitmap",	OPTbmap,	O(TkLabel, bitmap),		nil,
	"image",	OPTimag,	O(TkLabel, img),		nil,
	nil
};

static
TkOption choiceopts[] =
{
	"variable",	OPTtext,	O(TkLabel, variable),	nil,
	"values",	OPTlist,	O(TkLabel, values), nil,
	"command", OPTtext, O(TkLabel, command), nil,
	nil
};

static
TkEbind mbbindings[] = 
{
	{TkEnter,		"%W tkMBenter %s"},
	{TkLeave,		"%W tkMBleave"},
	{TkButton1P,		"%W tkMBpress 1"},
	{TkKey,		"%W tkMBkey 0x%K"},
	{TkButton1P|TkMotion,	"%W tkMBpress 0"},
};

extern Rectangle bbnil;
static char* tkmpost(Tk*, int, int, int, int, int);
static void menuclr(Tk*);
static void freemenu(Tk*);
static void appenditem(Tk*, Tk*, int);
static void layout(Tk*);
static Tk* tkmenuindex2ptr(Tk*, char**);
static void activateitem(Tk*);

/*
 * unmap menu cascade upto (but not including) tk
 */
static void
tkunmapmenus(TkTop *top, Tk *tk)
{
	TkTop *t;
	Tk *menu;
	TkWin *tkw;

	menu = top->ctxt->tkmenu;
	if (menu == nil)
		return;
	t = menu->env->top;

	/* if something went wrong, clear down all menus */
	if (tk != nil && tk->env->top != t)
		tk = nil;

	while (menu != nil && menu != tk) {
		menuclr(menu);
		tkunmap(menu);
		tkcancelrepeat(menu);
		tkw = TKobj(TkWin, menu);
		if (tkw->cascade != nil) {
			menu = tklook(t, tkw->cascade, 0);
			free(tkw->cascade);
			tkw->cascade = nil;
		} else
			menu = nil;
	}
	top->ctxt->tkmenu = menu;
	tksetmgrab(top, menu);
}

static void
tkunmapmenu(Tk *tk)
{
	TkTop *t;
	TkWin *tkw;
	Tk *parent;

	parent = nil;
	tkw = TKobj(TkWin, tk);
	t = tk->env->top;
	if (tkw->cascade != nil)
		parent = tklook(t, tkw->cascade, 0);
	tkunmapmenus(t, parent);
	if (tkw->freeonunmap)
		freemenu(tk);
}

static void
tksizemenubutton(Tk *tk)
{
	int w, h;
	char **v, *cur;
	TkLabel *tkl = TKobj(TkLabel, tk);

	tksizelabel(tk);
	if(tk->type != TKchoicebutton)
		return;
	w = tk->req.width;
	h = tk->req.height;
	v = tkl->values;
	if (v == nil || *v == nil)
		return;
	cur = tkl->text;
	for (; *v; v++) {
		tkl->text = *v;
		tksizelabel(tk);
		if (tk->req.width > w)
			w = tk->req.width;
		if (tk->req.height > h)
			h = tk->req.height;
	}
	tkl->text = cur;
	tksizelabel(tk);
	tk->req.width = w;
	tk->req.height = h;
}

static char*
tkmkmenubutton(TkTop *t, char *arg, char **ret, int type, TkOption *opts)
{
	Tk *tk;
	char *e, **v;
	TkName *names;
	TkLabel *tkl;
	TkOptab tko[3];

/* need to get the label from elsewhere */
	tk = tknewobj(t, type, sizeof(Tk)+sizeof(TkLabel));
	if(tk == nil)
		return TkNomem;
	tk->borderwidth = 2;
	tk->flag |= Tknograb;

	tkl = TKobj(TkLabel, tk);
	tkl->ul = -1;
	if(type == TKchoicebutton)
		tkl->anchor = Tknorth|Tkwest;

	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkl;
	tko[1].optab = opts;
	tko[2].ptr = nil;

	names = nil;
	e = tkparse(t, arg, tko, &names);
	if(e != nil) {
		tkfreeobj(tk);
		return e;
	}
	tkl->nvalues = 0;
	if (tkl->values != nil) {
		for (v = tkl->values; *v; v++)
			;
		tkl->nvalues = v - tkl->values;
	}
	if(type == TKchoicebutton){
		if(tkl->nvalues > 0)
			tkl->text = strdup(tkl->values[0]);
		else
			tkl->text = strdup(NOCHOICE);
	}
	tksettransparent(tk, 
		tkhasalpha(tk->env, TkCbackgnd) ||
		tkhasalpha(tk->env, TkCselectbgnd) ||
		tkhasalpha(tk->env, TkCactivebgnd));

	e = tkbindings(t, tk, mbbindings, nelem(mbbindings));

	if(e != nil) {
		tkfreeobj(tk);
		return e;
	}
	tksizemenubutton(tk);

	e = tkaddchild(t, tk, &names);
	tkfreename(names);
	if(e != nil) {
		tkfreeobj(tk);
		return e;
	}
	tk->name->link = nil;

	return tkvalue(ret, "%s", tk->name->name);
}

char*
tkchoicebutton(TkTop *t, char *arg, char **ret)
{
	return tkmkmenubutton(t, arg, ret, TKchoicebutton, choiceopts);
}

char*
tkmenubutton(TkTop *t, char *arg, char **ret)
{
	return tkmkmenubutton(t, arg, ret, TKmenubutton, mbopts);
}

static char*
tkmenubutcget(Tk *tk, char *arg, char **val)
{
	TkOptab tko[3];
	TkLabel *tkl = TKobj(TkLabel, tk);

	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkl;
	tko[1].optab = (tk->type == TKchoicebutton ? choiceopts : mbopts);
	tko[2].ptr = nil;

	return tkgencget(tko, arg, val, tk->env->top);
}

static char*
tkmenubutconf(Tk *tk, char *arg, char **val)
{
	char *e, **v;
	TkGeom g;
	int bd;
	TkOptab tko[3];
	TkLabel *tkl = TKobj(TkLabel, tk);

	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkl;
	tko[1].optab = (tk->type == TKchoicebutton ? choiceopts : mbopts);
	tko[2].ptr = nil;

	if(*arg == '\0')
		return tkconflist(tko, val);

	g = tk->req;
	bd = tk->borderwidth;
	e = tkparse(tk->env->top, arg, tko, nil);

	if (tk->type == TKchoicebutton) {
		tkl->nvalues = 0;
		if (tkl->values != nil) {
			for (v = tkl->values; *v; v++)
				;
			tkl->nvalues = v - tkl->values;
		}
		if (tkl->check >= tkl->nvalues || strcmp(tkl->text, tkl->values[tkl->check])) {
			/*
			 * try to keep selected value the same if possible
			 */
			for (v = tkl->values; v && *v; v++)
				if (!strcmp(*v, tkl->text))
					break;
			free(tkl->text);
			if (v == nil || *v == nil) {
				tkl->text = strdup(tkl->nvalues > 0 ? tkl->values[0] : NOCHOICE);
				tkl->check = 0;
			} else {
				tkl->check = v - tkl->values;
				tkl->text = strdup(*v);
			}
		}
	}
	tksettransparent(tk, 
		tkhasalpha(tk->env, TkCbackgnd) ||
		tkhasalpha(tk->env, TkCselectbgnd) ||
		tkhasalpha(tk->env, TkCactivebgnd));
	tksizemenubutton(tk);
	tkgeomchg(tk, &g, bd);

	tk->dirty = tkrect(tk, 1);
	return e;
}

static char*
tkMBleave(Tk *tk, char *arg, char **val)
{
	USED(arg);
	USED(val);

	tk->flag &= ~Tkactive;
	tk->dirty = tkrect(tk, 1);
	return nil;
}

static Tk*
mkchoicemenu(Tk *tkb)
{
	Tk *menu, *tkc;
	int i;
	TkLabel *tkl, *tkcl;
	TkWin *tkw;
	TkTop *t;

	tkl = TKobj(TkLabel, tkb);
	t = tkb->env->top;

	menu = tknewobj(t, TKmenu, sizeof(Tk)+sizeof(TkWin));
	if(menu == nil)
		return nil;

	menu->relief = TKraised;
	menu->flag |= Tknograb;
	menu->borderwidth = 1;
	tkputenv(menu->env);
	menu->env = tkb->env;
	menu->env->ref++;

	menu->flag |= Tkwindow;
	menu->geom = tkmoveresize;
	tkw = TKobj(TkWin, menu);
	tkw->cbname = strdup(tkb->name->name);
	tkw->di = (void*)-1;			// XXX

	for(i = tkl->nvalues - 1; i >= 0; i--){
		tkc = tknewobj(t, TKlabel, sizeof(Tk)+sizeof(TkLabel));
		/* XXX recover from malloc failure */
		tkc->flag = Tkwest|Tkfillx|Tktop;
		tkc->highlightwidth = 0;
		tkc->borderwidth = 1;
		tkc->relief = TKflat;
		tkputenv(tkc->env);
		tkc->env = tkb->env;
		tkc->env->ref++;
		tkcl = TKobj(TkLabel, tkc);
		tkcl->anchor = Tkwest;
		tkcl->ul = -1;
		tkcl->justify = Tkleft;
		tkcl->text = strdup(tkl->values[i]);
		tkcl->command = smprint("%s invoke %d", tkb->name->name, i);
		/* XXX recover from malloc failure */
		tksizelabel(tkc);
		tkc->req.height = tkb->req.height;
		appenditem(menu, tkc, 0);
	}
	layout(menu);

	tkw->next = t->windows;
	tkw->freeonunmap = 1;
	t->windows = menu;
	return menu;
}

static char*
tkMBpress(Tk *tk, char *arg, char **val)
{
	Tk *menu, *item;
	TkLabel *tkl = TKobj(TkLabel, tk);
	Point g;
	char buf[12], *bufp, *e;

	USED(arg);
	USED(val);

	g = tkposn(tk);
	if (tk->type == TKchoicebutton) {
		menu = mkchoicemenu(tk);
		if (menu == nil)
			return TkNomem;
		sprint(buf, "%d", tkl->check);
		bufp = buf;
		item = tkmenuindex2ptr(menu, &bufp);
		if(item == nil)
			return nil;
		g.y -= item->act.y;
		e = tkmpost(menu, g.x, g.y, 0, 0, 0);
		activateitem(item);
		return e;
	} else {
		if (tkl->menu == nil)
			return nil;
		menu = tklook(tk->env->top, tkl->menu, 0);
		if(menu == nil || menu->type != TKmenu)
			return TkBadwp;

		if(menu->flag & Tkmapped) {
			if(atoi(arg))
				tkunmapmenu(menu);
			return nil;
		}
		return tkmpost(menu, g.x, g.y, 0, tk->act.height + 2*tk->borderwidth, 1);
	}
}

static char*
tkMBkey(Tk *tk, char *arg, char **val)
{
	int key;
	USED(val);

	if(tk->flag & Tkdisabled)
		return nil;

	key = strtol(arg, nil, 0);
	if (key == '\n' || key == ' ')
		return tkMBpress(tk, "1", nil);
	return nil;
}

static char*
tkMBenter(Tk *tk, char *arg, char **val)
{
	USED(arg);
	USED(val);

	tk->flag |= Tkactive;
	tk->dirty = tkrect(tk, 1);
	return nil;
}

static char*
tkchoicebutset(Tk *tk, char *arg, char **val)
{
	char buf[12], *e;
	int v;
	TkLabel *tkl = TKobj(TkLabel, tk);

	USED(val);

	tkword(tk->env->top, arg, buf, buf+sizeof(buf), nil);
	if (*buf == '\0')
		return TkBadvl;
	v = atoi(buf);
	if (v < 0 || v >= tkl->nvalues)
		return TkBadvl;
	if (v == tkl->check)
		return nil;
	free(tkl->text);
	tkl->text = strdup(tkl->values[v]);
	/* XXX recover from malloc error */
	tkl->check = v;

	sprint(buf, "%d", v);
	e = tksetvar(tk->env->top, tkl->variable, buf);
	if(e != nil)
		return e;

	tk->dirty = tkrect(tk, 1);
	return nil;
}

static char*
tkchoicebutinvoke(Tk *tk, char *arg, char **val)
{
	TkLabel *tkl = TKobj(TkLabel, tk);
	char *e;

	e = tkchoicebutset(tk, arg, val);
	if(e != nil)
		return e;
	if(tkl->command)
		return tkexec(tk->env->top, tkl->command, val);
	return nil;
}

static char*
tkchoicebutgetvalue(Tk *tk, char *arg, char **val)
{
	char buf[12];
	int gotarg, v;
	TkLabel *tkl = TKobj(TkLabel, tk);
	if (tkl->nvalues == 0)
		return nil;
	tkword(tk->env->top, arg, buf, buf+sizeof(buf), &gotarg);
	if (!gotarg)
		return tkvalue(val, "%s", tkl->values[tkl->check]);
	v = atoi(buf);
	if (buf[0] < '0' || buf[0] > '9' || v >= tkl->nvalues)
		return TkBadvl;
	return tkvalue(val, "%s", tkl->values[tkl->check]);
}

static char*
tkchoicebutsetvalue(Tk *tk, char *arg, char **val)
{
	char *buf;
	char **v;
	int gotarg;
	TkLabel *tkl = TKobj(TkLabel, tk);

	USED(val);
	if (tkl->nvalues == 0)
		return TkBadvl;
	buf = mallocz(Tkmaxitem, 0);
	if (buf == nil)
		return TkNomem;
	tkword(tk->env->top, arg, buf, buf+Tkmaxitem, &gotarg);
	if (!gotarg) {
		free(buf);
		return TkBadvl;
	}
	for (v = tkl->values; *v; v++)
		if (strcmp(*v, buf) == 0)
			break;
	free(buf);
	if (*v == nil)
		return TkBadvl;
	free(tkl->text);
	tkl->text = strdup(*v);
	/* XXX recover from malloc error */
	tkl->check = v - tkl->values;

	tk->dirty = tkrect(tk, 1);
	return nil;
}

static char*
tkchoicebutget(Tk *tk, char *arg, char **val)
{
	TkLabel *tkl = TKobj(TkLabel, tk);
	char *buf, **v;
	int gotarg;
	
	if (tkl->nvalues == 0)
		return nil;
	buf = mallocz(Tkmaxitem, 0);
	if (buf == nil)
		return TkNomem;
	tkword(tk->env->top, arg, buf, buf+Tkmaxitem, &gotarg);
	if (!gotarg) {
		free(buf);
		return tkvalue(val, "%d", tkl->check);
	}

	for (v = tkl->values; *v; v++)
		if (strcmp(*v, buf) == 0)
			break;
	free(buf);
	if (*v)
		return tkvalue(val, "%d", v - tkl->values);
	return nil;
}

static char*
tkchoicebutvaluecount(Tk *tk, char *arg, char **val)
{
	TkLabel *tkl = TKobj(TkLabel, tk);
	USED(arg);
	return tkvalue(val, "%d", tkl->nvalues);
}


static void
tkchoicevarchanged(Tk *tk, char *var, char *value)
{
	TkLabel *tkl = TKobj(TkLabel, tk);
	int v;

	if(tkl->variable != nil && strcmp(tkl->variable, var) == 0){
		if(value[0] < '0' || value[0] > '9')
			return;
		v = atoi(value);
		if(v < 0 || v >= tkl->nvalues)
			return;		/* what else can we do? */
		free(tkl->text);
		tkl->text = strdup(tkl->values[v]);
		/* XXX recover from malloc error */
		tkl->check = v;
		tk->dirty = tkrect(tk, 0);
		tkdirty(tk);
	}
}

Tk *
tkfindchoicemenu(Tk *tkb)
{
	Tk *tk, *next;
	TkTop *top;
	TkWin *tkw;

	top = tkb->env->top;
	for (tk = top->windows; tk != nil; tk = next){
		tkw = TKobj(TkWin, tk);
		if(tk->name == nil){
			assert(strcmp(tkw->cbname, tkb->name->name) == 0);
			return tk;
		}
		next = tkw->next;
	}
	return nil;
}

static
TkOption menuopt[] =
{
	"postcommand",	OPTtext,	O(TkWin, postcmd),		nil,
	nil,
};

char*
tkmenu(TkTop *t, char *arg, char **ret)
{
	Tk *tk;
	char *e;
	TkWin *tkw;
	TkName *names;
	TkOptab tko[3];

	tk = tknewobj(t, TKmenu, sizeof(Tk)+sizeof(TkWin));
	if(tk == nil)
		return TkNomem;

	tkw = TKobj(TkWin, tk);
	tkw->di = (void*)-1;		// XXX
	tk->relief = TKraised;
	tk->flag |= Tknograb;
	tk->borderwidth = 1;

	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkw;
	tko[1].optab = menuopt;
	tko[2].ptr = nil;

	names = nil;
	e = tkparse(t, arg, tko, &names);
	if(e != nil) {
		tkfreeobj(tk);
		return e;
	}

	e = tkaddchild(t, tk, &names);
	tkfreename(names);
	if(e != nil) {
		tkfreeobj(tk);
		return e;
	}
	tk->name->link = nil;

	tk->flag |= Tkwindow;
	tk->geom = tkmoveresize;

	tkw->next = t->windows;
	t->windows = tk;

	return tkvalue(ret, "%s", tk->name->name);
}

static void
freemenu(Tk *top)
{
	Tk *tk, *f, *nexttk, *nextf;
	TkWin *tkw;

	tkunmapmenu(top);
	tkw = TKobj(TkWin, top);
	for(tk = tkw->slave; tk; tk = nexttk) {
		nexttk = tk->next;
		for(f = tk->slave; f; f = nextf) {
			nextf = f->next;
			tkfreeobj(f);
		}
		tkfreeobj(tk);
	}
	top->slave = nil;
	tkfreeframe(top);
}

static
TkOption mopt[] =
{
	"menu",		OPTtext,	O(TkLabel, menu),		nil,
	nil,
};

static void
tkbuildmopt(TkOptab *tko, int n, Tk *tk)
{
	memset(tko, 0, n*sizeof(TkOptab));

	n = 0;
	tko[n].ptr = tk;
	tko[n++].optab = tkgeneric;

	switch(tk->type) {
	case TKcascade:
		tko[n].ptr = TKobj(TkLabel, tk);
		tko[n++].optab = mopt;
		goto norm;
	case TKradiobutton:	
		tko[n].ptr = TKobj(TkLabel, tk);
		tko[n++].optab = tkradopts;
		goto norm;
	case TKcheckbutton:
		tko[n].ptr = TKobj(TkLabel, tk);
		tko[n++].optab = tkcbopts;
		/* fall through */
	case TKlabel:
	norm:
		tko[n].ptr = TKobj(TkLabel, tk);
		tko[n].optab = tkbutopts;
		break;	
	}
}

static char*
tkmenuentryconf(Tk *menu, Tk *tk, char *arg)
{
	char *e;
	TkOptab tko[4];

	USED(menu);

	tkbuildmopt(tko, nelem(tko), tk);
	e = tkparse(tk->env->top, arg, tko, nil);
	switch (tk->type) {
	case TKlabel:
	case TKcascade:
		tksizelabel(tk);
		break;
	case TKradiobutton:
	case TKcheckbutton:
		tksizebutton(tk);
	}

	return e;
}

static void
layout(Tk *menu)
{
	TkWin *tkw;
	Tk *tk;
	int m, w, y, maxmargin, maxw;

	y = 0;
	maxmargin = 0;
	maxw = 0;

	tkw = TKobj(TkWin, menu);

	/* determine padding for item text alignment */
	for (tk = tkw->slave; tk != nil; tk = tk->next) {
		m = tkbuttonmargin(tk);	/* TO DO: relies on buttonmargin defaulting to labelmargin */
		tk->act.x = m;		/* temp store */
		if (m > maxmargin)
			maxmargin = m;
	}
	/* set x pos and determine max width */
	for (tk = tkw->slave; tk != nil; tk = tk->next) {
		tk->act.x = tk->borderwidth + maxmargin - tk->act.x;
		tk->act.y = y + tk->borderwidth;
		tk->act.height = tk->req.height;
		tk->act.width = tk->req.width;
		y += tk->act.height+2*tk->borderwidth;
		w = tk->act.x + tk->req.width + 2* tk->borderwidth;
		if (w > maxw)
			maxw = w;
	}
	/* expand separators and cascades and mark all as dirty */
	for (tk = tkw->slave; tk != nil; tk = tk->next) {
		switch (tk->type) {
		case TKseparator:
			tk->act.x = tk->borderwidth;
			/*FALLTHRU*/
		case TKcascade:
			tk->act.width = (maxw - tk->act.x) - tk->borderwidth;
		}
		tk->dirty = tkrect(tk, 1);
	}
	menu->dirty = tkrect(menu, 1);
	tkmoveresize(menu, 0, 0, maxw, y);
}

static void
menuitemgeom(Tk *sub, int x, int y, int w, int h)
{
	if (sub->parent == nil)
		return;
	if(w < 0)
		w = 0;
	if(h < 0)
		h = 0;
	sub->req.x = x;
	sub->req.y = y;
	sub->req.width = w;
	sub->req.height = h;
	layout(sub->parent);
}

static void
appenditem(Tk *menu, Tk *item, int where)
{
	TkWin *tkw;
	Tk *f, **l;

	tkw = TKobj(TkWin, menu);
	l = &tkw->slave;
	for (f = *l; f != nil; f = f->next) {
		if (where-- == 0)
			break;
		l = &f->next;
	}
	*l = item;
	item->next = f;
	item->parent = menu;
	item->geom = menuitemgeom;
}

static char*
menuadd(Tk *menu, char *arg, int where)
{
	Tk *tkc;
	int configure;
	char *e;
	TkTop *t;
	TkLabel *tkl;
	char buf[Tkmaxitem];
	
	t = menu->env->top;
	arg = tkword(t, arg, buf, buf+sizeof(buf), nil);
	configure = 1;
	e = nil;

	if(strcmp(buf, "checkbutton") == 0)
		tkc = tkmkbutton(t, TKcheckbutton);
	else if(strcmp(buf, "radiobutton") == 0)
		tkc = tkmkbutton(t, TKradiobutton);
	else if(strcmp(buf, "command") == 0)
		tkc = tknewobj(t, TKlabel, sizeof(Tk)+sizeof(TkLabel));
	else if(strcmp(buf, "cascade") == 0)
		tkc = tknewobj(t, TKcascade, sizeof(Tk)+sizeof(TkLabel));
	else if(strcmp(buf, "separator") == 0) {
		tkc = tknewobj(t, TKseparator, sizeof(Tk));	/* it's really a frame */
		if (tkc != nil) {
			tkc->flag = Tkfillx|Tktop;
			tkc->req.height = Sepheight;
			configure = 0;
		}
	}
	else
		return TkBadvl;

	if (tkc == nil)
		e = TkNomem;

	if (e == nil) {
		if(tkc->env == t->env && menu->env != t->env) {
			tkputenv(tkc->env);
			tkc->env = menu->env;
			tkc->env->ref++;
		}
		if (configure) {
			tkc->flag = Tkwest|Tkfillx|Tktop;
			tkc->highlightwidth = 0;
			tkc->borderwidth = 1;
			tkc->relief = TKflat;
			tkl = TKobj(TkLabel, tkc);
			tkl->anchor = Tkwest;
			tkl->ul = -1;
			tkl->justify = Tkleft;
			e = tkmenuentryconf(menu, tkc, arg);
		}
	}

	if(e != nil) {
		if (tkc != nil)
			tkfreeobj(tkc);
		return e;
	}	

	appenditem(menu, tkc, where);
	layout(menu);
	return nil;
}

static int
tkmindex(Tk *tk, char *p)
{
	TkWin *tkw;
	int y, n;

	if(*p >= '0' && *p <= '9')
		return atoi(p);

	tkw = TKobj(TkWin, tk);
	n = 0;
	if(*p == '@') {
		y = atoi(p+1);
		for(tk = tkw->slave; tk; tk = tk->next) {
			if(y >= tk->act.y && y < tk->act.y+tk->act.height+2*tk->borderwidth )
				return n;
			n++;
		}
	}
	if(strcmp(p, "end") == 0 || strcmp(p, "last") == 0) {
		for(tk = tkw->slave; tk && tk->next; tk = tk->next)
			n++;
		return n;
	}
	if(strcmp(p, "active") == 0) {
		for(tk = tkw->slave; tk; tk = tk->next) {
			if(tk->flag & Tkactive)
				return n;
			n++;
		}
		return -2;
	}
	if(strcmp(p, "none") == 0)
		return -2;

	return -1;
}

static int
tkmenudel(Tk *tk, int y)
{
	TkWin *tkw;
	Tk *f, **l, *next;

	tkw = TKobj(TkWin, tk);
	l = &tkw->slave;
	for(tk = *l; tk; tk = tk->next) {
		if(y-- == 0) {
			*l = tk->next;
			for(f = tk->slave; f; f = next) {
				next = f->next;
				tkfreeobj(f);
			}
			tkfreeobj(tk);
			return 1;
		}
		l = &tk->next;
	}
	return 0;	
}

static char*
tkmpost(Tk *tk, int x, int y, int cascade, int bh, int adjust)
{
	char *e;
	TkWin *w;
	TkTop *t;
	Rectangle *dr;

	t = tk->env->top;
	if(adjust){
		dr = &t->screenr;
		if(x+tk->act.width > dr->max.x)
			x = dr->max.x - tk->act.width;
		if(x < 0)
			x = 0;
		if(y+bh+tk->act.height > dr->max.y)
			y -= tk->act.height + 2* tk->borderwidth;
		else
			y += bh;
		if(y < 0)
			y = 0;
	}
	menuclr(tk);
	tkmovewin(tk, Pt(x, y));

	/* stop possible postcommand recursion */
	if (tk->flag & Tkmapped)
		return nil;

	w = TKobj(TkWin, tk);
	if(w->postcmd != nil) {
		e = tkexec(tk->env->top, w->postcmd, nil);
		if(e != nil) {
			print("%s: postcommand: %s: %s\n", tkname(tk), w->postcmd, e);
			return e;
		}
	}
	if (!cascade)
		tkunmapmenus(t, nil);

	e = tkmap(tk);
	if(e != nil)
		return e;

	if (t->ctxt->tkmenu != nil)
		w->cascade = strdup(t->ctxt->tkmenu->name->name);
	t->ctxt->tkmenu = tk;
	tksetmgrab(t, tk);

	/* Make sure slaves are redrawn */
	return tkupdate(tk->env->top);
}

static Tk*
tkmenuindex2ptr(Tk *tk, char **arg)
{
	TkWin *tkw;
	int index;
	char *buf;

	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return nil;
	*arg = tkword(tk->env->top, *arg, buf, buf+Tkmaxitem, nil);
	index = tkmindex(tk, buf);
	free(buf);
	if(index < 0)
		return nil;

	tkw = TKobj(TkWin, tk);
	for(tk = tkw->slave; tk && index; tk = tk->next)
		index--;

	if(tk == nil)
		return nil;

	return tk;
}

static char*
tkmenuentrycget(Tk *tk, char *arg, char **val)
{
	Tk *etk;
	TkOptab tko[4];

	etk = tkmenuindex2ptr(tk, &arg);
	if(etk == nil)
		return TkBadix;

	tkbuildmopt(tko, nelem(tko), etk);
	return tkgencget(tko, arg, val, tk->env->top);
}

static char*
tkmenucget(Tk *tk, char *arg, char **val)
{
	TkWin *tkw;
	TkOptab tko[4];

	tkw = TKobj(TkWin, tk);
	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkw;
	tko[1].optab = tktop;
	tko[2].ptr = tkw;
	tko[2].optab = menuopt;
	tko[3].ptr = nil;

	return tkgencget(tko, arg, val, tk->env->top);
}

static char*
tkmenuconf(Tk *tk, char *arg, char **val)
{
	char *e;
	TkGeom g;
	int bd;
	TkWin *tkw;
	TkOptab tko[3];

	tkw = TKobj(TkWin, tk);
	tko[0].ptr = tk;
	tko[0].optab = tkgeneric;
	tko[1].ptr = tkw;
	tko[1].optab = menuopt;
	tko[2].ptr = nil;

	if(*arg == '\0')
		return tkconflist(tko, val);

	g = tk->req;
	bd = tk->borderwidth;
	e = tkparse(tk->env->top, arg, tko, nil);
	tkgeomchg(tk, &g, bd);
	tk->dirty = tkrect(tk, 1);
	return e;
}

static char*
tkmenuadd(Tk *tk, char *arg, char **val)
{
	USED(val);
	return menuadd(tk, arg, -1);	
}

static char*
tkmenuinsert(Tk *tk, char *arg, char **val)
{
	int index;
	char *buf;

	USED(val);
	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return TkNomem;
	arg = tkword(tk->env->top, arg, buf, buf+Tkmaxitem, nil);
	index = tkmindex(tk, buf);
	free(buf);
	if (index < 0)
		return TkBadix;
	return menuadd(tk, arg, index);
}

static void
menuitemdirty(Tk *item)
{
	Tk *menu;
	Rectangle r;

	menu = item->parent;
	if (menu == nil)
		return;
	item->dirty = tkrect(item, 1);
	r = rectaddpt(item->dirty, Pt(item->act.x, item->act.y));
	combinerect(&menu->dirty, r);
}

static void
menuclr(Tk *tk)
{
	TkWin *tkw;
	Tk *f;
	tkw = TKobj(TkWin, tk);
	for(f = tkw->slave; f; f = f->next) {
		if(f->flag & Tkactive) {
			f->flag &= ~Tkactive;
			menuitemdirty(f);
		}
	}
}

static char*
tkpostcascade(Tk *parent, Tk *tk, int toggle)
{
	Tk *tkm;
	TkWin *tkw;
	Point g;
	TkTop *t;
	TkLabel *tkl;
	char *e;

	if(tk->flag & Tkdisabled)
		return nil;

	tkl = TKobj(TkLabel, tk);
	t = tk->env->top;
	tkm = tklook(t, tkl->menu, 0);
	if(tkm == nil || tkm->type != TKmenu)
		return TkBadwp;

	if((tkm->flag & Tkmapped)) {
		if (toggle) {
			tkunmapmenus(t, parent);
			return nil;
		} else {
			/* check that it is immediate cascade */
			tkw = TKobj(TkWin, t->ctxt->tkmenu);
			if (strcmp(tkw->cascade, parent->name->name) == 0)
				return nil;
		}
	}

	tkunmapmenus(t, parent);

	tkl = TKobj(TkLabel, tk);
	if(tkl->command != nil) {
		e = tkexec(t, tkl->command, nil);
		if (e != nil)
			return e;
	}

	g = tkposn(tk);
	g.x += tk->act.width;
	g.y -= tkm->borderwidth;
	e = tkmpost(tkm, g.x, g.y, 1, 0, 1);
	return e;
}

static void
activateitem(Tk *item)
{
	Tk *menu;
	if (item == nil || (menu = item->parent) == nil)
		return;
	menuclr(menu);
	if (!(item->flag & Tkdisabled)) {
		item->flag |= Tkactive;
		menuitemdirty(item);
	}
}

static char*
tkmenuactivate(Tk *tk, char *arg, char **val)
{
	Tk *f;
	TkWin *tkw;
	int index;
	char *buf;
	
	USED(val);
	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return TkNomem;
	tkword(tk->env->top, arg, buf, buf+Tkmaxitem, nil);
	index = tkmindex(tk, buf);
	free(buf);
	if (index == -1)
		return TkBadix;
	if (index == -2) {
		menuclr(tk);
		return nil;
	}

	tkw = TKobj(TkWin, tk);
	for(f = tkw->slave; f; f = f->next)
		if(index-- == 0)
			break;

	if(f == nil || f->flag & Tkdisabled) {
		menuclr(tk);
		return nil;
	}
	if(f->flag & Tkactive)
		return nil;

	activateitem(f);
	return nil;
}

static int
iteminvoke(Tk *tk, Tk *tki, char *arg)
{
	int unmap = 0;
	menuitemdirty(tki);
	switch(tki->type) {
	case TKlabel:
		unmap = 1;
	case TKcheckbutton:
	case TKradiobutton:
		tkbuttoninvoke(tki, arg, nil);
		break;
	case TKcascade:
		tkpostcascade(tk, tki, 0);
		break;
	}
	return unmap;
}

static char*
tkmenuinvoke(Tk *tk, char *arg, char **val)
{
	Tk *tki;
	USED(val);
	tki = tkmenuindex2ptr(tk, &arg);
	if(tki == nil)
		return nil;
	iteminvoke(tk, tki, arg);
	return nil;
}

static char*
tkmenudelete(Tk *tk, char *arg, char **val)
{
	int index1, index2;
	char *buf;

	USED(val);
	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return TkNomem;
	arg = tkitem(buf, arg);
	index1 = tkmindex(tk, buf);
	if(index1 < 0) {
		free(buf);
		return TkBadix;
	}
	index2 = index1;
	if(*arg != '\0') {
		tkitem(buf, arg);
		index2 = tkmindex(tk, buf);
	}
	free(buf);
	if(index2 < 0)
		return TkBadix;
	while(index2 >= index1 && tkmenudel(tk, index2))
		index2--;

	layout(tk);
	return nil;
}

static char*
tkmenupost(Tk *tk, char *arg, char **val)
{
	int x, y;
	TkTop *t;
	char *buf;

	USED(val);

	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return TkNomem;
	t = tk->env->top;
	arg = tkword(t, arg, buf, buf+Tkmaxitem, nil);
	if(buf[0] == '\0') {
		free(buf);
		return TkBadvl;
	}
	x = atoi(buf);
	tkword(t, arg, buf, buf+Tkmaxitem, nil);
	if(buf[0] == '\0') {
		free(buf);
		return TkBadvl;
	}
	y = atoi(buf);
	free(buf);

	return tkmpost(tk, x, y, 0, 0, 1);
}

static char*
tkmenuunpost(Tk *tk, char *arg, char **val)
{
	USED(arg);
	USED(val);
	tkunmapmenu(tk);
	return nil;
}

static char*
tkmenuindex(Tk *tk, char *arg, char **val)
{
	char *buf;
	int index;

	buf = mallocz(Tkmaxitem, 0);
	if(buf == nil)
		return TkNomem;
	tkword(tk->env->top, arg, buf, buf+Tkmaxitem, nil);
	index = tkmindex(tk, buf);
	free(buf);
	if (index == -1)
		return TkBadix;
	if (index == -2)
		return "none";
	return tkvalue(val, "%d", index);
}

static char*
tkmenuyposn(Tk *tk, char *arg, char **val)
{
	tk = tkmenuindex2ptr(tk, &arg);
	if(tk == nil)
		return TkBadix;
	return tkvalue(val, "%d", tk->act.y);
}

static char*
tkmenupostcascade(Tk *tk, char *arg, char **val)
{
	Tk *tki;
	USED(val);
	tki = tkmenuindex2ptr(tk, &arg);
	if(tki == nil || tki->type != TKcascade)
		return nil;

	return tkpostcascade(tk, tki, 0);
}

static char*
tkmenutype(Tk *tk, char *arg, char **val)
{
	tk = tkmenuindex2ptr(tk, &arg);
	if(tk == nil)
		return TkBadix;

	return tkvalue(val, tk->type == TKlabel ? "command" : tkmethod[tk->type]->name);
}

static char*
tkmenususpend(Tk *tk, char *arg, char **val)
{
	USED(arg);
	USED(val);
	if(tk->type == TKchoicebutton){
		tk = tkfindchoicemenu(tk);
		if(tk == nil)
			return TkNotwm;
	}
	tk->flag |= Tksuspended;
	return nil;
}

static char*
tkmenuentryconfig(Tk *tk, char *arg, char **val)
{
	Tk *etk;
	char *e;

	USED(val);
	etk = tkmenuindex2ptr(tk, &arg);
	if(etk == nil)
		return TkBadix;

	e = tkmenuentryconf(tk, etk, arg);
	layout(tk);
	return e;
}

static Tk*
xymenuitem(Tk *tk, int x, int y)
{
	TkWin *tkw = TKobj(TkWin, tk);
	x -= tkw->act.x;
	y -= tkw->act.y;

	x -= tk->borderwidth;
	y -= tk->act.y + tk->borderwidth;
	if (x < tk->act.x || x > tk->act.x+tk->act.width)
		return nil;
	for(tk = tkw->slave; tk; tk = tk->next) {
		if(y >= tk->act.y && y < tk->act.y+tk->act.height+2*tk->borderwidth)
			return tk;
	}
	return nil;
}

static char *
menukey(Tk *tk, int key)
{
	Tk *scan, *active, *first, *last, *prev, *next;
	TkWin *tkw;
	TkTop *top;

	top = tk->env->top;

	active = first = last = prev = next = nil;
	tkw = TKobj(TkWin, tk);
	for(scan = tkw->slave; scan != nil; scan = scan->next) {
		if(scan->type == TKseparator)
			continue;
		if(first == nil)
			first = scan;
		if (active != nil && next == nil)
			next = scan;
		if(active == nil && scan->flag & Tkactive)
			active = scan;
		if (active == nil)
			prev = scan;
		last = scan;
	}
	if (next == nil)
		next = first;
	if (prev == nil)
		prev = last;

	switch (key) {
	case Esc:
		tkunmapmenus(top, nil);
		break;
	case Left:
		if (tkw->cascade != nil)
			tkunmapmenu(tk);
		break;
	case Right:
		if (active == nil || active->type != TKcascade)
			break;
	case ' ':
	case '\n':
		if (active != nil) {
			if (iteminvoke(tk, active, nil))
				tkunmapmenus(top, nil);
		}
		break;
	case Up:
		next = prev;
	case Down:
		if (next != nil)
			activateitem(next);
	}
	return nil;
}

static char*
drawmenu(Tk *tk, Point orig)
{
	Image *dst;
	TkWin *tkw;
	Tk *sub;
	Point p, bd;
	int bg;
	Rectangle mainr, clientr, subr;

	tkw = TKobj(TkWin, tk);
	dst = tkimageof(tk);

	bd = Pt(tk->borderwidth, tk->borderwidth);
	mainr.min = addpt(orig, Pt(tk->act.x, tk->act.y));
	clientr.min = addpt(mainr.min, bd);
	clientr.max = addpt(clientr.min, Pt(tk->act.width, tk->act.height));
	mainr.max = addpt(clientr.max, bd);

	/*
	 * note that we draw item background to get full menu width
	 * active indicator, this means we must dirty the entire
	 * item rectangle to ensure it is fully redrawn
	 */
	p = clientr.min;
	subr = clientr;
	for (sub = tkw->slave; sub != nil; sub = sub->next) {
		if (Dx(sub->dirty) == 0)
			continue;
		subr.min.y = p.y + sub->act.y - sub->borderwidth;
		subr.max.y = p.y + sub->act.y + sub->act.height + sub->borderwidth;
		bg = TkCbackgnd;
		if (sub->flag & Tkactive)
			bg = TkCactivebgnd;
		draw(dst, subr, tkgc(sub->env, bg), nil, ZP);
		sub->dirty = tkrect(sub, 1);
		sub->flag |= Tkrefresh;
		tkmethod[sub->type]->draw(sub, p);
		sub->dirty = bbnil;
		sub->flag &= ~Tkrefresh;
	}
	/* todo: dirty check */
	tkdrawrelief(dst, tk, mainr.min, TkCbackgnd, tk->relief);
	return nil;
}

static void
menudirty(Tk *sub)
{
	menuitemdirty(sub);
}

static Point
menurelpos(Tk *sub)
{
	return Pt(sub->act.x-sub->borderwidth, sub->act.y-sub->borderwidth);
}

static void
autoscroll(Tk *tk, void *v, int cancelled)
{
	TkWin *tkw;
	Rectangle r, dr;
	Point delta, od;
	TkMouse *m;
	Tk *item;
	USED(v);

	tkw = TKobj(TkWin, tk);
	if (cancelled) {
		tkw->speed = 0;
		return;
	}
	if(!eqpt(tkw->act, tkw->req)){
print("not autoscrolling, act: %P, req: %P\n", tkw->act, tkw->req);
		return;
}
	dr = tk->env->top->screenr;
	delta.x = TKF2I(tkw->delta.x * tkw->speed);
	delta.y = TKF2I(tkw->delta.y * tkw->speed);
	r = rectaddpt(tkrect(tk, 1), Pt(tk->borderwidth + tkw->act.x, tk->borderwidth + tkw->act.y));

	od = delta;
	/* make sure we don't go too far */
	if (delta.x > 0 && r.min.x + delta.x > dr.min.x)
		delta.x = dr.min.x - r.min.x;
	else if (delta.x < 0 && r.max.x + delta.x < dr.max.x)
		delta.x = dr.max.x - r.max.x;
	if (delta.y > 0 && r.min.y + delta.y > dr.min.y)
		delta.y = dr.min.y - r.min.y;
	else if (delta.y < 0 && r.max.y + delta.y < dr.max.y)
		delta.y = dr.max.y - r.max.y;

	m = &tk->env->top->ctxt->mstate;
	item = xymenuitem(tk, m->x - delta.x, m->y - delta.y);
	if (item == nil)
		menuclr(tk);
	else
		activateitem(item);
	tkmovewin(tk, Pt(tkw->req.x + delta.x, tkw->req.y + delta.y));
	tkupdate(tk->env->top);
	/* tkenterleave won't do this for us, so we have to do it ourselves */

	tkw->speed += tkw->speed / 3;

	r = rectaddpt(tkrect(tk, 1), Pt(tk->borderwidth + tkw->act.x, tk->borderwidth + tkw->act.y));
	if((delta.y > 0 && r.min.x >= dr.min.x) || (delta.x < 0 && r.max.x <= dr.max.x))
		tkw->delta.x = 0;
	if((delta.y > 0 && r.min.y >= dr.min.y) || (delta.y < 0 && r.max.y <= dr.max.y))
		tkw->delta.y = 0;
	if (eqpt(tkw->delta, ZP)) {
		tkcancelrepeat(tk);
		tkw->speed = 0;
	}
}

static void
startautoscroll(Tk *tk, TkMouse *m)
{
	Rectangle dr, r;
	Point d;
	TkWin *tkw;
	tkw = TKobj(TkWin, tk);
	dr = tk->env->top->screenr;
	r = rectaddpt(tkrect(tk, 1), Pt(tk->borderwidth + tkw->act.x, tk->borderwidth + tkw->act.y));
	d = Pt(0, 0);
	if(m->x <= 0 && r.min.x < dr.min.x)
		d.x = 1;
	else if (m->x >= dr.max.x - 1 && r.max.x >= dr.max.x)
		d.x = -1;
	if(m->y <= 0 && r.min.y < dr.min.y)
		d.y = 1;
	else if (m->y >= dr.max.y - 1 && r.max.y >= dr.max.y)
		d.y = -1;
//print("startautoscroll, delta %P\n", d);
	if (d.x == 0 && d.y == 0){
		if (tkw->speed > 0){
			tkcancelrepeat(tk);
			tkw->speed = 0;
		}
		return;
	}
	if (tkw->speed == 0) {
		tkw->speed = TKI2F(Dy(r)) / 100;
		tkrepeat(tk, autoscroll, nil, 0, TkRptinterval/2);
	}
	tkw->delta = d;
}

static void
menuevent1(Tk *tk, int event, void *a)
{
	TkMouse *m;
	Tk *item;

	if (event & TkKey) {
		menukey(tk, event & 0xffff);
		return;
	}

	if (event & TkLeave) {
		menuclr(tk);
		return;
	}

	if ((!(event & TkEmouse) || (event & TkTakefocus)) && !(event & TkEnter))
		return;

	m = (TkMouse*)a;

	startautoscroll(tk, m);

	item = xymenuitem(tk, m->x, m->y);
	if (item == nil)
		menuclr(tk);
	else
		activateitem(item);
	if ((event & (TkMotion|TkEnter)) && item == nil)
		return;
	if (event & TkEpress) {
		if (item == nil) {
			tkunmapmenus(tk->env->top, nil);
			return;
		}
		if (item->type == TKcascade)
			tkpostcascade(tk, item, !(event & TkMotion));
		else
			tkunmapmenus(tk->env->top, tk);
		return;
	}
	if ((event & TkErelease) && m->b == 0) {
		if (item != nil) {
			if (item->type == TKcascade)
				return;
			if (!iteminvoke(tk, item, nil))
				return;
		}
		tkunmapmenus(tk->env->top, nil);
	}
}

static Tk*
menuevent(Tk *tk, int event, void *a)
{
	menuevent1(tk, event, a);
	tksubdeliver(tk, tk->binds, event, a, 0);
	return nil;
}

static
TkCmdtab menucmd[] =
{
	"activate",		tkmenuactivate,
	"add",			tkmenuadd,
	"cget",			tkmenucget,
	"configure",		tkmenuconf,
	"delete",		tkmenudelete,
	"entryconfigure",	tkmenuentryconfig,
	"entrycget",		tkmenuentrycget,
	"index",		tkmenuindex,
	"insert",		tkmenuinsert,
	"invoke",		tkmenuinvoke,
	"post",			tkmenupost,
	"postcascade",		tkmenupostcascade,
	"type",			tkmenutype,
	"unpost",		tkmenuunpost,
	"yposition",		tkmenuyposn,
	"suspend",		tkmenususpend,
	nil
};

static
TkCmdtab menubutcmd[] =
{
	"cget",			tkmenubutcget,
	"configure",		tkmenubutconf,
	"tkMBenter",		tkMBenter,
	"tkMBleave",		tkMBleave,
	"tkMBpress",		tkMBpress,
	"tkMBkey",		tkMBkey,
	nil
};

static
TkCmdtab choicebutcmd[] =
{
	"cget",			tkmenubutcget,
	"configure",		tkmenubutconf,
	"set",			tkchoicebutset,
	"get",			tkchoicebutget,
	"setvalue",		tkchoicebutsetvalue,
	"getvalue",		tkchoicebutgetvalue,
	"invoke",			tkchoicebutinvoke,
	"valuecount",		tkchoicebutvaluecount,
	"tkMBenter",		tkMBenter,
	"tkMBleave",		tkMBleave,
	"tkMBpress",		tkMBpress,
	"tkMBkey",		tkMBkey,
	"suspend",		tkmenususpend,
	nil
};

TkMethod menumethod = {
	"menu",
	menucmd,
	freemenu,
	drawmenu,
	nil,
	nil,
	nil,
	menudirty,
	menurelpos,
	menuevent
};

TkMethod menubuttonmethod = {
	"menubutton",
	menubutcmd,
	tkfreelabel,
	tkdrawlabel
};

TkMethod choicebuttonmethod = {
	"choicebutton",
	choicebutcmd,
	tkfreelabel,
	tkdrawlabel,
	nil,
	nil,
	nil,
	nil,
	nil,
	nil,
	nil,
	nil,
	tkchoicevarchanged
};

TkMethod separatormethod = {
	"separator",
	nil,
	tkfreeframe,
	tkdrawframe
};

TkMethod cascademethod = {
	"cascade",
	nil,
	tkfreelabel,
	tkdrawlabel
};