Xlib tutorial part 14 -- Pie Menus

by Alan at Mon 29th Jun 2009 1:00AM EST

Hello, and welcome to part 14 of this Xlib tutorial.

Another long hiatus, I'm not going to claim we're back this time. We'll see. Summertime might keep me too busy.

This time, I'm going to start on something a litle more fun and interesting: flower menus similar to what you saw in the first version of the Sims. They're a type of pie menu and can be quite pretty as well as functional. Download the code and give it a try so you can see visually what's happening.

First off, we need to set up a way to get to a context menu in the app. So, in app.c change the XSelectInput line to:


XSelectInput(dpy, win, StructureNotifyMask|ExposureMask);

and add a few helper functions:


Window setAppContextMenu(Display *dpy, XContext ctxt, Window w,
              Window contextmenu){
      App *app;
      Window old;
      XFindContext(dpy, w, ctxt, (XPointer *)&app);
      /*printf("storing context menu %p in %p
", contextmenu, app); */
      old = app->contextmenu;
      app->contextmenu = contextmenu;
      return old;
}

static void buttonPress(Block *block, XEvent *ev){
      if (ev->xbutton.button == Button3){
              App *app = &block->app;
              if (!app || !app->contextmenu) return; /* oops */
              /* printf("context menu is %p in %p
",app->contextmenu, app); */
              pieMenuMap(app->contextmenu, ev, app->ctxt);
              XUngrabPointer(ev->xbutton.display, ev->xbutton.time);
      }
}
static void buttonRelease(Block *block, XEvent *ev){
      if (ev->xbutton.button == Button3){
              App *app = &block->app;
              if (!app || !app->contextmenu) return; /* oops */
              XUnmapWindow(ev->xany.display, app->contextmenu);
              /* printf("context menu is %p in %p
",app->contextmenu, app); */
      }
}

A point of interest in this code, is the printf's that are currently commented out. I put them there as I was testing. You can put them in so that you can see what's happening as you click on the application. Of course, just sticking in this code won't let you compile.

First we have to register the callbacks:


static struct Funcs _AppFuncs = {
	appConfigureNotify,
	NULL, /* leave */
	NULL, /* enter */
	NULL, /* expose */
	buttonPress, /* button press */
	buttonRelease /* button release */
};

And add the contextmenu item to the App struct.


struct App {
	struct Funcs *funcs;
	Window menubar; /* so we can keep it the width of the app window */
	int menubarHeight;
	Window contextmenu;

	XContext ctxt;
	int width, height;
	unsigned long border, background, foreground;
};

While there, also add a reference to the setAppContextMenu()

Now, there will be some changes to menus as well, but first let's get the changes in button out of the way. We need to support buttons with centred text. Not left justitfied as we had in the earlier menus. To do that I've added a attribute set when the button is created with the unimaginative name of "center". Variables and fields with obvious names help with later debugging.

In button.c where we had been just setting textx = 0 for the menus (and earlier had (button->width - button->text_width)/2) we want to be able to run it off what center is set to and rather than using an if statement if we set center to 0 when we want the text left justified, and 1 when we want it centered, we can simply multiply the longer value by the field.


              textx = button->center*((button->width - button->text_width)/2);

Okay, now we can look at the changes in menu.c to see what is happening. First, we need to include the shape extension:

#include <X11/extensions/shape.h>

We'll also have add -lXext when we compile. The shape extension allows us to create non-rectangular windows in our application. It was an early extension in X, but unfortunately, you don't see its use very much, except in gimmicky things. I've never quite figured out why.

One of the first things to notice, is that there will be a lot of common code in creating a pie menu as a normal menu. So, I renamed the newMenu() initialization to newXMenu() and added the pointer to the data as one of the arguments. That way, we can reuse pretty much all of the function and have a new newMenu() initialization that allocates the a Menu and calls newXMenu and a new newPieMenu() initialization that does the same for PieMenus, setting the funcs to point to the pie menu methods instead of to the normal menu functions.

I've added a few changes to the menuSetSubWins, including a call to piemenuSetSubWins when calling it with a piemenu. piemenuSetSubWins just sets the menu's width to 0 so that we know it's not set up yet.

Now for the interesting math bits. It turns out that most flower menus seem to work best with ony 6 pedals in the flower. If you get too many items, they bunch too close and it's hard to pick the pedal you actually want. So, what've I done, is calculate the precise best placing for six pedals.


enum { MaxPIES = 6 };
/* these numbers are 6 points evenly spaced around a circle */
static float xs[MaxPIES] = { 0, .86, .86, 0, -.86, -.86};
static float ys[MaxPIES] = { -1, -.5, .5, 1, .5, -.5};

I worked these numbers out with sines and cosines. The xs are the sines of 0, 60, 120, 180, 240, 300, and 260. The ys are the cosines of those same angles.

Next, let's look at what happens when the application asks the menu to reveal itself. (Ie when someone right clicks on the application and app.c's buttonPress is called.)


