/*- * Copyright (c) 2001 Jordan DeLong * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the author nor the names of contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include "menu.h" /* per-screen structure */ static struct menuscr { GC menugc; } *menuscr; /* font to use */ #ifdef I18N static XFontSet menufont; static XFontSetExtents *menufont_extents; #else static XFontStruct *menufont = NULL; #endif /* decoration group */ static dgroup_t *menu_dgroup = NULL; /* pixmaps */ static pixmap_t *submenu_bullet = NULL; /* stacking layer */ int menu_stacklayer; /* XContext to get menus */ XContext menu_context; /* initialize the menu system */ int menu_init(char *fontname, dgroup_t *dgroup, pixmap_t *bullet) { XGCValues gcvalues; int i, cnt; /* this is used to get menu_t's from windows */ menu_context = XUniqueContext(); /* * get the font structure, fall back on a semi-decent * font, and if we can't even get that just use 'fixed', * which should always be there. If we can't use 'fixed' * we give up. */ if (fontname) { #ifdef I18N menufont = XLoadQueryFontSet(display, fontname); #else menufont = XLoadQueryFont(display, fontname); #endif if (menufont) goto gotfont; PWARN("unable to get requested menu_font, trying default"); } PWARN("using default font"); #ifdef I18N menufont = XLoadQueryFontSet(display, "-*-helvetica-medium-r-normal-*-12-*-*-*-*-*-*-*"); #else menufont = XLoadQueryFont(display, "-*-helvetica-medium-r-normal-*-12-*-*-*-*-*-*-*"); #endif if (menufont) goto gotfont; PWARN("failed to load default font; trying 'fixed' as last resort"); #ifdef I18N menufont = XLoadQueryFontSet(display, "fixed"); #else menufont = XLoadQueryFont(display, "fixed"); #endif if (menufont) goto gotfont; PWARN("failed to load font 'fixed', giving up on menus"); return -1; gotfont: #ifdef I18N menufont_extents = XExtentsOfFontSet(menufont); #endif /* get the per-screen structure */ cnt = ScreenCount(display); menuscr = calloc(cnt, sizeof(struct menuscr)); if (!menuscr) return -1; /* fill in the structure */ for (i = 0; i < cnt; i++) { /* * XXX: color needs to be in a parameter */ gcvalues.foreground = WhitePixel(display, i); gcvalues.background = BlackPixel(display, i); #ifdef I18N menuscr[i].menugc = XCreateGC(display, RootWindow(display, i), GCForeground | GCBackground, &gcvalues); #else gcvalues.font = menufont->fid; menuscr[i].menugc = XCreateGC(display, RootWindow(display, i), GCFont | GCForeground | GCBackground, &gcvalues); #endif } menu_dgroup = dgroup; submenu_bullet = bullet; return 0; } /* shutdown the menu system */ void menu_shutdown() { int i, cnt; cnt = ScreenCount(display); if (menuscr) { for (i = 0; i < cnt; i++) if (menuscr[i].menugc) XFreeGC(display, menuscr[i].menugc); free(menuscr); } if (menufont) #ifdef I18N XFreeFontSet(display, menufont); #else XFreeFont(display, menufont); #endif } /* create a menu */ menu_t *menu_create() { menu_t *menu; menu = calloc(1, sizeof(menu_t)); if (!menu) return NULL; menu->forefather = menu; return menu; } /* add an entry to a menu, pos is the position to add it at */ menuent_t *menu_addent(menu_t *menu, int pos, int type, char *text, void *dat) { menuent_t *ent; menuent_t **tmp; menu_t **tmpsubmenus; int i; ent = calloc(1, sizeof(menuent_t)); if (!ent) return NULL; ent->type = type; ent->text = text; /* get space for the new entry */ tmp = realloc(menu->entries, (menu->entry_count + 1) * sizeof(menuent_t *)); if (!tmp) goto free1; menu->entries = tmp; menu->entry_count++; /* submenus need some special attention */ if (type == ET_SUBMENU) { tmpsubmenus = realloc(menu->submenus, (menu->submenu_count + 1) * sizeof(menu_t *)); if (!tmpsubmenus) goto free2; menu->submenus = tmpsubmenus; menu->submenus[menu->submenu_count] = (menu_t *) dat; ent->dat.submenu = menu->submenu_count++; /* set the forefather pointer, and set it on all subwindows of this one */ ((menu_t *) dat)->forefather = menu->forefather; for (i = 0; i < ((menu_t *) dat)->submenu_count; i++) ((menu_t *) dat)->submenus[i]->forefather = menu->forefather; } else ent->dat.dat = dat; /* move to make space if necessary, and put it in */ if (pos == POS_LAST || pos >= menu->entry_count) pos = menu->entry_count - 1; else memmove(&menu->entries[pos + 1], &menu->entries[pos], (menu->entry_count - pos) * sizeof(menuent_t *)); menu->entries[pos] = ent; return ent; free2: menu->entry_count--; free1: free(ent); return NULL; } /* dealloc a menu entry */ void menu_freeent(menuent_t *ent) { free(ent->text); /* free entry-type specific data */ switch (ent->type) { case ET_RESTART: case ET_COMMAND: if (ent->dat.cmd) free(ent->dat.cmd); break; default: break; } free(ent); } /* resize a menu */ void menu_size(menu_t *menu) { screen_t *screen; int width, height; int strwidth, i; width = MENU_MINWIDTH; height = MENU_YBORDER * 2; for (i = 0; i < menu->entry_count; i++) { #ifdef I18N height += menufont_extents->max_ink_extent.height; strwidth = XmbTextEscapement(menufont, menu->entries[i]->text, strlen(menu->entries[i]->text)); #else height += menufont->ascent + menufont->descent; strwidth = XTextWidth(menufont, menu->entries[i]->text, strlen(menu->entries[i]->text)); #endif if (submenu_bullet && menu->entries[i]->type == ET_SUBMENU) strwidth += submenu_bullet->width; if (width < strwidth) width = strwidth; } width += MENU_XBORDER * 2; /* fill in the client_t for each screen */ TAILQ_FOREACH(screen, &screen_list, s_list) { menu->client[screen->num]->width = width; menu->client[screen->num]->height = height; client_sizeframe(menu->client[screen->num]); } } /* realize a menu (called from post-init, i.e. start) */ int menu_realize(menu_t *menu) { XSetWindowAttributes attr; clientflags_t flags; screen_t *screen; Window wnd; int i; menu->client = calloc(screen_count, sizeof(client_t *)); if (!menu->client) return -1; menu->active_sub = calloc(screen_count, sizeof(menu_t *)); if (!menu->active_sub) goto free; bzero(&flags, sizeof(clientflags_t)); flags.internal = 1; flags.nofocus = 1; flags.noresize = 1; flags.noiconify = 1; flags.nodelete = 1; flags.sticky = 1; TAILQ_FOREACH(screen, &screen_list, s_list) { attr.background_pixel = BlackPixel(display, screen->num); wnd = XCreateWindow(display, RootWindow(display, screen->num), 0, 0, 50, 50, 0, CopyFromParent, CopyFromParent, CopyFromParent, CWBackPixel, &attr); menu->client[screen->num] = client_add(screen, wnd, &flags, menu_dgroup); if (!menu->client[screen->num]) return -1; /* set layer */ menu->client[screen->num]->stacklayer = menu_stacklayer; XSaveContext(display, menu->client[screen->num]->frame, menu_context, (XPointer) menu); XSelectInput(display, menu->client[screen->num]->window, ExposureMask | ButtonPressMask | ButtonReleaseMask | EnterWindowMask); plugin_setcontext(plugin_this, menu->client[screen->num]->window); XMapWindow(display, menu->client[screen->num]->window); } menu_size(menu); /* realize the submenus */ for (i = 0; i < menu->submenu_count; i++) menu_realize(menu->submenus[i]); return 0; free: free(menu->client); return -1; } /* kill a menu, this may be before it's realized, so a lot is conditional */ void menu_delete(menu_t *menu) { int cnt, i; /* kill submenus */ for (i = 0; i < menu->submenu_count; i++) menu_delete(menu->submenus[i]); if (menu->submenus) free(menu->submenus); /* get rid of the menu entries */ for (i = 0; i < menu->entry_count; i++) if (menu->entries[i]) menu_freeent(menu->entries[i]); if (menu->entries) free(menu->entries); /* free created clients */ if (menu->client) { cnt = ScreenCount(display); for (i = 0; i < cnt; i++) { plugin_rmcontext(menu->client[i]->window); XDeleteContext(display, menu->client[i]->frame, menu_context); if (menu->client[i]) client_rm(menu->client[i]); } free(menu->client); } /* free the active submenu pointers */ if (menu->active_sub) free(menu->active_sub); free(menu); } /* draw a menu entry */ static void menu_drawent(menu_t *menu, client_t *client, int i, int y) { int tmpx, tmpy; if (submenu_bullet && menu->entries[i]->type == ET_SUBMENU) { tmpx = client->width - submenu_bullet->width; #ifdef I18N tmpy = y + (((menufont_extents->max_ink_extent.height) / 2) - submenu_bullet->height / 2); #else tmpy = y + (((menufont->ascent + menufont->descent) / 2) - submenu_bullet->height / 2); #endif XSetClipMask(display, menuscr[client->screen->num].menugc, submenu_bullet->shapemasks[client->screen->num]); XSetClipOrigin(display, menuscr[client->screen->num].menugc, tmpx, tmpy); XCopyArea(display, submenu_bullet->pixmaps[client->screen->num], client->window, menuscr[client->screen->num].menugc, 0, 0, submenu_bullet->width, submenu_bullet->height, tmpx, tmpy); XSetClipMask(display, menuscr[client->screen->num].menugc, None); } #ifdef I18N XmbDrawString(display, client->window, menufont, menuscr[client->screen->num].menugc, MENU_XBORDER, y + menufont_extents->max_logical_extent.height * 4 / 5, menu->entries[i]->text, strlen(menu->entries[i]->text)); #else XDrawString(display, client->window, menuscr[client->screen->num].menugc, MENU_XBORDER, y + menufont->ascent, menu->entries[i]->text, strlen(menu->entries[i]->text)); #endif } /* draw a menu; entidx is for use during menu_interact */ void menu_expose(menu_t *menu, client_t *client, XExposeEvent *e) { int i, y, first, last; /* * be slightly intelligent about what lines to draw; it's * probably not worth the time to worry about the width * of the line getting drawn... */ y = MENU_YBORDER; last = first = -1; for (i = 0; i < menu->entry_count; i++) { #ifdef I18N y += menufont_extents->max_ink_extent.height; #else y += menufont->ascent + menufont->descent; #endif if (y > e->y && first == -1) first = i - 1; if (y > e->y + e->height && last == -1) last = i; } /* now we draw the lines that need it */ if (first < 0) first = 0; if (last == -1) last = menu->entry_count - 1; #ifdef I18N y = MENU_YBORDER + (menufont_extents->max_ink_extent.height) * first; #else y = MENU_YBORDER + (menufont->ascent + menufont->descent) * first; #endif for (i = first; i < last + 1; i++) { menu_drawent(menu, client, i, y); #ifdef I18N y += menufont_extents->max_ink_extent.height; #else y += menufont->ascent + menufont->descent; #endif } } /* perform an action for a menu entry */ static void menu_action(menu_t *menu, menuent_t *ent, int snum) { switch (ent->type) { case ET_COMMAND: action_exec(menu->client[snum]->screen->num, ent->dat.cmd); break; case ET_RESTART: if (ent->dat.cmd) restart_bin = ent->dat.cmd; else restart_bin = binary_name; /* fall through */ case ET_EXIT: restart_flag = 1; break; case ET_ABORT: /* this is useful for debug */ if (!fork()) { abort(); exit(1); } break; } } /* open a menu */ static void menu_open(client_t *client, int x, int y) { client->x = x; client->y = y; XMoveWindow(display, client->frame, client->x, client->y); workspace_add_client(client->screen->desktop->current_space, client); desktop_add_client(client); XMapWindow(display, client->frame); stacking_raise(client); client->state = NormalState; } /* close a menu */ static void menu_close(menu_t *menu, client_t *client) { int snum = client->screen->num; /* close any open submenus */ if (menu->active_sub[snum]) { menu_close(menu->active_sub[snum], menu->active_sub[snum]->client[snum]); menu->active_sub[snum] = NULL; } if (client->workspace) { desktop_rm_client(client); workspace_rm_client(client); } XUnmapWindow(display, client->frame); client->state = WithdrawnState; } /* draw (or erase) a highlight */ static void drawhighlight(client_t *client, int idx) { #ifdef I18N XFillRectangle(display, client->window, client->screen->xorgc, 2, idx * (menufont_extents->max_ink_extent.height) + MENU_YBORDER, client->width - 5, menufont_extents->max_ink_extent.height); #else XFillRectangle(display, client->window, client->screen->xorgc, 2, idx * (menufont->ascent + menufont->descent) + MENU_YBORDER, client->width - 5, menufont->ascent + menufont->descent); #endif } /* open submenus or close them as neccessary while dragging across */ static void passopen(menu_t *menu, client_t *client, int entidx, int *oldentidx, int snum) { if (menu->entries[entidx]->type == ET_SUBMENU) { if (menu->active_sub[snum] != menu->submenus[menu->entries[entidx]->dat.submenu]) { if (menu->active_sub[snum]) menu_close(menu, menu->active_sub[snum]->client[snum]); menu->active_sub[snum] = menu->submenus[menu->entries[entidx]->dat.submenu]; #ifdef I18N menu_open(menu->active_sub[snum]->client[snum], client->x + FULLWIDTH(client), client->y + MENU_YBORDER + (entidx * (menufont_extents->max_ink_extent.height))); #else menu_open(menu->active_sub[snum]->client[snum], client->x + FULLWIDTH(client), client->y + MENU_YBORDER + (entidx * (menufont->ascent + menufont->descent))); #endif } *oldentidx = -1; } else { drawhighlight(client, entidx); if (menu->active_sub[snum]) { menu_close(menu, menu->active_sub[snum]->client[snum]); menu->active_sub[snum] = NULL; } *oldentidx = entidx; } } /* main menu operation routine, e == NULL if not from a keypress */ static void menu_interact(menu_t *menu, client_t *client, XButtonEvent *butev) { XEvent event; XMotionEvent *e; menu_t *tmpmenu; Window lastsubwin, dumwin; int xpos, ypos, snum; int oldentidx, entidx, mask; int moving; /* grab the pointer */ mask = ButtonReleaseMask | PointerMotionMask | ButtonMotionMask; if (XGrabPointer(display, client->screen->root, 0, mask, GrabModeAsync, GrabModeAsync, client->screen->root, None, CurrentTime) != GrabSuccess) return; stacking_raise(client); snum = client->screen->num; /* set the entry index var if we got a event structure */ if (butev) { #ifdef I18N oldentidx = entidx = (butev->y - MENU_YBORDER) / (menufont_extents->max_ink_extent.height); #else oldentidx = entidx = (butev->y - MENU_YBORDER) / (menufont->ascent + menufont->descent); #endif if (entidx >= 0 && entidx < menu->entry_count) { XSync(display, 0); while (XCheckMaskEvent(display, ExposureMask, &event)) event_handle(&event); passopen(menu, client, entidx, &oldentidx, snum); } else { oldentidx = entidx = -1; } } else { oldentidx = entidx = -1; } moving = 0; lastsubwin = -1; mask |= ExposureMask; while (1) { XMaskEvent(display, mask, &event); switch (event.type) { case MotionNotify: e = &event.xmotion; moving = 1; /* see if we moved to another window */ if (e->subwindow != lastsubwin && e->subwindow != client->frame) { lastsubwin = e->subwindow; if (oldentidx != -1) drawhighlight(client, oldentidx); if (e->subwindow == None || XFindContext(display, e->subwindow, menu_context, (XPointer *) &tmpmenu) != 0) { oldentidx = entidx = -1; break; } if (menu->active_sub[snum] && menu->active_sub[snum]->client[snum]->frame != e->subwindow) menu_close(menu, menu->active_sub[snum]->client[snum]); menu = tmpmenu; client = menu->client[snum]; oldentidx = -1; } /* get cordinates and erase old highlights, draw new if needed */ XTranslateCoordinates(display, e->root, client->window, e->x_root, e->y_root, &xpos, &ypos, &dumwin); if (ypos >= client->height - MENU_YBORDER || ypos <= MENU_YBORDER || xpos <= MENU_XBORDER || xpos >= client->width - MENU_XBORDER) { if (oldentidx != -1) drawhighlight(client, oldentidx); entidx = oldentidx = -1; break; } #ifdef I18N entidx = (ypos - MENU_YBORDER) / (menufont_extents->max_ink_extent.height); #else entidx = (ypos - MENU_YBORDER) / (menufont->ascent + menufont->descent); #endif if (entidx != oldentidx) { if (oldentidx != -1) drawhighlight(client, oldentidx); passopen(menu, client, entidx, &oldentidx, snum); } else oldentidx = entidx; break; case ButtonRelease: XUngrabPointer(display, CurrentTime); if (oldentidx != -1) drawhighlight(client, oldentidx); if (entidx != -1) { if (menu->entries[entidx]->type == ET_SUBMENU) return; menu_action(menu, menu->entries[entidx], snum); /* * for now, always close when doing an action. at some point * I may do break-off-able menus and such */ menu_close(menu->forefather, menu->forefather->client[snum]); return; } if (moving) menu_close(menu->forefather, menu->forefather->client[snum]); return; case Expose: event_handle(&event); break; } } } /* open or close a menu */ void menu_use(menu_t *menu, screen_t *screen) { Window dumwin; client_t *client; int dumint, x, y; client = menu->client[screen->num]; if (client->state == WithdrawnState) { XQueryPointer(display, screen->root, &dumwin, &dumwin, &x, &y, &dumint, &dumint, &dumint); menu_open(client, x - client->width / 2, y); menu_interact(menu, client, NULL); } else { /* need an if it's moved thing */ menu_close(menu, client); } } /* a click on a menu, go into interaction with the menu */ void menu_click(menu_t *menu, client_t *client, XButtonEvent *e) { menu_interact(menu, client, e); }