void pieMenuMap(Window menuwin, XEvent *ev, XContext ctxt){
	int j, menuwidth, menuheight;
	int maxwidth = 0; /* of the subwindows */
	Menu *menu = NULL;
	Button**buttons;
	Pixmap pmap;
	GC gc;

	/*printf("
RETREIVING %p %p %p %p

", ev->xany.display, menuwin, ctxt, menu); */
	XFindContext(ev->xany.display, menuwin, ctxt, (XPointer *)&menu);
	if (!menu) return; /* oops */
	if (menu->width){
		menuwidth = 2.72*menu->width;
		menuheight = 3*menu->width;
		XMoveWindow(ev->xany.display, menuwin,
			ev->xbutton.x_root - menuwidth/2,
			ev->xbutton.y_root - menuheight/2);
		XMapWindow(ev->xany.display, menuwin);
		return;
	}

This much should be straightforward if you've read the previous parts. First we find the Menu object, and return if we don't find it. Then we readjust the menu to show up where the person clicked and map it if the menu has a valid width.


	buttons = malloc(sizeof(*buttons)*menu->nsubws);
	for(j =0; j < menu->nsubws; j ++ ){
		Button *button = NULL;
		if (XFindContext(ev->xany.display, menu->subws[j], menu->ctxt, (XPointer*)&button))
			continue;
		buttons[j] = button;
		/*printf("button %d is %d wide and %d high
",
			j, button->width, button->font_ascent); */
		if (maxwidth < button->text_width + 2 * button->font_ascent);
			maxwidth = button->text_width + 2 * button->font_ascent;
	}

Here we collect the buttons as they were set in piemenuSetSubWins(), and as we go along calculate the widest of them.


	/* width of the complete pie is 2.72 times the width/height of a piece */
	menuwidth = 2.72*maxwidth;
	menuheight = 3*maxwidth;
	XMoveResizeWindow(ev->xany.display, menuwin,
		ev->xbutton.x_root - menuwidth/2,  ev->xbutton.y_root - menuheight/2,
		menuwidth+1, menuheight);
	for(j =0; j < MaxPIES && j < menu->nsubws; j ++ ){
		XMoveResizeWindow(ev->xany.display, menu->subws[j],
			(1.36+xs[j])*maxwidth-maxwidth/2, (1.5+ys[j])*maxwidth-maxwidth/2,
				maxwidth, maxwidth);
	}
	free(buttons);

The numbers 2.72, 3, 1.36, and 1.5 look good on my system, they make the pedals look good. You can try playing with them to get them just how you want. They should both be a least 2 so that the text of the menu item will fit. You'll notice of course that the first pair of numbers is double the second. What we did here was to move all the subwindows into place inside the menu.



	/* now shape it nicely */
	pmap = XCreatePixmap(ev->xany.display, menuwin, menuwidth, menuheight,1);
	if (!pmap) fprintf(stderr, " can't create a pixmap
");

A pixmap is an area on the display server, like a window, except you can't see it, nor can it have sub windows. It's good for storing pictures of things you want the display system to have quick access to. What we're doing here is creating a set of nice roundish circles in a pixmap to use as the template for the main flower menu.



	gc = XCreateGC(ev->xany.display, pmap, 0, NULL);
	XSetForeground(ev->xany.display, gc, 0);
	XFillRectangle(ev->xany.display, pmap, gc, 0, 0, menuwidth, menuheight);
	XSetForeground(ev->xany.display, gc, 1);
	for(j =0; j < MaxPIES && j < menu->nsubws; j ++ ){
		XFillArc(ev->xany.display, pmap, gc,
			(1.36+xs[j])*maxwidth-maxwidth/2, (1.5+ys[j])*maxwidth-maxwidth/2,
			maxwidth, maxwidth, 0, 360*64);
	}
	XShapeCombineMask(ev->xany.display, menuwin, ShapeBounding, 0, 0, pmap, ShapeSet);
	XFreePixmap(ev->xany.display, pmap);

XFillArc can create circles and elipses (among other things), XShapeCombineMask sets the shape of the menu to the set of circles and XFreePixmap is called because we're done with that pixmap. That's important since we don't want to use up the all display system's memory. Now we repeat the setup, but this time, we create a pixmap with only one circle in it and set the shape of the various windows in the menu to it.



	pmap = XCreatePixmap(ev->xany.display, menuwin, maxwidth, maxwidth,1);
	XSetForeground(ev->xany.display, gc, 0);
	XFillRectangle(ev->xany.display, pmap, gc, 0, 0, maxwidth, maxwidth);
	XSetForeground(ev->xany.display, gc, 1);
	XFillArc(ev->xany.display, pmap, gc, 0, 0, maxwidth, maxwidth, 0, 360*64);
	for(j =0; j < MaxPIES && j < menu->nsubws; j ++ ){
		XShapeCombineMask(ev->xany.display, menu->subws[j], 
			ShapeBounding, 0, 0, pmap, ShapeSet);
	}
	XFreePixmap(ev->xany.display, pmap);
	XFreeGC(ev->xany.display, gc);

And then we can map the menu and we'll see the up to six pedals of the flower.




	menu->width = maxwidth;

	XMapWindow(ev->xany.display, menuwin);
}

This has been a rather long section. Next time we'll make them look a bit more interesting.

Comments are closed.